Skip to main content

sequoia_seq/
lower.rs

1use std::cell::Cell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Fill, ImageSource, Length, Point, Rect, Vec2};
5use sequoia_elements::{
6    Button, Checkbox, Container, DateField, DragValue, DropdownButton, Field, MultiSelect,
7    NumberField, ProgressBar, Slider1D, Switch, TextArea, TextField,
8};
9use sequoia_input::{CursorIcon, PointerButton, PointerEvent};
10use sequoia_reactive::{Signal, effect};
11use sequoia_style::{BoxStyle, ButtonStyle, ShadowSpec, Theme};
12use sequoia_view::{
13    Align, AnimSpec, AnimatedSize, AnyView, AsyncView, Axis, Canvas, Column, EventCtx, Grid,
14    GridFlow, GridPlacement, Icon, Image, Indents, Justify, Live, LiveOffset, LiveOpacity,
15    MAX_ANIM_DT, Margined, Motion, Overflow, Painter, Panel, Pulse, Rotated, Row, Scaled,
16    ScrollAxis, ScrollView, Shake, Spacer, SplitPanel, Stack, StackPos, Text, Track, View,
17    VirtualList, Wrap, when,
18};
19
20use crate::bindings::{ActionSig, FieldSignal, HostBindings, PropType, SeqRowData};
21use crate::error::SeqError;
22use crate::ir::{
23    ComponentDef, DeferredKind, LengthVal, SeqApp, SeqArg, SeqNode, SeqValue, SeqWindow, StyleDecl,
24    ThemeName,
25};
26use crate::value::Value;
27
28pub fn resolve_theme(file: &str, app: &SeqApp) -> Result<Theme, SeqError> {
29    match &app.theme {
30        ThemeName::Dark => Ok(Theme::dark()),
31        ThemeName::Light => Ok(Theme::light()),
32        ThemeName::File(path) => {
33            let full = theme_file_path(file, path);
34            Theme::from_tokens_file(full.to_str().unwrap_or(path))
35                .map_err(|e| SeqError::new(file, app.app_line, 1, format!("theme: {e}")))
36        }
37    }
38}
39
40fn theme_file_path(seq_file: &str, path: &str) -> std::path::PathBuf {
41    let requested = std::path::Path::new(path);
42    if requested.is_absolute() {
43        return requested.to_path_buf();
44    }
45    match std::path::Path::new(seq_file)
46        .parent()
47        .filter(|p| !p.as_os_str().is_empty())
48    {
49        Some(dir) => dir.join(requested),
50        None => requested.to_path_buf(),
51    }
52}
53
54#[derive(Clone, Copy)]
55pub(crate) enum Resolver<'a> {
56    Static,
57
58    Host(&'a HostBindings),
59}
60
61pub fn lower(file: &str, app: &SeqApp) -> Result<AnyView, SeqError> {
62    if let Some(d) = app.deferred.first() {
63        let what = match d.kind {
64            DeferredKind::Prop => "prop",
65            DeferredKind::On => "callback on",
66            DeferredKind::Style => "blok style",
67        };
68        return Err(SeqError::new(
69            file,
70            d.line,
71            1,
72            format!("{what} '{}' wymaga hosta Rust — inkrement 2", d.name),
73        ));
74    }
75    let resolved;
76    let app = if app.fns.is_empty() {
77        app
78    } else {
79        resolved = crate::resolve_seq_calls(app.clone());
80        &resolved
81    };
82    let app = expand_components(file, app)?;
83    let theme = resolve_theme(file, &app)?;
84    let window = app
85        .windows
86        .first()
87        .ok_or_else(|| SeqError::new(file, 1, 1, "app wymaga min. 1 bloku `window`".to_string()))?;
88    let styles = build_styles(file, &theme, &app.styles)?;
89    lower_window(file, &theme, &styles, window)
90}
91
92pub fn lower_with(file: &str, app: &SeqApp, bindings: &HostBindings) -> Result<AnyView, SeqError> {
93    check_decls(file, app, bindings)?;
94    let resolved;
95    let app = if app.fns.is_empty() {
96        app
97    } else {
98        resolved = crate::resolve_seq_calls(app.clone());
99        &resolved
100    };
101    let owned;
102    let bindings = if app.fns.is_empty() {
103        bindings
104    } else {
105        match bindings.get_seq() {
106            Some(rt) if rt.file() == file => {
107                rt.rebind(&app.fns);
108                bindings
109            }
110            _ => {
111                owned = bindings
112                    .clone()
113                    .with_seq(Rc::new(crate::eval::SeqRuntime::new(
114                        file,
115                        &app.fns,
116                        &app.states,
117                    )));
118                &owned
119            }
120        }
121    };
122    let app = expand_components(file, app)?;
123    let theme = resolve_theme(file, &app)?;
124    let window = app
125        .windows
126        .first()
127        .ok_or_else(|| SeqError::new(file, 1, 1, "app wymaga min. 1 bloku `window`".to_string()))?;
128    let styles = build_styles(file, &theme, &app.styles)?;
129    lower_window_with(file, &theme, &styles, window, bindings)
130}
131
132const MAX_COMP_DEPTH: usize = 32;
133
134const MAX_EXPANDED_NODES: usize = 100_000;
135
136fn take_budget(file: &str, node: &SeqNode, budget: &mut usize) -> Result<(), SeqError> {
137    *budget = budget.saturating_sub(1);
138    if *budget == 0 {
139        return Err(SeqError::new(
140            file,
141            node.line,
142            1,
143            format!("component expansion exceeded {MAX_EXPANDED_NODES} node budget"),
144        ));
145    }
146    Ok(())
147}
148
149pub fn expand_components(file: &str, app: &SeqApp) -> Result<SeqApp, SeqError> {
150    if app.components.is_empty() {
151        return Ok(app.clone());
152    }
153    let mut table: std::collections::HashMap<&str, &ComponentDef> =
154        std::collections::HashMap::new();
155    for c in &app.components {
156        table.insert(c.name.as_str(), c);
157    }
158    let mut out = app.clone();
159    let mut budget = MAX_EXPANDED_NODES;
160    for w in &mut out.windows {
161        w.nodes = expand_nodes(file, &table, &w.nodes, 0, &mut budget)?;
162    }
163    Ok(out)
164}
165
166fn expand_nodes(
167    file: &str,
168    table: &std::collections::HashMap<&str, &ComponentDef>,
169    nodes: &[SeqNode],
170    depth: usize,
171    budget: &mut usize,
172) -> Result<Vec<SeqNode>, SeqError> {
173    let mut out = Vec::new();
174    for n in nodes {
175        if let Some(def) = table.get(n.kind.as_str()) {
176            out.extend(expand_call(file, table, def, n, depth, budget)?);
177        } else {
178            take_budget(file, n, budget)?;
179            let mut n = n.clone();
180            n.children = expand_nodes(file, table, &n.children, depth, budget)?;
181            out.push(n);
182        }
183    }
184    Ok(out)
185}
186
187fn expand_call(
188    file: &str,
189    table: &std::collections::HashMap<&str, &ComponentDef>,
190    def: &ComponentDef,
191    node: &SeqNode,
192    depth: usize,
193    budget: &mut usize,
194) -> Result<Vec<SeqNode>, SeqError> {
195    if depth > MAX_COMP_DEPTH {
196        return Err(SeqError::new(
197            file,
198            node.line,
199            1,
200            format!("component nesting limit exceeded ({MAX_COMP_DEPTH})"),
201        ));
202    }
203
204    let mut pmap: std::collections::HashMap<&str, &SeqValue> = std::collections::HashMap::new();
205    let mut flex: Option<SeqValue> = None;
206    for arg in &node.args {
207        if arg.name == "flex" {
208            flex = Some(arg.value.clone());
209            continue;
210        }
211        let Some(param) = def.params.iter().find(|p| p.name == arg.name) else {
212            return Err(SeqError::new(
213                file,
214                arg.line,
215                1,
216                format!("nieznany parametr '{}' w {}", arg.name, def.name),
217            ));
218        };
219        pmap.insert(param.name.as_str(), &arg.value);
220    }
221    for p in &def.params {
222        if !pmap.contains_key(p.name.as_str()) {
223            return Err(SeqError::new(
224                file,
225                node.line,
226                1,
227                format!("brakuje parametru '{}' w {}", p.name, def.name),
228            ));
229        }
230    }
231
232    let kids = expand_nodes(file, table, &node.children, depth + 1, budget)?;
233
234    let body: Vec<SeqNode> = def.body.iter().map(|b| substitute_node(b, &pmap)).collect();
235    let mut body = splice_children(&body, &kids);
236
237    if let Some(flex_val) = flex {
238        if body.len() == 1 {
239            let root = body.pop().expect("one root");
240            body.push(with_flex(root, node.line, flex_val));
241        } else {
242            take_budget(file, node, budget)?;
243            body = vec![wrap_column(node.line, body, Some(flex_val))];
244        }
245    } else if body.len() != 1 {
246        take_budget(file, node, budget)?;
247        body = vec![wrap_column(node.line, body, None)];
248    }
249
250    expand_nodes(&def.file, table, &body, depth + 1, budget)
251}
252
253fn with_flex(mut node: SeqNode, line: usize, flex: SeqValue) -> SeqNode {
254    node.args.retain(|a| a.name != "flex");
255    node.args.push(SeqArg {
256        name: "flex".to_string(),
257        line,
258        value: flex,
259    });
260    node
261}
262
263fn wrap_column(line: usize, children: Vec<SeqNode>, flex: Option<SeqValue>) -> SeqNode {
264    let mut args = Vec::new();
265    if let Some(value) = flex {
266        args.push(SeqArg {
267            name: "flex".to_string(),
268            line,
269            value,
270        });
271    }
272    SeqNode {
273        kind: "Column".to_string(),
274        line,
275        label: None,
276        args,
277        children,
278    }
279}
280
281fn substitute_node(node: &SeqNode, pmap: &std::collections::HashMap<&str, &SeqValue>) -> SeqNode {
282    let mut out = node.clone();
283    out.args = out
284        .args
285        .into_iter()
286        .map(|mut a| {
287            a.value = substitute_value(&a.value, pmap);
288            a
289        })
290        .collect();
291    out.children = out
292        .children
293        .into_iter()
294        .map(|c| substitute_node(&c, pmap))
295        .collect();
296    out
297}
298
299fn substitute_value(
300    value: &SeqValue,
301    pmap: &std::collections::HashMap<&str, &SeqValue>,
302) -> SeqValue {
303    match value {
304        SeqValue::Ident(name) => pmap
305            .get(name.as_str())
306            .map(|v| (*v).clone())
307            .unwrap_or_else(|| value.clone()),
308        SeqValue::Tuple(items) => {
309            SeqValue::Tuple(items.iter().map(|v| substitute_value(v, pmap)).collect())
310        }
311        SeqValue::Array(items) => {
312            SeqValue::Array(items.iter().map(|v| substitute_value(v, pmap)).collect())
313        }
314        SeqValue::Callback { action, args } => SeqValue::Callback {
315            action: action.clone(),
316            args: args.iter().map(|v| substitute_value(v, pmap)).collect(),
317        },
318        SeqValue::CallSeq { name, args } => SeqValue::CallSeq {
319            name: name.clone(),
320            args: args.iter().map(|v| substitute_value(v, pmap)).collect(),
321        },
322        _ => value.clone(),
323    }
324}
325
326fn splice_children(nodes: &[SeqNode], kids: &[SeqNode]) -> Vec<SeqNode> {
327    let mut out = Vec::new();
328    for n in nodes {
329        if n.kind == "children" {
330            out.extend(kids.iter().cloned());
331        } else {
332            let mut n = n.clone();
333            n.children = splice_children(&n.children, kids);
334            out.push(n);
335        }
336    }
337    out
338}
339
340pub fn check_decls(file: &str, app: &SeqApp, bindings: &HostBindings) -> Result<(), SeqError> {
341    for d in &app.deferred {
342        match d.kind {
343            DeferredKind::Style => {
344                return Err(SeqError::new(
345                    file,
346                    d.line,
347                    1,
348                    format!("style block '{}' - style system later", d.name),
349                ));
350            }
351            DeferredKind::Prop => {
352                let want = d.sig.first().map(String::as_str).unwrap_or("");
353                let want = PropType::from_seq(want).ok_or_else(|| {
354                    SeqError::new(
355                        file,
356                        d.line,
357                        1,
358                        format!("nieznany typ prop '{want}' (String/Bool/Int/Float)"),
359                    )
360                })?;
361                match bindings.prop_type(&d.name) {
362                    Some(got) if got == want => {}
363                    Some(got) => {
364                        return Err(SeqError::new(
365                            file,
366                            d.line,
367                            1,
368                            format!(
369                                "prop '{}' is {} in .seq and host gave {}",
370                                d.name,
371                                want.seq_name(),
372                                got.seq_name()
373                            ),
374                        ));
375                    }
376                    None => {
377                        return Err(SeqError::new(
378                            file,
379                            d.line,
380                            1,
381                            format!(
382                                "prop '{}' ({}) without binding - HostBindings::prop_{}()",
383                                d.name,
384                                want.seq_name(),
385                                want.seq_name().to_lowercase()
386                            ),
387                        ));
388                    }
389                }
390            }
391            DeferredKind::On => {
392                if app.fns.iter().any(|f| f.name == d.name) {
393                    continue;
394                }
395                let want = match d
396                    .sig
397                    .iter()
398                    .map(String::as_str)
399                    .collect::<Vec<_>>()
400                    .as_slice()
401                {
402                    [] => ActionSig::A0,
403                    ["Int"] => ActionSig::Int,
404                    ["Float"] => ActionSig::Float,
405                    ["String"] => ActionSig::Str,
406                    ["Bool"] => ActionSig::Bool,
407                    ["Float", "Float"] => ActionSig::Point,
408                    ["Float", "Float", "String"] => ActionSig::PointNamed,
409                    ["Float", "Float", "Float"] => ActionSig::Wheel,
410                    [t] => {
411                        return Err(SeqError::new(
412                            file,
413                            d.line,
414                            1,
415                            format!("nieznany typ parametru '{t}' (Int/Float/String/Bool)"),
416                        ));
417                    }
418                    _ => {
419                        return Err(SeqError::new(
420                            file,
421                            d.line,
422                            1,
423                            format!(
424                                "callback '{}' — wspierane sygnatury: (), (Int/Float/String/Bool), (Float, Float), (Float, Float, String), (Float, Float, Float)",
425                                d.name
426                            ),
427                        ));
428                    }
429                };
430                match bindings.action_sig(&d.name) {
431                    Some(got)
432                        if got == want || (want == ActionSig::Wheel && got == ActionSig::Pinch) => {
433                    }
434                    Some(_) => {
435                        return Err(SeqError::new(
436                            file,
437                            d.line,
438                            1,
439                            format!("the signature on {} does not match the host", d.name),
440                        ));
441                    }
442                    None => {
443                        return Err(SeqError::new(
444                            file,
445                            d.line,
446                            1,
447                            format!(
448                                "callback '{}' without binding - HostBindings::on_..()",
449                                d.name
450                            ),
451                        ));
452                    }
453                }
454            }
455        }
456    }
457    Ok(())
458}
459
460#[derive(Clone, Copy, Debug, PartialEq, Eq)]
461pub enum TextWeight {
462    Bold,
463
464    Semi,
465
466    Medium,
467
468    Normal,
469
470    Light,
471
472    Black,
473}
474
475#[derive(Clone, Copy, Debug, Default)]
476pub struct SeqTextStyle {
477    pub size: Option<f32>,
478
479    pub color: Option<Color>,
480
481    pub weight: Option<TextWeight>,
482}
483
484#[derive(Clone, Debug, Default)]
485pub struct BuiltStyles {
486    boxes: std::collections::HashMap<String, BoxStyle>,
487    buttons: std::collections::HashMap<String, ButtonStyle>,
488    texts: std::collections::HashMap<String, SeqTextStyle>,
489}
490
491impl BuiltStyles {
492    pub fn empty() -> Self {
493        Self::default()
494    }
495
496    pub fn box_style(&self, name: &str) -> Option<BoxStyle> {
497        self.boxes.get(name).copied()
498    }
499
500    pub fn button_style(&self, name: &str) -> Option<ButtonStyle> {
501        self.buttons.get(name).copied()
502    }
503
504    pub fn text_style(&self, name: &str) -> Option<SeqTextStyle> {
505        self.texts.get(name).copied()
506    }
507}
508
509pub(crate) fn box_style_of(
510    file: &str,
511    styles: &BuiltStyles,
512    node: &SeqNode,
513) -> Result<Option<BoxStyle>, SeqError> {
514    let Some(arg) = node.args.iter().find(|a| a.name == "style") else {
515        return Ok(None);
516    };
517    let name = match &arg.value {
518        SeqValue::Ident(name) | SeqValue::Str(name) => name.clone(),
519        _ => {
520            return Err(SeqError::new(
521                file,
522                arg.line,
523                1,
524                "style: to nazwa bloku (style: Card)",
525            ));
526        }
527    };
528    styles.box_style(&name).map(Some).ok_or_else(|| {
529        SeqError::new(
530            file,
531            arg.line,
532            1,
533            format!("unknown style '{name}' (check `style` blocks)"),
534        )
535    })
536}
537
538fn text_style_of(
539    file: &str,
540    styles: &BuiltStyles,
541    node: &SeqNode,
542) -> Result<Option<SeqTextStyle>, SeqError> {
543    let Some(arg) = node.args.iter().find(|a| a.name == "style") else {
544        return Ok(None);
545    };
546    let name = match &arg.value {
547        SeqValue::Ident(name) | SeqValue::Str(name) => name.clone(),
548        _ => {
549            return Err(SeqError::new(
550                file,
551                arg.line,
552                1,
553                "style: to nazwa bloku (style: Card)",
554            ));
555        }
556    };
557    styles.text_style(&name).map(Some).ok_or_else(|| {
558        SeqError::new(
559            file,
560            arg.line,
561            1,
562            format!("unknown style '{name}' (check `style` blocks)"),
563        )
564    })
565}
566
567fn apply_seq_text_style(mut v: Text, s: SeqTextStyle) -> Text {
568    if let Some(size) = s.size {
569        v = v.size(size);
570    }
571    if let Some(color) = s.color {
572        v = v.color(color);
573    }
574    match s.weight {
575        Some(TextWeight::Bold) => v = v.bold(),
576        Some(TextWeight::Semi) => v = v.semibold(),
577        Some(TextWeight::Medium) => v = v.medium(),
578        Some(TextWeight::Normal) => v = v.normal(),
579        Some(TextWeight::Light) => v = v.light(),
580        Some(TextWeight::Black) => v = v.black(),
581        None => {}
582    }
583    v
584}
585
586pub fn build_styles(
587    file: &str,
588    theme: &Theme,
589    styles: &[StyleDecl],
590) -> Result<BuiltStyles, SeqError> {
591    let mut out = BuiltStyles::empty();
592    let res = Resolver::Static;
593    for s in styles {
594        match s.target.as_str() {
595            "Box" => {
596                let mut v = BoxStyle::new();
597                for base in base_chain(file, styles, s)? {
598                    apply_box_args(file, theme, &res, base, &mut v)?;
599                }
600                apply_box_args(file, theme, &res, s, &mut v)?;
601                out.boxes.insert(s.name.clone(), v);
602            }
603            "Button" => {
604                let mut v = ButtonStyle::new();
605                for base in base_chain(file, styles, s)? {
606                    if let Some(preset) = button_preset(&base.base) {
607                        v = preset;
608                    } else {
609                        apply_button_args(file, theme, &res, base, &mut v)?;
610                    }
611                }
612                if let Some(preset) = button_preset(&s.base) {
613                    v = preset;
614                }
615                apply_button_args(file, theme, &res, s, &mut v)?;
616                out.buttons.insert(s.name.clone(), v);
617            }
618            "Text" => {
619                let mut v = SeqTextStyle::default();
620                for base in base_chain(file, styles, s)? {
621                    apply_text_args(file, theme, &res, base, &mut v)?;
622                }
623                apply_text_args(file, theme, &res, s, &mut v)?;
624                out.texts.insert(s.name.clone(), v);
625            }
626
627            other => {
628                return Err(SeqError::new(
629                    file,
630                    s.line,
631                    1,
632                    format!(
633                        "style '{}': nieznany typ '{other}' (Box/Button/Text)",
634                        s.name
635                    ),
636                ));
637            }
638        }
639    }
640    Ok(out)
641}
642
643fn base_chain<'s>(
644    file: &str,
645    styles: &'s [StyleDecl],
646    start: &StyleDecl,
647) -> Result<Vec<&'s StyleDecl>, SeqError> {
648    let mut chain = Vec::new();
649    let mut next = start.base.clone();
650    let mut seen = vec![start.name.clone()];
651    while let Some(name) = next {
652        if start.target == "Button" && button_preset(&Some(name.clone())).is_some() {
653            break;
654        }
655        if seen.contains(&name) {
656            return Err(SeqError::new(
657                file,
658                start.line,
659                1,
660                format!("style '{}': cykliczne extends", start.name),
661            ));
662        }
663        seen.push(name.clone());
664        let Some(decl) = styles.iter().find(|o| o.name == name) else {
665            return Err(SeqError::new(
666                file,
667                start.line,
668                1,
669                format!("style '{}': nieznana baza '{name}'", start.name),
670            ));
671        };
672        if decl.target != start.target {
673            return Err(SeqError::new(
674                file,
675                start.line,
676                1,
677                format!(
678                    "style '{}': baza '{name}' to {}, a styl to {}",
679                    start.name, decl.target, start.target
680                ),
681            ));
682        }
683        chain.push(decl);
684        next = decl.base.clone();
685        if chain.len() > styles.len() {
686            return Err(SeqError::new(
687                file,
688                start.line,
689                1,
690                format!("style '{}': cykliczne extends", start.name),
691            ));
692        }
693    }
694    chain.reverse();
695    Ok(chain)
696}
697
698fn button_preset(base: &Option<String>) -> Option<ButtonStyle> {
699    match base.as_deref() {
700        Some("primary") => Some(ButtonStyle::PRIMARY),
701        Some("secondary") => Some(ButtonStyle::SECONDARY),
702        Some("danger") => Some(ButtonStyle::DANGER),
703        Some("ghost") => Some(ButtonStyle::GHOST),
704        Some("dark_filled") => Some(ButtonStyle::DARK_FILLED),
705        Some("outline_rose") => Some(ButtonStyle::OUTLINE_ROSE),
706        _ => None,
707    }
708}
709
710fn border_side(
711    file: &str,
712    theme: &Theme,
713    style: &str,
714    arg: &SeqArg,
715) -> Result<(f32, Color), SeqError> {
716    match &arg.value {
717        SeqValue::Array(items) if items.len() == 2 => {
718            let w = match &items[0] {
719                SeqValue::Num(n) => *n,
720                _ => {
721                    return Err(SeqError::new(
722                        file,
723                        arg.line,
724                        1,
725                        format!("'{style}' in style: [width, color]"),
726                    ));
727                }
728            };
729            let c = match &items[1] {
730                SeqValue::Color(hex) => hex_color(file, arg.line, hex)?,
731                SeqValue::Theme(f) => theme_color(file, arg.line, theme, f)?,
732                _ => {
733                    return Err(SeqError::new(
734                        file,
735                        arg.line,
736                        1,
737                        format!("'{style}' in style: [width, color]"),
738                    ));
739                }
740            };
741            Ok((w, c))
742        }
743        _ => Err(SeqError::new(
744            file,
745            arg.line,
746            1,
747            format!("'{style}' in style: [width, color]"),
748        )),
749    }
750}
751
752fn shadow_spec(
753    file: &str,
754    theme: &Theme,
755    style: &str,
756    arg: &SeqArg,
757) -> Result<ShadowSpec, SeqError> {
758    let err = || {
759        SeqError::new(
760            file,
761            arg.line,
762            1,
763            format!("'{style}' w style: [kolor, blur] albo [kolor, blur, ox, oy]"),
764        )
765    };
766    let SeqValue::Array(items) = &arg.value else {
767        return Err(err());
768    };
769    if items.len() != 2 && items.len() != 4 {
770        return Err(err());
771    }
772    let color = match &items[0] {
773        SeqValue::Color(hex) => hex_color(file, arg.line, hex)?,
774        SeqValue::Theme(f) => theme_color(file, arg.line, theme, f)?,
775        _ => return Err(err()),
776    };
777    let num_at = |i: usize| match &items[i] {
778        SeqValue::Num(n) if n.is_finite() => Ok(*n),
779        _ => Err(err()),
780    };
781    let blur = num_at(1)?;
782    let mut spec = ShadowSpec::new(color, blur);
783    if items.len() == 4 {
784        spec = spec.offset(num_at(2)?, num_at(3)?);
785    }
786    Ok(spec)
787}
788
789fn apply_box_args(
790    file: &str,
791    theme: &Theme,
792    res: &Resolver<'_>,
793    decl: &StyleDecl,
794    v: &mut BoxStyle,
795) -> Result<(), SeqError> {
796    for arg in &decl.args {
797        match arg.name.as_str() {
798            "background" => *v = v.background(color(file, theme, arg)?),
799            "radius" => *v = v.radius(num(file, arg, res, None)?),
800            "border" => {
801                let (w, c) = border_pair(file, theme, arg)?;
802                *v = v.border(w, c);
803            }
804            "border_left" => {
805                let (w, c) = border_side(file, theme, "border_left", arg)?;
806                *v = v.border_left(w, c);
807            }
808            "border_right" => {
809                let (w, c) = border_side(file, theme, "border_right", arg)?;
810                *v = v.border_right(w, c);
811            }
812            "border_top" => {
813                let (w, c) = border_side(file, theme, "border_top", arg)?;
814                *v = v.border_top(w, c);
815            }
816            "border_bottom" => {
817                let (w, c) = border_side(file, theme, "border_bottom", arg)?;
818                *v = v.border_bottom(w, c);
819            }
820            "border_x" => {
821                let (w, c) = border_side(file, theme, "border_x", arg)?;
822                *v = v.border_x(w, c);
823            }
824            "border_y" => {
825                let (w, c) = border_side(file, theme, "border_y", arg)?;
826                *v = v.border_y(w, c);
827            }
828            "shadow" => *v = v.shadow(shadow_spec(file, theme, "shadow", arg)?),
829            "inset_shadow" => {
830                *v = v.inset_shadow(shadow_spec(file, theme, "inset_shadow", arg)?);
831            }
832            _ => {
833                return Err(SeqError::new(
834                    file,
835                    arg.line,
836                    1,
837                    format!("nieznane pole '{}' w style Box", arg.name),
838                ));
839            }
840        }
841    }
842    Ok(())
843}
844
845fn apply_button_args(
846    file: &str,
847    theme: &Theme,
848    res: &Resolver<'_>,
849    decl: &StyleDecl,
850    v: &mut ButtonStyle,
851) -> Result<(), SeqError> {
852    for arg in &decl.args {
853        match arg.name.as_str() {
854            "background" => *v = v.background(color(file, theme, arg)?),
855            "hover" => *v = v.hover(color(file, theme, arg)?),
856            "pressed" => *v = v.pressed(color(file, theme, arg)?),
857            "disabled" => *v = v.disabled(color(file, theme, arg)?),
858            "text" => *v = v.text(color(file, theme, arg)?),
859            "text_disabled" => *v = v.text_disabled(color(file, theme, arg)?),
860            "font_size" => *v = v.font_size(num(file, arg, res, None)?),
861            "font_weight" => {
862                let n = num(file, arg, res, None)?;
863                if !(n.is_finite() && n.fract() == 0.0 && (0.0..=1000.0).contains(&n)) {
864                    return Err(SeqError::new(
865                        file,
866                        arg.line,
867                        1,
868                        "'font_weight' in style is an integer 0..=1000",
869                    ));
870                }
871                #[allow(clippy::cast_possible_truncation)]
872                let w = n as u16;
873                *v = v.font_weight(w);
874            }
875            "radius" => *v = v.radius(num(file, arg, res, None)?),
876            "padding" => match &arg.value {
877                SeqValue::Array(items) if items.len() == 2 => {
878                    let (Some(x), Some(y)) = (array_num(&items[0]), array_num(&items[1])) else {
879                        return Err(SeqError::new(
880                            file,
881                            arg.line,
882                            1,
883                            "'padding' w style: [x, y]",
884                        ));
885                    };
886                    *v = v.padding_xy(x, y);
887                }
888                _ => {
889                    return Err(SeqError::new(
890                        file,
891                        arg.line,
892                        1,
893                        "'padding' w style: [x, y]",
894                    ));
895                }
896            },
897            "border" => {
898                let (w, c) = border_pair(file, theme, arg)?;
899                *v = v.border(w, c);
900            }
901            "border_width" => *v = v.border_width(num(file, arg, res, None)?),
902            "border_color" => *v = v.border_color(color(file, theme, arg)?),
903            _ => {
904                return Err(SeqError::new(
905                    file,
906                    arg.line,
907                    1,
908                    format!("nieznane pole '{}' w style Button", arg.name),
909                ));
910            }
911        }
912    }
913    Ok(())
914}
915
916fn array_num(value: &SeqValue) -> Option<f32> {
917    match value {
918        SeqValue::Num(n) if n.is_finite() => Some(*n),
919        _ => None,
920    }
921}
922
923fn apply_text_args(
924    file: &str,
925    theme: &Theme,
926    res: &Resolver<'_>,
927    decl: &StyleDecl,
928    v: &mut SeqTextStyle,
929) -> Result<(), SeqError> {
930    for arg in &decl.args {
931        match arg.name.as_str() {
932            "size" => v.size = Some(num(file, arg, res, None)?),
933            "color" => v.color = Some(color(file, theme, arg)?),
934            "bold" => v.weight = Some(TextWeight::Bold),
935            "semi" => v.weight = Some(TextWeight::Semi),
936            "medium" => v.weight = Some(TextWeight::Medium),
937            "normal" => v.weight = Some(TextWeight::Normal),
938            "light" => v.weight = Some(TextWeight::Light),
939            "black" => v.weight = Some(TextWeight::Black),
940            _ => {
941                return Err(SeqError::new(
942                    file,
943                    arg.line,
944                    1,
945                    format!("nieznane pole '{}' w style Text", arg.name),
946                ));
947            }
948        }
949    }
950    Ok(())
951}
952
953pub fn lower_window(
954    file: &str,
955    theme: &Theme,
956    styles: &BuiltStyles,
957    window: &SeqWindow,
958) -> Result<AnyView, SeqError> {
959    lower_window_res(file, theme, styles, window, &Resolver::Static)
960}
961
962pub fn lower_window_with(
963    file: &str,
964    theme: &Theme,
965    styles: &BuiltStyles,
966    window: &SeqWindow,
967    bindings: &HostBindings,
968) -> Result<AnyView, SeqError> {
969    lower_window_res(file, theme, styles, window, &Resolver::Host(bindings))
970}
971
972fn lower_window_res(
973    file: &str,
974    theme: &Theme,
975    styles: &BuiltStyles,
976    window: &SeqWindow,
977    res: &Resolver<'_>,
978) -> Result<AnyView, SeqError> {
979    let mut kids = Vec::new();
980    for n in &window.nodes {
981        kids.push(lower_node(file, theme, styles, n, res, None)?);
982    }
983    let mut col = Column::new().fill_size();
984    for k in kids {
985        col = col.child(k);
986    }
987    Ok(AnyView::new(col))
988}
989
990fn lower_node(
991    file: &str,
992    theme: &Theme,
993    styles: &BuiltStyles,
994    node: &SeqNode,
995    res: &Resolver<'_>,
996    row: Option<&SeqRowData>,
997) -> Result<AnyView, SeqError> {
998    for arg in &node.args {
999        if let SeqValue::Callback { action, .. } = &arg.value {
1000            match res {
1001                Resolver::Static => {
1002                    return Err(SeqError::new(
1003                        file,
1004                        arg.line,
1005                        1,
1006                        format!("callback '{action}' wymaga hosta Rust — inkrement 2"),
1007                    ));
1008                }
1009                Resolver::Host(_) => {}
1010            }
1011        }
1012        if let SeqValue::CallSeq { name, .. } = &arg.value {
1013            match res {
1014                Resolver::Static => {
1015                    return Err(SeqError::new(
1016                        file,
1017                        arg.line,
1018                        1,
1019                        format!("funkcja '{name}' (.seq) wymaga hosta Rust (lower_with)"),
1020                    ));
1021                }
1022                Resolver::Host(_) => {}
1023            }
1024        }
1025        if let SeqValue::Dynamic(name) = &arg.value {
1026            match res {
1027                Resolver::Static => {
1028                    return Err(SeqError::new(
1029                        file,
1030                        arg.line,
1031                        1,
1032                        format!("dynamic({name}) wymaga hosta Rust — inkrement 2"),
1033                    ));
1034                }
1035
1036                Resolver::Host(_) => {}
1037            }
1038        }
1039        if let SeqValue::Env(name) = &arg.value {
1040            match res {
1041                Resolver::Static => {
1042                    return Err(SeqError::new(
1043                        file,
1044                        arg.line,
1045                        1,
1046                        format!("env.{name} wymaga hosta Rust (HostBindings::env_..)"),
1047                    ));
1048                }
1049                Resolver::Host(_) => {}
1050            }
1051        }
1052        if let SeqValue::ItemField(field) = &arg.value {
1053            match res {
1054                Resolver::Static => {
1055                    return Err(SeqError::new(
1056                        file,
1057                        arg.line,
1058                        1,
1059                        format!("item.{field} wymaga hosta Rust — wiersze List (inkrement 3)"),
1060                    ));
1061                }
1062
1063                Resolver::Host(_) => {}
1064            }
1065        }
1066    }
1067    let built = match node.kind.as_str() {
1068        "Column" => {
1069            if has_click(node) {
1070                return maybe_indent(
1071                    lower_clickable_flex(file, theme, styles, node, res, row, Column::new())?,
1072                    file,
1073                    node,
1074                    res,
1075                    row,
1076                );
1077            }
1078            let v = apply_flex_box(file, theme, node, Column::new(), res, row)?;
1079            Ok(boxed_children(file, theme, styles, node, v, res, row)?)
1080        }
1081        "Row" => {
1082            if has_click(node) {
1083                return maybe_indent(
1084                    lower_clickable_flex(file, theme, styles, node, res, row, Row::new())?,
1085                    file,
1086                    node,
1087                    res,
1088                    row,
1089                );
1090            }
1091            let v = apply_flex_box(file, theme, node, Row::new(), res, row)?;
1092            Ok(boxed_children(file, theme, styles, node, v, res, row)?)
1093        }
1094        "Container" => {
1095            let mut v = Container::new();
1096            if let Some(s) = box_style_of(file, styles, node)? {
1097                v = v.box_style(s);
1098            }
1099            for arg in &node.args {
1100                match arg.name.as_str() {
1101                    "style" => {}
1102                    "width" => v = v.width(len(file, arg, res, row)?),
1103                    "height" => v = v.height(len(file, arg, res, row)?),
1104                    "padding" => v = apply_padding(v, file, arg, res, row)?,
1105                    "padding_x" => v = v.padding_x(num(file, arg, res, row)?),
1106                    "padding_y" => v = v.padding_y(num(file, arg, res, row)?),
1107                    "background" => v = v.background(color(file, theme, arg)?),
1108                    "flex" => v = v.flex(num(file, arg, res, row)?),
1109                    "radius" => v = v.radius(num(file, arg, res, row)?),
1110                    "border" => {
1111                        let (w, c) = border_pair(file, theme, arg)?;
1112                        v = v.border(w, c);
1113                    }
1114                    "border_left" => {
1115                        let (w, c) = border_side(file, theme, "border_left", arg)?;
1116                        v = v.border_left(w, c);
1117                    }
1118                    "border_right" => {
1119                        let (w, c) = border_side(file, theme, "border_right", arg)?;
1120                        v = v.border_right(w, c);
1121                    }
1122                    "border_top" => {
1123                        let (w, c) = border_side(file, theme, "border_top", arg)?;
1124                        v = v.border_top(w, c);
1125                    }
1126                    "border_bottom" => {
1127                        let (w, c) = border_side(file, theme, "border_bottom", arg)?;
1128                        v = v.border_bottom(w, c);
1129                    }
1130                    "border_x" => {
1131                        let (w, c) = border_side(file, theme, "border_x", arg)?;
1132                        v = v.border_x(w, c);
1133                    }
1134                    "border_y" => {
1135                        let (w, c) = border_side(file, theme, "border_y", arg)?;
1136                        v = v.border_y(w, c);
1137                    }
1138                    "min_width" => v = v.min_width(num(file, arg, res, row)?),
1139                    "max_width" => v = v.max_width(num(file, arg, res, row)?),
1140                    "min_height" => v = v.min_height(num(file, arg, res, row)?),
1141                    "max_height" => v = v.max_height(num(file, arg, res, row)?),
1142                    "margin" => v = apply_container_margin(v, file, arg, res, row)?,
1143                    "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
1144                    "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
1145                    "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
1146                    "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
1147                    "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
1148                    "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
1149                    "overflow" => v = v.overflow(overflow_val(file, arg)?),
1150                    "on_click" => match res {
1151                        Resolver::Static => {
1152                            return Err(SeqError::new(
1153                                file,
1154                                arg.line,
1155                                1,
1156                                "on_click wymaga hosta Rust",
1157                            ));
1158                        }
1159                        Resolver::Host(b) => match &arg.value {
1160                            SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
1161                                let fire = wire_action(file, arg.line, b, &arg.value, row)?;
1162                                v = v.on_click(move || fire());
1163                            }
1164                            _ => {
1165                                return Err(SeqError::new(
1166                                    file,
1167                                    arg.line,
1168                                    1,
1169                                    "on_click: { akcja(args) }",
1170                                ));
1171                            }
1172                        },
1173                    },
1174                    "hover" => v = v.hover(color(file, theme, arg)?),
1175                    "hit" => v = v.hit_testable(flag(file, arg, res, row)?),
1176                    "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right"
1177                    | "indent_top" | "indent_bottom" => {}
1178                    _ => unreachable_checked(file, node, arg)?,
1179                }
1180            }
1181            if let Some(kid) = single_child(file, theme, styles, node, res, row)? {
1182                v = v.child(kid);
1183            }
1184            Ok(AnyView::new(v))
1185        }
1186        "Panel" => {
1187            if !node.children.is_empty() {
1188                return Err(SeqError::new(
1189                    file,
1190                    node.line,
1191                    1,
1192                    "Panel is a leaf (only background/frame, no children) - put content in Container or Column",
1193                ));
1194            }
1195            let mut v = Panel::new();
1196            if let Some(s) = box_style_of(file, styles, node)? {
1197                v = v.box_style(s);
1198            }
1199            for arg in &node.args {
1200                match arg.name.as_str() {
1201                    "style" => {}
1202                    "width" => v = v.width(len(file, arg, res, row)?),
1203                    "height" => v = v.height(len(file, arg, res, row)?),
1204                    "padding" => v = apply_padding(v, file, arg, res, row)?,
1205                    "padding_x" => v = v.padding_x(num(file, arg, res, row)?),
1206                    "padding_y" => v = v.padding_y(num(file, arg, res, row)?),
1207                    "background" => v = v.background(color(file, theme, arg)?),
1208                    "flex" => v = v.flex(num(file, arg, res, row)?),
1209                    "radius" => v = v.radius(num(file, arg, res, row)?),
1210                    "border" => {
1211                        let (w, c) = border_pair(file, theme, arg)?;
1212                        v = v.border(w, c);
1213                    }
1214                    "border_left" => {
1215                        let (w, c) = border_side(file, theme, "border_left", arg)?;
1216                        v = v.border_left(w, c);
1217                    }
1218                    "border_right" => {
1219                        let (w, c) = border_side(file, theme, "border_right", arg)?;
1220                        v = v.border_right(w, c);
1221                    }
1222                    "border_top" => {
1223                        let (w, c) = border_side(file, theme, "border_top", arg)?;
1224                        v = v.border_top(w, c);
1225                    }
1226                    "border_bottom" => {
1227                        let (w, c) = border_side(file, theme, "border_bottom", arg)?;
1228                        v = v.border_bottom(w, c);
1229                    }
1230                    "border_x" => {
1231                        let (w, c) = border_side(file, theme, "border_x", arg)?;
1232                        v = v.border_x(w, c);
1233                    }
1234                    "border_y" => {
1235                        let (w, c) = border_side(file, theme, "border_y", arg)?;
1236                        v = v.border_y(w, c);
1237                    }
1238                    "min_width" => v = v.min_width(num(file, arg, res, row)?),
1239                    "max_width" => v = v.max_width(num(file, arg, res, row)?),
1240                    "min_height" => v = v.min_height(num(file, arg, res, row)?),
1241                    "max_height" => v = v.max_height(num(file, arg, res, row)?),
1242                    "margin" => v = apply_panel_margin(v, file, arg, res, row)?,
1243                    "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
1244                    "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
1245                    "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
1246                    "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
1247                    "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
1248                    "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
1249                    "overflow" => v = v.overflow(overflow_val(file, arg)?),
1250                    "hit" => v = v.hit_testable(flag(file, arg, res, row)?),
1251                    "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right"
1252                    | "indent_top" | "indent_bottom" => {}
1253                    _ => unreachable_checked(file, node, arg)?,
1254                }
1255            }
1256            Ok(AnyView::new(v))
1257        }
1258        "ScrollView" => {
1259            let mut axis = ScrollAxis::Vertical;
1260            for arg in &node.args {
1261                match arg.name.as_str() {
1262                    "axis" => {
1263                        axis = match ident(file, arg)?.as_str() {
1264                            "vertical" => ScrollAxis::Vertical,
1265                            "horizontal" => ScrollAxis::Horizontal,
1266                            "both" => ScrollAxis::Both,
1267                            other => {
1268                                return Err(SeqError::new(
1269                                    file,
1270                                    arg.line,
1271                                    1,
1272                                    format!("axis to vertical/horizontal/both, nie '{other}'"),
1273                                ));
1274                            }
1275                        };
1276                    }
1277                    "width" | "height" | "padding" | "padding_x" | "padding_y" | "background" => {
1278                        return Err(SeqError::new(
1279                            file,
1280                            arg.line,
1281                            1,
1282                            format!(
1283                                "propka '{}' on ScrollView in increment 1 - wrap content in Column",
1284                                arg.name
1285                            ),
1286                        ));
1287                    }
1288                    "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right"
1289                    | "indent_top" | "indent_bottom" => {}
1290                    _ => unreachable_checked(file, node, arg)?,
1291                }
1292            }
1293            let inner = match node.children.len() {
1294                0 => AnyView::new(Column::new()),
1295                1 => lower_node(file, theme, styles, &node.children[0], res, row)?,
1296                _ => {
1297                    let mut col = Column::new();
1298                    for c in &node.children {
1299                        let kid = lower_node(file, theme, styles, c, res, row)?;
1300                        col = match child_flex_weight(file, res, row, c)? {
1301                            Some(w) => col.child_flex(kid, w),
1302                            None => col.child(kid),
1303                        };
1304                    }
1305                    AnyView::new(col)
1306                }
1307            };
1308            let mut v = ScrollView::new(inner);
1309            v = match axis {
1310                ScrollAxis::Vertical => v.vertical(),
1311                ScrollAxis::Horizontal => v.horizontal(),
1312                _ => v.axis(axis),
1313            };
1314            Ok(AnyView::new(v))
1315        }
1316        "Text" => lower_text(file, theme, styles, node, res, row),
1317        "Button" => lower_button(file, styles, node, res, row),
1318        "Spacer" => {
1319            let mut flex = 1.0;
1320            let mut hit: Option<bool> = None;
1321            let mut width: Option<Length> = None;
1322            let mut height: Option<Length> = None;
1323            let mut min_width: Option<f32> = None;
1324            let mut max_width: Option<f32> = None;
1325            let mut min_height: Option<f32> = None;
1326            let mut max_height: Option<f32> = None;
1327            for arg in &node.args {
1328                match arg.name.as_str() {
1329                    "flex" => flex = num(file, arg, res, row)?,
1330                    "width" => width = Some(len(file, arg, res, row)?),
1331                    "height" => height = Some(len(file, arg, res, row)?),
1332                    "min_width" => min_width = Some(num(file, arg, res, row)?),
1333                    "max_width" => max_width = Some(num(file, arg, res, row)?),
1334                    "min_height" => min_height = Some(num(file, arg, res, row)?),
1335                    "max_height" => max_height = Some(num(file, arg, res, row)?),
1336                    "hit" => hit = Some(flag(file, arg, res, row)?),
1337                    "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right"
1338                    | "indent_top" | "indent_bottom" => {}
1339                    _ => unreachable_checked(file, node, arg)?,
1340                }
1341            }
1342            let mut v = Spacer::flex(flex);
1343            if let Some(on) = hit {
1344                v = v.hit_testable(on);
1345            }
1346            if let Some(w) = width {
1347                v = v.width(w);
1348            }
1349            if let Some(h) = height {
1350                v = v.height(h);
1351            }
1352            if let Some(x) = min_width {
1353                v = v.min_width(x);
1354            }
1355            if let Some(x) = max_width {
1356                v = v.max_width(x);
1357            }
1358            if let Some(x) = min_height {
1359                v = v.min_height(x);
1360            }
1361            if let Some(x) = max_height {
1362                v = v.max_height(x);
1363            }
1364            Ok(AnyView::new(v))
1365        }
1366        "Icon" => {
1367            let name = opt_str(file, node, "name", res, row)?.ok_or_else(|| {
1368                SeqError::new(file, node.line, 1, "Icon wymaga name: \"...\"".to_string())
1369            })?;
1370            let mut v = Icon::new(name);
1371            for arg in &node.args {
1372                match arg.name.as_str() {
1373                    "name" => {}
1374                    "size" => v = v.size(num(file, arg, res, row)?),
1375                    "color" => v = v.color(color(file, theme, arg)?),
1376                    "width" => v = v.width(len(file, arg, res, row)?),
1377                    "height" => v = v.height(len(file, arg, res, row)?),
1378                    "min_width" => v = v.min_width(num(file, arg, res, row)?),
1379                    "max_width" => v = v.max_width(num(file, arg, res, row)?),
1380                    "min_height" => v = v.min_height(num(file, arg, res, row)?),
1381                    "max_height" => v = v.max_height(num(file, arg, res, row)?),
1382                    "margin" => v = apply_icon_margin(v, file, arg, res, row)?,
1383                    "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
1384                    "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
1385                    "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
1386                    "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
1387                    "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
1388                    "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
1389                    "flex" => {}
1390                    "hit" => v = v.hit_testable(flag(file, arg, res, row)?),
1391                    "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right"
1392                    | "indent_top" | "indent_bottom" => {}
1393                    _ => unreachable_checked(file, node, arg)?,
1394                }
1395            }
1396            Ok(AnyView::new(v))
1397        }
1398        "Image" => {
1399            let path = opt_str(file, node, "path", res, row)?
1400                .or_else(|| node.label.clone())
1401                .ok_or_else(|| {
1402                    SeqError::new(file, node.line, 1, "Image wymaga path: \"...\"".to_string())
1403                })?;
1404            let mut v = Image::file(path);
1405            for arg in &node.args {
1406                match arg.name.as_str() {
1407                    "path" => {}
1408                    "width" => v = v.width(len(file, arg, res, row)?),
1409                    "height" => v = v.height(len(file, arg, res, row)?),
1410                    "min_width" => v = v.min_width(num(file, arg, res, row)?),
1411                    "max_width" => v = v.max_width(num(file, arg, res, row)?),
1412                    "min_height" => v = v.min_height(num(file, arg, res, row)?),
1413                    "max_height" => v = v.max_height(num(file, arg, res, row)?),
1414                    "margin" => v = apply_image_margin(v, file, arg, res, row)?,
1415                    "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
1416                    "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
1417                    "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
1418                    "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
1419                    "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
1420                    "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
1421
1422                    "flex" => {}
1423                    "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right"
1424                    | "indent_top" | "indent_bottom" => {}
1425                    _ => unreachable_checked(file, node, arg)?,
1426                }
1427            }
1428            Ok(AnyView::new(v))
1429        }
1430        "List" => lower_list(file, theme, styles, node, res),
1431        "VirtualList" => lower_virtual_list(file, theme, styles, node, res, row),
1432        "SplitPanel" => lower_split(file, theme, styles, node, res, row),
1433        "Canvas" => lower_canvas(file, theme, node, res, row),
1434        "Checkbox" => lower_checkbox(file, theme, node, res, row),
1435        "Switch" => lower_switch(file, theme, node, res, row),
1436        "TextField" => lower_textfield(file, theme, node, res, row),
1437        "TextArea" => lower_textarea(file, theme, node, res, row),
1438        "Field" => lower_form_field(file, theme, node, res, row),
1439        "NumberField" => lower_number_field(file, theme, node, res, row),
1440        "DateField" => lower_date_field(file, theme, node, res, row),
1441        "MultiSelect" => lower_multi_select(file, theme, node, res, row),
1442        "Slider" => lower_slider(file, theme, node, res, row),
1443        "DragValue" => lower_drag_value(file, theme, node, res, row),
1444        "Dropdown" => lower_dropdown(file, theme, node, res, row),
1445        "Progress" => lower_progress(file, theme, node, res, row),
1446        "Stack" => lower_stack(file, theme, styles, node, res, row),
1447        "Grid" => lower_grid(file, theme, styles, node, res, row),
1448        "Wrap" => lower_wrap(file, theme, styles, node, res, row),
1449        "If" => lower_if(file, theme, styles, node, res, row),
1450        "Width" => lower_width(file, theme, styles, node, res, row),
1451        "Height" => lower_height(file, theme, styles, node, res, row),
1452        "Opacity" => lower_opacity(file, theme, styles, node, res, row),
1453        "Scale" => lower_scale(file, theme, styles, node, res, row),
1454        "Rotate" => lower_rotate(file, theme, styles, node, res, row),
1455        "Translate" => lower_translate(file, theme, styles, node, res, row),
1456        "Pulse" => lower_pulse(file, theme, styles, node, res, row),
1457        "Shake" => lower_shake(file, theme, styles, node, res, row),
1458        "Async" => lower_async(file, theme, styles, node, res, row),
1459        "Effect" => lower_effect(file, node, res, row),
1460        other => Err(SeqError::new(
1461            file,
1462            node.line,
1463            1,
1464            format!("nieznany komponent '{other}'"),
1465        )),
1466    };
1467    maybe_indent(built?, file, node, res, row)
1468}
1469
1470fn apply_flex_box<V: FlexBoxOps>(
1471    file: &str,
1472    theme: &Theme,
1473    node: &SeqNode,
1474    mut v: V,
1475    res: &Resolver<'_>,
1476    row: Option<&SeqRowData>,
1477) -> Result<V, SeqError> {
1478    for arg in &node.args {
1479        v = match arg.name.as_str() {
1480            "width" => v.set_width(len(file, arg, res, row)?),
1481            "height" => v.set_height(len(file, arg, res, row)?),
1482            "padding" => v.set_padding(padding_val(file, arg, res, row)?),
1483            "padding_x" => v.set_padding_x(num(file, arg, res, row)?),
1484            "padding_y" => v.set_padding_y(num(file, arg, res, row)?),
1485            "background" => v.set_background(color(file, theme, arg)?),
1486            "flex" => v.set_flex(num(file, arg, res, row)?),
1487            "gap" => v.set_gap(num(file, arg, res, row)?),
1488            "main_gap" => v.set_gap(num(file, arg, res, row)?),
1489            "cross_gap" => v.set_cross_gap(num(file, arg, res, row)?),
1490            "wrap" => v.set_wrap(flag(file, arg, res, row)?),
1491            "align_content" => v.set_align(align_val(file, arg)?),
1492            "align" => v.set_align(align_val(file, arg)?),
1493            "justify" => v.set_justify(justify_val(file, arg)?),
1494            "min_width" => v.set_min_width(num(file, arg, res, row)?),
1495            "max_width" => v.set_max_width(num(file, arg, res, row)?),
1496            "min_height" => v.set_min_height(num(file, arg, res, row)?),
1497            "max_height" => v.set_max_height(num(file, arg, res, row)?),
1498            "margin" => v.set_margin(margin_val(file, arg, res, row)?),
1499            "margin_x" => v.set_margin_x(num(file, arg, res, row)?),
1500            "margin_y" => v.set_margin_y(num(file, arg, res, row)?),
1501            "margin_left" => v.set_margin_left(num(file, arg, res, row)?),
1502            "margin_right" => v.set_margin_right(num(file, arg, res, row)?),
1503            "margin_top" => v.set_margin_top(num(file, arg, res, row)?),
1504            "margin_bottom" => v.set_margin_bottom(num(file, arg, res, row)?),
1505            "overflow" => v.set_overflow(overflow_val(file, arg)?),
1506            "hit" => v.set_hit_testable(flag(file, arg, res, row)?),
1507            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
1508            | "indent_bottom" => v,
1509            _ => return unreachable_checked(file, node, arg),
1510        };
1511    }
1512    Ok(v)
1513}
1514
1515fn lower_text(
1516    file: &str,
1517    theme: &Theme,
1518    styles: &BuiltStyles,
1519    node: &SeqNode,
1520    res: &Resolver<'_>,
1521    row: Option<&SeqRowData>,
1522) -> Result<AnyView, SeqError> {
1523    if let Some(t_arg) = find_arg(node, "t") {
1524        match res {
1525            Resolver::Static => {
1526                return Err(SeqError::new(
1527                    file,
1528                    t_arg.line,
1529                    1,
1530                    "t: wymaga hosta Rust (HostBindings::i18n)",
1531                ));
1532            }
1533            Resolver::Host(b) => match &t_arg.value {
1534                SeqValue::Str(key) => {
1535                    let i18n = b.get_i18n().ok_or_else(|| {
1536                        SeqError::new(file, t_arg.line, 1, "t: wymaga HostBindings::i18n()")
1537                    })?;
1538                    let key = key.clone();
1539                    let mut v = Text::with(move || i18n.t(&key));
1540                    v = apply_text_box(file, styles, &mut v, node, res, row, theme)?;
1541                    return Ok(AnyView::new(v));
1542                }
1543                _ => {
1544                    return Err(SeqError::new(
1545                        file,
1546                        t_arg.line,
1547                        1,
1548                        "t: is the key in quotation marks (t: \"save\")",
1549                    ));
1550                }
1551            },
1552        }
1553    }
1554
1555    for arg in &node.args {
1556        if arg.name == "dynamic" {
1557            return match res {
1558                Resolver::Static => Err(SeqError::new(
1559                    file,
1560                    arg.line,
1561                    1,
1562                    "dynamic: wymaga hosta Rust — inkrement 2",
1563                )),
1564                Resolver::Host(b) => match &arg.value {
1565                    SeqValue::Dynamic(name) | SeqValue::Ident(name) => {
1566                        let read = display_of(file, arg.line, b, name)?;
1567                        let mut v = Text::with(move || read());
1568                        v = apply_text_box(file, styles, &mut v, node, res, row, theme)?;
1569                        Ok(AnyView::new(v))
1570                    }
1571                    SeqValue::Env(name) => {
1572                        let read = display_env_of(file, arg.line, b, name)?;
1573                        let mut v = Text::with(move || read());
1574                        v = apply_text_box(file, styles, &mut v, node, res, row, theme)?;
1575                        Ok(AnyView::new(v))
1576                    }
1577                    SeqValue::ItemField(field) => {
1578                        let read = display_field(file, arg.line, row, field)?;
1579                        let mut v = Text::with(move || read());
1580                        v = apply_text_box(file, styles, &mut v, node, res, row, theme)?;
1581                        Ok(AnyView::new(v))
1582                    }
1583                    _ => Err(SeqError::new(
1584                        file,
1585                        arg.line,
1586                        1,
1587                        "dynamic: to nazwa propertu (dynamic: tytul)",
1588                    )),
1589                },
1590            };
1591        }
1592        if arg.name == "text"
1593            && let SeqValue::Dynamic(name) | SeqValue::Ident(name) = &arg.value
1594        {
1595            return match res {
1596                Resolver::Static => Err(SeqError::new(
1597                    file,
1598                    arg.line,
1599                    1,
1600                    "dynamic() wymaga hosta Rust — inkrement 2",
1601                )),
1602                Resolver::Host(b) => {
1603                    let read = display_of(file, arg.line, b, name)?;
1604                    let mut v = Text::with(move || read());
1605                    v = apply_text_box(file, styles, &mut v, node, res, row, theme)?;
1606                    Ok(AnyView::new(v))
1607                }
1608            };
1609        }
1610        if arg.name == "text"
1611            && let SeqValue::Env(name) = &arg.value
1612        {
1613            return match res {
1614                Resolver::Static => Err(SeqError::new(file, arg.line, 1, "env. wymaga hosta Rust")),
1615                Resolver::Host(b) => {
1616                    let read = display_env_of(file, arg.line, b, name)?;
1617                    let mut v = Text::with(move || read());
1618                    v = apply_text_box(file, styles, &mut v, node, res, row, theme)?;
1619                    Ok(AnyView::new(v))
1620                }
1621            };
1622        }
1623    }
1624    let mut v = Text::new(
1625        opt_str(file, node, "text", res, row)?
1626            .unwrap_or_else(|| node.label.clone().unwrap_or_default()),
1627    );
1628    if let Some(s) = text_style_of(file, styles, node)? {
1629        v = apply_seq_text_style(v, s);
1630    }
1631    for arg in &node.args {
1632        match arg.name.as_str() {
1633            "text" | "dynamic" | "t" => {}
1634            "style" => {}
1635            "size" => v = v.size(num(file, arg, res, row)?),
1636            "color" => v = v.color(color(file, theme, arg)?),
1637            "bold" => {
1638                if flag(file, arg, res, row)? {
1639                    v = v.bold();
1640                }
1641            }
1642            "semi" => {
1643                if flag(file, arg, res, row)? {
1644                    v = v.semibold();
1645                }
1646            }
1647            "medium" => {
1648                if flag(file, arg, res, row)? {
1649                    v = v.medium();
1650                }
1651            }
1652            "normal" => {
1653                if flag(file, arg, res, row)? {
1654                    v = v.normal();
1655                }
1656            }
1657            "light" => {
1658                if flag(file, arg, res, row)? {
1659                    v = v.light();
1660                }
1661            }
1662            "black" => {
1663                if flag(file, arg, res, row)? {
1664                    v = v.black();
1665                }
1666            }
1667            "truncate" => v = v.truncate(num(file, arg, res, row)?),
1668
1669            "wrap" => {
1670                let _ = flag(file, arg, res, row)?;
1671            }
1672
1673            "flex" => v = v.flex(num(file, arg, res, row)?),
1674            "width" => v = v.width(len(file, arg, res, row)?),
1675            "height" => v = v.height(len(file, arg, res, row)?),
1676            "min_width" => v = v.min_width(num(file, arg, res, row)?),
1677            "max_width" => v = v.max_width(num(file, arg, res, row)?),
1678            "min_height" => v = v.min_height(num(file, arg, res, row)?),
1679            "max_height" => v = v.max_height(num(file, arg, res, row)?),
1680            "margin" => v = apply_text_margin(v, file, arg, res, row)?,
1681            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
1682            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
1683            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
1684            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
1685            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
1686            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
1687            "hit" => v = v.hit_testable(flag(file, arg, res, row)?),
1688            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
1689            | "indent_bottom" => {}
1690            _ => unreachable_checked(file, node, arg)?,
1691        }
1692    }
1693    Ok(AnyView::new(v))
1694}
1695
1696fn apply_text_box(
1697    file: &str,
1698    styles: &BuiltStyles,
1699    v: &mut Text,
1700    node: &SeqNode,
1701    res: &Resolver<'_>,
1702    row: Option<&SeqRowData>,
1703    theme: &Theme,
1704) -> Result<Text, SeqError> {
1705    let mut out = std::mem::replace(v, Text::new(""));
1706    if let Some(s) = text_style_of(file, styles, node)? {
1707        out = apply_seq_text_style(out, s);
1708    }
1709    for arg in &node.args {
1710        match arg.name.as_str() {
1711            "text" | "dynamic" | "t" => {}
1712            "style" => {}
1713            "size" => out = out.size(num(file, arg, res, row)?),
1714            "color" => out = out.color(color(file, theme, arg)?),
1715            "bold" => {
1716                if flag(file, arg, res, row)? {
1717                    out = out.bold();
1718                }
1719            }
1720            "semi" => {
1721                if flag(file, arg, res, row)? {
1722                    out = out.semibold();
1723                }
1724            }
1725            "medium" => {
1726                if flag(file, arg, res, row)? {
1727                    out = out.medium();
1728                }
1729            }
1730            "normal" => {
1731                if flag(file, arg, res, row)? {
1732                    out = out.normal();
1733                }
1734            }
1735            "light" => {
1736                if flag(file, arg, res, row)? {
1737                    out = out.light();
1738                }
1739            }
1740            "black" => {
1741                if flag(file, arg, res, row)? {
1742                    out = out.black();
1743                }
1744            }
1745            "truncate" => out = out.truncate(num(file, arg, res, row)?),
1746
1747            "wrap" => {
1748                let _ = flag(file, arg, res, row)?;
1749            }
1750
1751            "flex" => out = out.flex(num(file, arg, res, row)?),
1752            "width" => out = out.width(len(file, arg, res, row)?),
1753            "height" => out = out.height(len(file, arg, res, row)?),
1754            "min_width" => out = out.min_width(num(file, arg, res, row)?),
1755            "max_width" => out = out.max_width(num(file, arg, res, row)?),
1756            "min_height" => out = out.min_height(num(file, arg, res, row)?),
1757            "max_height" => out = out.max_height(num(file, arg, res, row)?),
1758            "margin" => out = apply_text_margin(out, file, arg, res, row)?,
1759            "margin_x" => out = out.margin_x(num(file, arg, res, row)?),
1760            "margin_y" => out = out.margin_y(num(file, arg, res, row)?),
1761            "margin_left" => out = out.margin_left(num(file, arg, res, row)?),
1762            "margin_right" => out = out.margin_right(num(file, arg, res, row)?),
1763            "margin_top" => out = out.margin_top(num(file, arg, res, row)?),
1764            "margin_bottom" => out = out.margin_bottom(num(file, arg, res, row)?),
1765            "hit" => out = out.hit_testable(flag(file, arg, res, row)?),
1766            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
1767            | "indent_bottom" => {}
1768            _ => unreachable_checked(file, node, arg)?,
1769        }
1770    }
1771    Ok(out)
1772}
1773
1774fn apply_button_margin(
1775    v: Button,
1776    file: &str,
1777    arg: &crate::ir::SeqArg,
1778    res: &Resolver<'_>,
1779    row: Option<&SeqRowData>,
1780) -> Result<Button, SeqError> {
1781    match margin_val(file, arg, res, row)? {
1782        MarginVal::All(n) => Ok(v.margin(n)),
1783        MarginVal::Xy(x, y) => Ok(v.margin_xy(x, y)),
1784    }
1785}
1786
1787fn button_px(
1788    file: &str,
1789    arg: &crate::ir::SeqArg,
1790    res: &Resolver<'_>,
1791    row: Option<&SeqRowData>,
1792) -> Result<Option<f32>, SeqError> {
1793    match &arg.value {
1794        SeqValue::Length(LengthVal::Px(n)) => Ok(Some(*n)),
1795        SeqValue::Length(LengthVal::Fit) => Ok(None),
1796        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
1797            Resolver::Static => Err(SeqError::new(
1798                file,
1799                arg.line,
1800                1,
1801                format!("'{}' to px()/fit", arg.name),
1802            )),
1803            Resolver::Host(b) => Ok(Some(prop_num_env(file, arg.line, b, prop)?)),
1804        },
1805        SeqValue::Env(name) => match res {
1806            Resolver::Static => Err(SeqError::new(
1807                file,
1808                arg.line,
1809                1,
1810                format!("'{}' to px()/fit", arg.name),
1811            )),
1812            Resolver::Host(b) => Ok(Some(env_num(file, arg.line, b, name)?)),
1813        },
1814        SeqValue::ItemField(field) => match res {
1815            Resolver::Static => Err(SeqError::new(
1816                file,
1817                arg.line,
1818                1,
1819                format!("'{}' to px()/fit", arg.name),
1820            )),
1821            Resolver::Host(_) => Ok(Some(row_num(file, arg.line, row, field)?)),
1822        },
1823        _ => Err(SeqError::new(
1824            file,
1825            arg.line,
1826            1,
1827            format!("'{}' is px()/fit (stretch by flex: in parent)", arg.name),
1828        )),
1829    }
1830}
1831
1832fn apply_icon_margin(
1833    v: Icon,
1834    file: &str,
1835    arg: &crate::ir::SeqArg,
1836    res: &Resolver<'_>,
1837    row: Option<&SeqRowData>,
1838) -> Result<Icon, SeqError> {
1839    match margin_val(file, arg, res, row)? {
1840        MarginVal::All(n) => Ok(v.margin(n)),
1841        MarginVal::Xy(x, y) => Ok(v.margin_xy(x, y)),
1842    }
1843}
1844
1845fn apply_image_margin(
1846    v: Image,
1847    file: &str,
1848    arg: &crate::ir::SeqArg,
1849    res: &Resolver<'_>,
1850    row: Option<&SeqRowData>,
1851) -> Result<Image, SeqError> {
1852    match margin_val(file, arg, res, row)? {
1853        MarginVal::All(n) => Ok(v.margin(n)),
1854        MarginVal::Xy(x, y) => Ok(v.margin_xy(x, y)),
1855    }
1856}
1857
1858fn apply_text_margin(
1859    v: Text,
1860    file: &str,
1861    arg: &crate::ir::SeqArg,
1862    res: &Resolver<'_>,
1863    row: Option<&SeqRowData>,
1864) -> Result<Text, SeqError> {
1865    match margin_val(file, arg, res, row)? {
1866        MarginVal::All(n) => Ok(v.margin(n)),
1867        MarginVal::Xy(x, y) => Ok(v.margin_xy(x, y)),
1868    }
1869}
1870
1871fn lower_clip(
1872    file: &str,
1873    theme: &Theme,
1874    node: &SeqNode,
1875    res: &Resolver<'_>,
1876    row: Option<&SeqRowData>,
1877) -> Result<DrawLayer, SeqError> {
1878    let r = rect_src_of(file, res, row, node)?;
1879    let radius = opt_num(file, res, row, node, "radius")?;
1880    let has_radius = find_arg(node, "radius").is_some();
1881    let visible = match find_arg(node, "visible") {
1882        None => BoolSrc::Lit(true),
1883        Some(arg) => dyn_bool(file, arg.line, res, row, &arg.value, "visible")?,
1884    };
1885    let mut layers = Vec::new();
1886    let touch_list = vec![r.touch_rc(), radius.touch_rc(), visible.touch_rc()];
1887    for c in &node.children {
1888        layers.push(lower_layer(file, theme, c, res, row)?);
1889    }
1890    Ok(DrawLayer::Clip {
1891        rect: r,
1892        radius,
1893        has_radius,
1894        visible,
1895        touch_extra: touches(&touch_list),
1896        layers,
1897    })
1898}
1899
1900fn lower_button(
1901    file: &str,
1902    styles: &BuiltStyles,
1903    node: &SeqNode,
1904    res: &Resolver<'_>,
1905    row: Option<&SeqRowData>,
1906) -> Result<AnyView, SeqError> {
1907    let mut v = Button::new(node.label.clone().unwrap_or_default());
1908    if let Some(s) = button_style_of(file, styles, node)? {
1909        v = v.style(s);
1910    }
1911    for arg in &node.args {
1912        match arg.name.as_str() {
1913            "style" => {}
1914            "enabled" => v = v.disabled(!flag(file, arg, res, row)?),
1915            "font_size" => v = v.font_size(num(file, arg, res, row)?),
1916            "width" => {
1917                if let Some(px) = button_px(file, arg, res, row)? {
1918                    v = v.width(px);
1919                }
1920            }
1921            "height" => {
1922                if let Some(px) = button_px(file, arg, res, row)? {
1923                    v = v.height(px);
1924                }
1925            }
1926            "min_width" => v = v.min_width(num(file, arg, res, row)?),
1927            "max_width" => v = v.max_width(num(file, arg, res, row)?),
1928            "min_height" => v = v.min_height(num(file, arg, res, row)?),
1929            "max_height" => v = v.max_height(num(file, arg, res, row)?),
1930            "margin" => v = apply_button_margin(v, file, arg, res, row)?,
1931            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
1932            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
1933            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
1934            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
1935            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
1936            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
1937            "flex" => {}
1938            "on_click" => match res {
1939                Resolver::Static => {
1940                    return Err(SeqError::new(
1941                        file,
1942                        arg.line,
1943                        1,
1944                        "on_click wymaga hosta Rust — inkrement 2",
1945                    ));
1946                }
1947                Resolver::Host(b) => match &arg.value {
1948                    SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
1949                        let fire = wire_action(file, arg.line, b, &arg.value, row)?;
1950                        v = v.on_click(move || fire());
1951                    }
1952                    _ => {
1953                        return Err(SeqError::new(
1954                            file,
1955                            arg.line,
1956                            1,
1957                            "on_click: { akcja(args) }",
1958                        ));
1959                    }
1960                },
1961            },
1962            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
1963            | "indent_bottom" => {}
1964            _ => unreachable_checked(file, node, arg)?,
1965        }
1966    }
1967    Ok(AnyView::new(v))
1968}
1969
1970fn without_placement(node: &SeqNode) -> SeqNode {
1971    SeqNode {
1972        kind: node.kind.clone(),
1973        line: node.line,
1974        label: node.label.clone(),
1975        args: node
1976            .args
1977            .iter()
1978            .filter(|a| {
1979                !matches!(
1980                    a.name.as_str(),
1981                    "at" | "offset" | "z" | "col" | "row" | "col_span" | "row_span"
1982                )
1983            })
1984            .cloned()
1985            .collect(),
1986        children: node.children.clone(),
1987    }
1988}
1989
1990fn stack_pos_of(file: &str, node: &SeqNode) -> Result<StackPos, SeqError> {
1991    match find_arg(node, "at") {
1992        None => Ok(StackPos::default()),
1993        Some(arg) => match &arg.value {
1994            SeqValue::Ident(name) => match name.as_str() {
1995                "top" => Ok(StackPos::top()),
1996                "bottom" => Ok(StackPos::bottom()),
1997                "left" => Ok(StackPos::left()),
1998                "right" => Ok(StackPos::right()),
1999                "top_left" => Ok(StackPos::top_left()),
2000                "top_right" => Ok(StackPos::top_right()),
2001                "bottom_left" => Ok(StackPos::bottom_left()),
2002                "bottom_right" => Ok(StackPos::bottom_right()),
2003                "fill" => Ok(StackPos::fill()),
2004                "center" => Ok(StackPos::default()),
2005                other => Err(SeqError::new(
2006                    file,
2007                    arg.line,
2008                    1,
2009                    format!(
2010                        "at to top/bottom/left/right/top_left/top_right/bottom_left/bottom_right/fill/center, nie '{other}'"
2011                    ),
2012                )),
2013            },
2014            _ => Err(SeqError::new(
2015                file,
2016                arg.line,
2017                1,
2018                "at: to nazwa (at: top_left)",
2019            )),
2020        },
2021    }
2022}
2023
2024fn stack_offset_of(
2025    file: &str,
2026    res: &Resolver<'_>,
2027    row: Option<&SeqRowData>,
2028    node: &SeqNode,
2029) -> Result<(f32, f32), SeqError> {
2030    match find_arg(node, "offset") {
2031        None => Ok((0.0, 0.0)),
2032        Some(arg) => match &arg.value {
2033            SeqValue::Tuple(items) if items.len() == 2 => {
2034                let x = dyn_f32(file, arg.line, res, row, &items[0], "offset.dx")?;
2035                let y = dyn_f32(file, arg.line, res, row, &items[1], "offset.dy")?;
2036                Ok((x.val(), y.val()))
2037            }
2038            _ => Err(SeqError::new(
2039                file,
2040                arg.line,
2041                1,
2042                "offset: to krotka (dx, dy)",
2043            )),
2044        },
2045    }
2046}
2047
2048fn stack_z_of(
2049    file: &str,
2050    res: &Resolver<'_>,
2051    _row: Option<&SeqRowData>,
2052    node: &SeqNode,
2053) -> Result<i32, SeqError> {
2054    match find_arg(node, "z") {
2055        None => Ok(0),
2056        Some(arg) => match &arg.value {
2057            SeqValue::Num(n) => {
2058                if n.is_finite() {
2059                    #[allow(clippy::cast_possible_truncation)]
2060                    Ok(*n as i32)
2061                } else {
2062                    Err(SeqError::new(file, arg.line, 1, "'z' must be finished"))
2063                }
2064            }
2065            SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
2066                Resolver::Static => Err(SeqError::new(file, arg.line, 1, "'z' to liczba")),
2067                Resolver::Host(b) => {
2068                    let v = prop_num_env(file, arg.line, b, prop)?;
2069                    #[allow(clippy::cast_possible_truncation)]
2070                    Ok(v as i32)
2071                }
2072            },
2073            _ => Err(SeqError::new(file, arg.line, 1, "'z' is an integer")),
2074        },
2075    }
2076}
2077
2078fn lower_stack(
2079    file: &str,
2080    theme: &Theme,
2081    styles: &BuiltStyles,
2082    node: &SeqNode,
2083    res: &Resolver<'_>,
2084    row: Option<&SeqRowData>,
2085) -> Result<AnyView, SeqError> {
2086    let mut v = Stack::new();
2087    for arg in &node.args {
2088        match arg.name.as_str() {
2089            "width" => v = v.width(len(file, arg, res, row)?),
2090            "height" => v = v.height(len(file, arg, res, row)?),
2091            "padding" => match padding_val(file, arg, res, row)? {
2092                PaddingVal::All(n) => v = v.padding(n),
2093                PaddingVal::Xy(x, y) => v = v.padding_xy(x, y),
2094            },
2095            "padding_x" => v = v.padding_x(num(file, arg, res, row)?),
2096            "padding_y" => v = v.padding_y(num(file, arg, res, row)?),
2097            "background" => v = v.background(color(file, theme, arg)?),
2098            "flex" => v = v.flex(num(file, arg, res, row)?),
2099            "radius" => v = v.radius(num(file, arg, res, row)?),
2100            "border" => {
2101                let (w, c) = border_pair(file, theme, arg)?;
2102                v = v.border(w, c);
2103            }
2104            "min_width" => v = v.min_width(num(file, arg, res, row)?),
2105            "max_width" => v = v.max_width(num(file, arg, res, row)?),
2106            "min_height" => v = v.min_height(num(file, arg, res, row)?),
2107            "max_height" => v = v.max_height(num(file, arg, res, row)?),
2108            "margin" => match margin_val(file, arg, res, row)? {
2109                MarginVal::All(n) => v = v.margin(n),
2110                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
2111            },
2112            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
2113            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
2114            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
2115            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
2116            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
2117            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
2118            "overflow" => v = v.overflow(overflow_val(file, arg)?),
2119            "hit" => v = v.hit_testable(flag(file, arg, res, row)?),
2120            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
2121            | "indent_bottom" => {}
2122            _ => unreachable_checked(file, node, arg)?,
2123        }
2124    }
2125    for c in &node.children {
2126        let pos = stack_pos_of(file, c)?;
2127        let (dx, dy) = stack_offset_of(file, res, row, c)?;
2128        let z = stack_z_of(file, res, row, c)?;
2129        let pos = pos.offset(dx, dy).z(z);
2130        let clean = without_placement(c);
2131        let kid = lower_node(file, theme, styles, &clean, res, row)?;
2132
2133        let _ = child_flex_weight(file, res, row, c)?;
2134        v = v.child_at(kid, pos);
2135    }
2136    Ok(AnyView::new(v))
2137}
2138
2139fn track_of(
2140    file: &str,
2141    line: usize,
2142    res: &Resolver<'_>,
2143    row: Option<&SeqRowData>,
2144    value: &SeqValue,
2145) -> Result<Track, SeqError> {
2146    match value {
2147        SeqValue::Length(l) => match *l {
2148            LengthVal::Fit => Ok(Track::Fit),
2149            LengthVal::Px(n) => Ok(Track::Px(n)),
2150            LengthVal::Fill(w) => Ok(Track::Fill(w)),
2151            LengthVal::Percent(p) => Ok(Track::Percent(p)),
2152        },
2153        SeqValue::Num(n) => Ok(Track::Px(*n)),
2154        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
2155            Resolver::Static => Err(SeqError::new(file, line, 1, "tor to px()/fill()/fit")),
2156            Resolver::Host(b) => Ok(Track::Px(prop_num_env(file, line, b, prop)?)),
2157        },
2158        SeqValue::Env(name) => match res {
2159            Resolver::Static => Err(SeqError::new(file, line, 1, "tor to px()/fill()/fit")),
2160            Resolver::Host(b) => Ok(Track::Px(env_num(file, line, b, name)?)),
2161        },
2162        SeqValue::ItemField(field) => match res {
2163            Resolver::Static => Err(SeqError::new(file, line, 1, "tor to px()/fill()/fit")),
2164            Resolver::Host(_) => Ok(Track::Px(row_num(file, line, row, field)?)),
2165        },
2166        _ => Err(SeqError::new(
2167            file,
2168            line,
2169            1,
2170            "tor to px(n)/fill(w)/fit/percent(n)",
2171        )),
2172    }
2173}
2174
2175fn tracks_of(
2176    file: &str,
2177    res: &Resolver<'_>,
2178    row: Option<&SeqRowData>,
2179    node: &SeqNode,
2180    name: &str,
2181) -> Result<Vec<Track>, SeqError> {
2182    match find_arg(node, name) {
2183        None => Ok(vec![Track::Fill(1.0)]),
2184        Some(arg) => match &arg.value {
2185            SeqValue::Array(items) if !items.is_empty() => items
2186                .iter()
2187                .map(|it| track_of(file, arg.line, res, row, it))
2188                .collect(),
2189            _ => Err(SeqError::new(
2190                file,
2191                arg.line,
2192                1,
2193                format!("'{name}' to [px(100), fill(1), ...] (min. 1 tor)"),
2194            )),
2195        },
2196    }
2197}
2198
2199fn grid_int(
2200    file: &str,
2201    res: &Resolver<'_>,
2202    _row: Option<&SeqRowData>,
2203    node: &SeqNode,
2204    name: &str,
2205) -> Result<Option<usize>, SeqError> {
2206    match find_arg(node, name) {
2207        None => Ok(None),
2208        Some(arg) => match &arg.value {
2209            SeqValue::Num(n) => {
2210                if n.is_finite() && *n >= 0.0 {
2211                    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2212                    Ok(Some(*n as usize))
2213                } else {
2214                    Err(SeqError::new(
2215                        file,
2216                        arg.line,
2217                        1,
2218                        format!("'{name}' to liczba >= 0"),
2219                    ))
2220                }
2221            }
2222            SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
2223                Resolver::Static => Err(SeqError::new(
2224                    file,
2225                    arg.line,
2226                    1,
2227                    format!("'{name}' to liczba"),
2228                )),
2229                Resolver::Host(b) => {
2230                    let v = prop_num_env(file, arg.line, b, prop)?;
2231                    if v.is_finite() && v >= 0.0 {
2232                        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
2233                        Ok(Some(v as usize))
2234                    } else {
2235                        Err(SeqError::new(
2236                            file,
2237                            arg.line,
2238                            1,
2239                            format!("'{name}' to liczba >= 0"),
2240                        ))
2241                    }
2242                }
2243            },
2244            _ => Err(SeqError::new(
2245                file,
2246                arg.line,
2247                1,
2248                format!("'{name}' to liczba"),
2249            )),
2250        },
2251    }
2252}
2253
2254fn lower_grid(
2255    file: &str,
2256    theme: &Theme,
2257    styles: &BuiltStyles,
2258    node: &SeqNode,
2259    res: &Resolver<'_>,
2260    row: Option<&SeqRowData>,
2261) -> Result<AnyView, SeqError> {
2262    let columns = tracks_of(file, res, row, node, "columns")?;
2263    let mut v = Grid::new(columns);
2264    for arg in &node.args {
2265        match arg.name.as_str() {
2266            "columns" => {}
2267            "rows" => {
2268                v = match &arg.value {
2269                    SeqValue::Array(items) if !items.is_empty() => {
2270                        let tracks: Vec<Track> = items
2271                            .iter()
2272                            .map(|it| track_of(file, arg.line, res, row, it))
2273                            .collect::<Result<_, _>>()?;
2274                        v.rows(tracks)
2275                    }
2276                    _ => {
2277                        return Err(SeqError::new(
2278                            file,
2279                            arg.line,
2280                            1,
2281                            "'rows' to [px(40), fill(1), ...]",
2282                        ));
2283                    }
2284                };
2285            }
2286            "width" => v = v.width(len(file, arg, res, row)?),
2287            "height" => v = v.height(len(file, arg, res, row)?),
2288            "padding" => match padding_val(file, arg, res, row)? {
2289                PaddingVal::All(n) => v = v.padding(n),
2290                PaddingVal::Xy(x, y) => v = v.padding_xy(x, y),
2291            },
2292            "padding_x" => v = v.padding_x(num(file, arg, res, row)?),
2293            "padding_y" => v = v.padding_y(num(file, arg, res, row)?),
2294            "background" => v = v.background(color(file, theme, arg)?),
2295            "flex" => v = v.flex(num(file, arg, res, row)?),
2296            "radius" => v = v.radius(num(file, arg, res, row)?),
2297            "border" => {
2298                let (w, c) = border_pair(file, theme, arg)?;
2299                v = v.border(w, c);
2300            }
2301            "gap" => v = v.gap(num(file, arg, res, row)?),
2302            "column_gap" => v = v.column_gap(num(file, arg, res, row)?),
2303            "row_gap" => v = v.row_gap(num(file, arg, res, row)?),
2304            "auto_flow" => {
2305                v = match ident(file, arg)?.as_str() {
2306                    "row" => v.auto_flow(GridFlow::Row),
2307                    "column" => v.column_flow(),
2308                    other => {
2309                        return Err(SeqError::new(
2310                            file,
2311                            arg.line,
2312                            1,
2313                            format!("auto_flow to row/column, nie '{other}'"),
2314                        ));
2315                    }
2316                };
2317            }
2318            "align" => v = v.align_items(align_val(file, arg)?),
2319            "justify" => v = v.justify_items(align_val(file, arg)?),
2320            "min_width" => v = v.min_width(num(file, arg, res, row)?),
2321            "max_width" => v = v.max_width(num(file, arg, res, row)?),
2322            "min_height" => v = v.min_height(num(file, arg, res, row)?),
2323            "max_height" => v = v.max_height(num(file, arg, res, row)?),
2324            "margin" => match margin_val(file, arg, res, row)? {
2325                MarginVal::All(n) => v = v.margin(n),
2326                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
2327            },
2328            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
2329            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
2330            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
2331            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
2332            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
2333            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
2334            "overflow" => v = v.overflow(overflow_val(file, arg)?),
2335            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
2336            | "indent_bottom" => {}
2337            _ => unreachable_checked(file, node, arg)?,
2338        }
2339    }
2340    for c in &node.children {
2341        let clean = without_placement(c);
2342        let kid = lower_node(file, theme, styles, &clean, res, row)?;
2343        let _ = child_flex_weight(file, res, row, c)?;
2344        let col = grid_int(file, res, row, c, "col")?;
2345        let row_i = grid_int(file, res, row, c, "row")?;
2346        let col_span = grid_int(file, res, row, c, "col_span")?.unwrap_or(1).max(1);
2347        let row_span = grid_int(file, res, row, c, "row_span")?.unwrap_or(1).max(1);
2348        match (col, row_i) {
2349            (Some(cc), Some(rr)) => {
2350                v = v.child_at(
2351                    kid,
2352                    GridPlacement::new().at(cc, rr).span(col_span, row_span),
2353                );
2354            }
2355            (Some(cc), None) => {
2356                v = v.child_at(
2357                    kid,
2358                    GridPlacement::new().column(cc).span(col_span, row_span),
2359                );
2360            }
2361            (None, Some(rr)) => {
2362                v = v.child_at(kid, GridPlacement::new().row(rr).span(col_span, row_span));
2363            }
2364            (None, None) if col_span > 1 || row_span > 1 => {
2365                v = v.child_at(kid, GridPlacement::new().span(col_span, row_span));
2366            }
2367            (None, None) => {
2368                v = v.child(kid);
2369            }
2370        }
2371    }
2372    Ok(AnyView::new(v))
2373}
2374
2375fn lower_wrap(
2376    file: &str,
2377    theme: &Theme,
2378    styles: &BuiltStyles,
2379    node: &SeqNode,
2380    res: &Resolver<'_>,
2381    row: Option<&SeqRowData>,
2382) -> Result<AnyView, SeqError> {
2383    let mut v = Wrap::new();
2384    for arg in &node.args {
2385        match arg.name.as_str() {
2386            "width" => v = v.width(len(file, arg, res, row)?),
2387            "height" => v = v.height(len(file, arg, res, row)?),
2388            "padding" => match padding_val(file, arg, res, row)? {
2389                PaddingVal::All(n) => v = v.padding(n),
2390                PaddingVal::Xy(x, y) => v = v.padding_xy(x, y),
2391            },
2392            "padding_x" => v = v.padding_x(num(file, arg, res, row)?),
2393            "padding_y" => v = v.padding_y(num(file, arg, res, row)?),
2394            "background" => v = v.background(color(file, theme, arg)?),
2395            "flex" => v = v.flex(num(file, arg, res, row)?),
2396            "axis" => {
2397                v = match ident(file, arg)?.as_str() {
2398                    "horizontal" => v.axis(Axis::Horizontal),
2399                    "vertical" => v.axis(Axis::Vertical),
2400                    other => {
2401                        return Err(SeqError::new(
2402                            file,
2403                            arg.line,
2404                            1,
2405                            format!("axis to horizontal/vertical, nie '{other}'"),
2406                        ));
2407                    }
2408                };
2409            }
2410            "gap" => v = v.gap(num(file, arg, res, row)?),
2411            "column_gap" => v = v.column_gap(num(file, arg, res, row)?),
2412            "row_gap" => v = v.row_gap(num(file, arg, res, row)?),
2413            "align" => v = v.align_items(align_val(file, arg)?),
2414            "min_width" => v = v.min_width(num(file, arg, res, row)?),
2415            "max_width" => v = v.max_width(num(file, arg, res, row)?),
2416            "min_height" => v = v.min_height(num(file, arg, res, row)?),
2417            "max_height" => v = v.max_height(num(file, arg, res, row)?),
2418            "margin" => match margin_val(file, arg, res, row)? {
2419                MarginVal::All(n) => v = v.margin(n),
2420                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
2421            },
2422            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
2423            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
2424            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
2425            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
2426            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
2427            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
2428            "overflow" => v = v.overflow(overflow_val(file, arg)?),
2429            "hit" => v = v.hit_testable(flag(file, arg, res, row)?),
2430            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
2431            | "indent_bottom" => {}
2432            _ => unreachable_checked(file, node, arg)?,
2433        }
2434    }
2435    for c in &node.children {
2436        let clean = without_placement(c);
2437        let kid = lower_node(file, theme, styles, &clean, res, row)?;
2438        let _ = child_flex_weight(file, res, row, c)?;
2439        v = v.child(kid);
2440    }
2441    Ok(AnyView::new(v))
2442}
2443
2444fn when_signal(
2445    file: &str,
2446    line: usize,
2447    res: &Resolver<'_>,
2448    row: Option<&SeqRowData>,
2449    value: &SeqValue,
2450) -> Result<Signal<bool>, SeqError> {
2451    match value {
2452        SeqValue::Bool(b) => Ok(Signal::new(*b)),
2453        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
2454            Resolver::Static => Err(SeqError::new(
2455                file,
2456                line,
2457                1,
2458                "'when' z propertu wymaga hosta",
2459            )),
2460            Resolver::Host(b) => {
2461                if let Some(sig) = b.get_bool(prop) {
2462                    Ok(sig)
2463                } else if let Some(m) = b.get_memo_bool(prop) {
2464                    let sig = Signal::new(m.get_untracked());
2465                    let s2 = sig.clone();
2466                    effect(move || {
2467                        let v = m.get();
2468                        s2.set(v);
2469                    })
2470                    .forget();
2471                    Ok(sig)
2472                } else {
2473                    Err(prop_err(file, line, prop, "Bool"))
2474                }
2475            }
2476        },
2477        SeqValue::Env(name) => match res {
2478            Resolver::Static => Err(SeqError::new(
2479                file,
2480                line,
2481                1,
2482                "'when' from environment requires host",
2483            )),
2484            Resolver::Host(b) => b.get_env_bool(name).ok_or_else(|| {
2485                SeqError::new(
2486                    file,
2487                    line,
2488                    1,
2489                    format!("unknown env.{name} or wrong type (here you need: Bool)"),
2490                )
2491            }),
2492        },
2493        SeqValue::ItemField(field) => match res {
2494            Resolver::Static => Err(SeqError::new(
2495                file,
2496                line,
2497                1,
2498                "'when' z wiersza wymaga hosta",
2499            )),
2500            Resolver::Host(_) => match row_value(file, line, row, field)? {
2501                RowVal::Sig(FieldSignal::Bool(sig)) => Ok(sig),
2502                _ => Err(SeqError::new(
2503                    file,
2504                    line,
2505                    1,
2506                    format!("pole wiersza '{field}' to nie Bool"),
2507                )),
2508            },
2509        },
2510        _ => Err(SeqError::new(
2511            file,
2512            line,
2513            1,
2514            "'when' to true/false albo dynamic(nazwa)",
2515        )),
2516    }
2517}
2518
2519fn lower_if(
2520    file: &str,
2521    theme: &Theme,
2522    styles: &BuiltStyles,
2523    node: &SeqNode,
2524    res: &Resolver<'_>,
2525    row: Option<&SeqRowData>,
2526) -> Result<AnyView, SeqError> {
2527    let when_arg = find_arg(node, "when").ok_or_else(|| {
2528        SeqError::new(
2529            file,
2530            node.line,
2531            1,
2532            "If wymaga when: true/false/dynamic()".to_string(),
2533        )
2534    })?;
2535
2536    if let SeqValue::Bool(b) = &when_arg.value {
2537        if !b {
2538            return Ok(AnyView::new(Column::new()));
2539        }
2540        return match node.children.as_slice() {
2541            [] => Ok(AnyView::new(Column::new())),
2542            [single] => lower_node(file, theme, styles, single, res, row),
2543            _ => {
2544                let mut col = Column::new();
2545                for c in &node.children {
2546                    let kid = lower_node(file, theme, styles, c, res, row)?;
2547                    col = match child_flex_weight(file, res, row, c)? {
2548                        Some(w) => col.child_flex(kid, w),
2549                        None => col.child(kid),
2550                    };
2551                }
2552                Ok(AnyView::new(col))
2553            }
2554        };
2555    }
2556    let bindings = match res {
2557        Resolver::Static => {
2558            return Err(SeqError::new(
2559                file,
2560                when_arg.line,
2561                1,
2562                "If(when: prop) wymaga hosta Rust (HostBindings::prop_bool)",
2563            ));
2564        }
2565        Resolver::Host(b) => (*b).clone(),
2566    };
2567    let cond = when_signal(file, when_arg.line, res, row, &when_arg.value)?;
2568    let template = node.children.clone();
2569    let file_owned = file.to_string();
2570    let theme_owned = *theme;
2571    let styles_owned = styles.clone();
2572    let branch = move || -> AnyView {
2573        let res = Resolver::Host(&bindings);
2574        match template.as_slice() {
2575            [] => AnyView::new(Column::new()),
2576            [single] => lower_node(&file_owned, &theme_owned, &styles_owned, single, &res, row)
2577                .unwrap_or_else(|e| AnyView::new(Text::new(format!("seq: {e}")))),
2578            _ => {
2579                let mut col = Column::new();
2580                for c in &template {
2581                    match lower_node(&file_owned, &theme_owned, &styles_owned, c, &res, row) {
2582                        Ok(kid) => {
2583                            let w = child_flex_weight(&file_owned, &res, row, c).unwrap_or(None);
2584                            col = match w {
2585                                Some(w) => col.child_flex(kid, w),
2586                                None => col.child(kid),
2587                            };
2588                        }
2589                        Err(e) => {
2590                            col = col.child(Text::new(format!("seq: {e}")));
2591                        }
2592                    }
2593                }
2594                AnyView::new(col)
2595            }
2596        }
2597    };
2598
2599    let _ = cond.get();
2600    let mut view = when(cond, branch());
2601    if let Some(arg) = find_arg(node, "enter") {
2602        let spec = anim_spec_of(file, arg)?;
2603        view = view.enter(Motion::from(spec));
2604    }
2605    if let Some(arg) = find_arg(node, "exit") {
2606        let spec = anim_spec_of(file, arg)?;
2607        view = view.exit(Motion::from(spec));
2608    }
2609    Ok(AnyView::new(view))
2610}
2611
2612fn lower_animation_child(
2613    file: &str,
2614    theme: &Theme,
2615    styles: &BuiltStyles,
2616    node: &SeqNode,
2617    res: &Resolver<'_>,
2618    row: Option<&SeqRowData>,
2619) -> Result<AnyView, SeqError> {
2620    match node.children.as_slice() {
2621        [] => Ok(AnyView::new(Column::new())),
2622        [single] => lower_node(file, theme, styles, single, res, row),
2623        many => {
2624            let mut col = Column::new();
2625            for c in many {
2626                let kid = lower_node(file, theme, styles, c, res, row)?;
2627                col = col.child(kid);
2628            }
2629            Ok(AnyView::new(col))
2630        }
2631    }
2632}
2633
2634fn anim_spec_of(file: &str, arg: &SeqArg) -> Result<AnimSpec, SeqError> {
2635    let name = match &arg.value {
2636        SeqValue::Ident(s) | SeqValue::Dynamic(s) => s.as_str(),
2637        _ => {
2638            return Err(SeqError::new(
2639                file,
2640                arg.line,
2641                1,
2642                "animate/enter/exit: nazwa presetu (fast/smooth/snappy/gentle/slow)".to_string(),
2643            ));
2644        }
2645    };
2646    Ok(match name {
2647        "fast" => AnimSpec::FAST,
2648        "smooth" => AnimSpec::SMOOTH,
2649        "snappy" => AnimSpec::SNAPPY,
2650        "gentle" => AnimSpec::GENTLE,
2651        "slow" => AnimSpec::SLOW,
2652        _ => {
2653            return Err(SeqError::new(
2654                file,
2655                arg.line,
2656                1,
2657                format!("nieznany preset animacji '{name}'"),
2658            ));
2659        }
2660    })
2661}
2662
2663fn lower_opacity(
2664    file: &str,
2665    theme: &Theme,
2666    styles: &BuiltStyles,
2667    node: &SeqNode,
2668    res: &Resolver<'_>,
2669    row: Option<&SeqRowData>,
2670) -> Result<AnyView, SeqError> {
2671    let child = lower_animation_child(file, theme, styles, node, res, row)?;
2672    let sig = if let Some(arg) = find_arg(node, "value") {
2673        bridge_f32(file, arg.line, res, row, &arg.value, "value")?
2674    } else {
2675        Signal::new(1.0_f32)
2676    };
2677    let mut v = LiveOpacity::new(child, Live::from(sig));
2678    if let Some(arg) = find_arg(node, "animate") {
2679        v = v.animate(anim_spec_of(file, arg)?);
2680    }
2681    Ok(AnyView::new(v))
2682}
2683
2684fn lower_scale(
2685    file: &str,
2686    theme: &Theme,
2687    styles: &BuiltStyles,
2688    node: &SeqNode,
2689    res: &Resolver<'_>,
2690    row: Option<&SeqRowData>,
2691) -> Result<AnyView, SeqError> {
2692    let child = lower_animation_child(file, theme, styles, node, res, row)?;
2693    let sig = if let Some(arg) = find_arg(node, "value") {
2694        bridge_f32(file, arg.line, res, row, &arg.value, "value")?
2695    } else {
2696        Signal::new(1.0_f32)
2697    };
2698    let mut v = Scaled::new(child, Live::from(sig));
2699    if let Some(arg) = find_arg(node, "animate") {
2700        v = v.animate(anim_spec_of(file, arg)?);
2701    }
2702    Ok(AnyView::new(v))
2703}
2704
2705fn lower_rotate(
2706    file: &str,
2707    theme: &Theme,
2708    styles: &BuiltStyles,
2709    node: &SeqNode,
2710    res: &Resolver<'_>,
2711    row: Option<&SeqRowData>,
2712) -> Result<AnyView, SeqError> {
2713    let child = lower_animation_child(file, theme, styles, node, res, row)?;
2714    let sig = if let Some(arg) = find_arg(node, "value") {
2715        bridge_f32(file, arg.line, res, row, &arg.value, "value")?
2716    } else {
2717        Signal::new(0.0_f32)
2718    };
2719    let mut v = Rotated::new(child, Live::from(sig));
2720    if let Some(arg) = find_arg(node, "animate") {
2721        v = v.animate(anim_spec_of(file, arg)?);
2722    }
2723    Ok(AnyView::new(v))
2724}
2725
2726fn lower_width(
2727    file: &str,
2728    theme: &Theme,
2729    styles: &BuiltStyles,
2730    node: &SeqNode,
2731    res: &Resolver<'_>,
2732    row: Option<&SeqRowData>,
2733) -> Result<AnyView, SeqError> {
2734    let child = lower_animation_child(file, theme, styles, node, res, row)?;
2735    let sig = if let Some(arg) = find_arg(node, "value") {
2736        bridge_f32(file, arg.line, res, row, &arg.value, "value")?
2737    } else {
2738        Signal::new(0.0_f32)
2739    };
2740    let mut v = AnimatedSize::width(child, Live::from(sig));
2741    if let Some(arg) = find_arg(node, "animate") {
2742        v = v.animate(anim_spec_of(file, arg)?);
2743    }
2744    Ok(AnyView::new(v))
2745}
2746
2747fn lower_height(
2748    file: &str,
2749    theme: &Theme,
2750    styles: &BuiltStyles,
2751    node: &SeqNode,
2752    res: &Resolver<'_>,
2753    row: Option<&SeqRowData>,
2754) -> Result<AnyView, SeqError> {
2755    let child = lower_animation_child(file, theme, styles, node, res, row)?;
2756    let sig = if let Some(arg) = find_arg(node, "value") {
2757        bridge_f32(file, arg.line, res, row, &arg.value, "value")?
2758    } else {
2759        Signal::new(0.0_f32)
2760    };
2761    let mut v = AnimatedSize::height(child, Live::from(sig));
2762    if let Some(arg) = find_arg(node, "animate") {
2763        v = v.animate(anim_spec_of(file, arg)?);
2764    }
2765    Ok(AnyView::new(v))
2766}
2767
2768fn lower_translate(
2769    file: &str,
2770    theme: &Theme,
2771    styles: &BuiltStyles,
2772    node: &SeqNode,
2773    res: &Resolver<'_>,
2774    row: Option<&SeqRowData>,
2775) -> Result<AnyView, SeqError> {
2776    let child = lower_animation_child(file, theme, styles, node, res, row)?;
2777    let x_sig = if let Some(arg) = find_arg(node, "x") {
2778        bridge_f32(file, arg.line, res, row, &arg.value, "x")?
2779    } else {
2780        Signal::new(0.0_f32)
2781    };
2782    let y_sig = if let Some(arg) = find_arg(node, "y") {
2783        bridge_f32(file, arg.line, res, row, &arg.value, "y")?
2784    } else {
2785        Signal::new(0.0_f32)
2786    };
2787    let x_live = x_sig.clone();
2788    let y_live = y_sig.clone();
2789    let live = Live::<Vec2>::derived(move || Vec2::new(x_live.get(), y_live.get()));
2790    let mut v = LiveOffset::new(child, live);
2791    if let Some(arg) = find_arg(node, "animate") {
2792        v = v.animate(anim_spec_of(file, arg)?);
2793    }
2794    Ok(AnyView::new(v))
2795}
2796
2797fn lower_pulse(
2798    file: &str,
2799    theme: &Theme,
2800    styles: &BuiltStyles,
2801    node: &SeqNode,
2802    res: &Resolver<'_>,
2803    row: Option<&SeqRowData>,
2804) -> Result<AnyView, SeqError> {
2805    let child = lower_animation_child(file, theme, styles, node, res, row)?;
2806    let mut v = Pulse::new(child);
2807    if let Some(arg) = find_arg(node, "min") {
2808        v = v.min_opacity(num(file, arg, res, row)?);
2809    }
2810    if let Some(arg) = find_arg(node, "period") {
2811        v = v.period_duration(std::time::Duration::from_secs_f32(num(
2812            file, arg, res, row,
2813        )?));
2814    }
2815    Ok(AnyView::new(v))
2816}
2817
2818fn lower_shake(
2819    file: &str,
2820    theme: &Theme,
2821    styles: &BuiltStyles,
2822    node: &SeqNode,
2823    res: &Resolver<'_>,
2824    row: Option<&SeqRowData>,
2825) -> Result<AnyView, SeqError> {
2826    let child = lower_animation_child(file, theme, styles, node, res, row)?;
2827    let mut v = Shake::new(child);
2828    if let Some(arg) = find_arg(node, "amplitude") {
2829        v = v.amplitude(num(file, arg, res, row)?);
2830    }
2831    if let Some(arg) = find_arg(node, "period") {
2832        v = v.period_duration(std::time::Duration::from_secs_f32(num(
2833            file, arg, res, row,
2834        )?));
2835    }
2836    if let Some(arg) = find_arg(node, "duration") {
2837        v = v.total_duration(std::time::Duration::from_secs_f32(num(
2838            file, arg, res, row,
2839        )?));
2840    }
2841    if let Some(arg) = find_arg(node, "delay") {
2842        v = v.delay_duration(std::time::Duration::from_secs_f32(num(
2843            file, arg, res, row,
2844        )?));
2845    }
2846    Ok(AnyView::new(v))
2847}
2848
2849fn lower_async(
2850    file: &str,
2851    theme: &Theme,
2852    styles: &BuiltStyles,
2853    node: &SeqNode,
2854    res: &Resolver<'_>,
2855    row: Option<&SeqRowData>,
2856) -> Result<AnyView, SeqError> {
2857    let source = find_arg(node, "source").ok_or_else(|| {
2858        SeqError::new(file, node.line, 1, "Async wymaga source: nazwa".to_string())
2859    })?;
2860    let source_name = match &source.value {
2861        SeqValue::Ident(name) => name.clone(),
2862        _ => {
2863            return Err(SeqError::new(
2864                file,
2865                source.line,
2866                1,
2867                "source: to nazwa zasobu z hosta (source: users)",
2868            ));
2869        }
2870    };
2871    let bindings = match res {
2872        Resolver::Static => {
2873            return Err(SeqError::new(
2874                file,
2875                node.line,
2876                1,
2877                "Async(source:) wymaga hosta Rust (HostBindings::resource())",
2878            ));
2879        }
2880        Resolver::Host(b) => (*b).clone(),
2881    };
2882    let resource = bindings.get_resource(&source_name).ok_or_else(|| {
2883        SeqError::new(
2884            file,
2885            source.line,
2886            1,
2887            format!("unknown resource '{source_name}' (HostBindings::resource())"),
2888        )
2889    })?;
2890
2891    let mut loading: Vec<SeqNode> = Vec::new();
2892    let mut data: Vec<SeqNode> = Vec::new();
2893    let mut error: Vec<SeqNode> = Vec::new();
2894    let mut idle: Vec<SeqNode> = Vec::new();
2895    let mut cancelled: Vec<SeqNode> = Vec::new();
2896    for c in &node.children {
2897        match c.kind.as_str() {
2898            "Loading" => loading.extend(c.children.clone()),
2899            "Data" => data.extend(c.children.clone()),
2900            "Error" => error.extend(c.children.clone()),
2901            "Idle" => idle.extend(c.children.clone()),
2902            "Cancelled" => cancelled.extend(c.children.clone()),
2903            other => {
2904                return Err(SeqError::new(
2905                    file,
2906                    c.line,
2907                    1,
2908                    format!("interior Async is Loading/Data/Error/Idle/Cancelled, not '{other}'"),
2909                ));
2910            }
2911        }
2912    }
2913
2914    let data_sig = Signal::new(String::new());
2915    let error_sig = Signal::new(String::new());
2916    {
2917        let res_sync = resource.clone();
2918        let data_sig = data_sig.clone();
2919        let error_sig = error_sig.clone();
2920        effect(move || match res_sync.state().get() {
2921            sequoia_async::AsyncState::Ready(v) => data_sig.set(v.clone()),
2922            sequoia_async::AsyncState::Failed(e) => error_sig.set(e.clone()),
2923            _ => {}
2924        })
2925        .forget();
2926    }
2927    let lower_once = |items: &[SeqNode], extra: Option<(&str, Signal<String>)>| -> AnyView {
2928        let mut b2 = bindings.clone();
2929        if let Some((k, sig)) = extra {
2930            b2 = b2.prop_str(k, sig);
2931        }
2932        let res = Resolver::Host(&b2);
2933        match items {
2934            [] => AnyView::new(Column::new()),
2935            [single] => lower_node(file, theme, styles, single, &res, row)
2936                .unwrap_or_else(|e| AnyView::new(Text::new(format!("seq: {e}")))),
2937            _ => {
2938                let mut col = Column::new();
2939                for n in items {
2940                    match lower_node(file, theme, styles, n, &res, row) {
2941                        Ok(kid) => {
2942                            let w = child_flex_weight(file, &res, row, n).unwrap_or(None);
2943                            col = match w {
2944                                Some(w) => col.child_flex(kid, w),
2945                                None => col.child(kid),
2946                            };
2947                        }
2948                        Err(e) => {
2949                            col = col.child(Text::new(format!("seq: {e}")));
2950                        }
2951                    }
2952                }
2953                AnyView::new(col)
2954            }
2955        }
2956    };
2957    let loading_view = lower_once(&loading, None);
2958    let data_view = lower_once(&data, Some(("data", data_sig)));
2959    let error_view = lower_once(&error, Some(("error", error_sig)));
2960    let mut view = AsyncView::new(resource);
2961    {
2962        let v = loading_view.clone();
2963        view = view.loading(move || v.clone());
2964    }
2965    {
2966        let v = data_view.clone();
2967        view = view.data(move |_: &String| v.clone());
2968    }
2969    {
2970        let v = error_view.clone();
2971        view = view.error(move |_: &str| v.clone());
2972    }
2973    if !idle.is_empty() {
2974        let v = lower_once(&idle, None);
2975        view = view.idle(move || v.clone());
2976    }
2977    if !cancelled.is_empty() {
2978        let v = lower_once(&cancelled, None);
2979        view = view.cancelled(move || v.clone());
2980    }
2981    Ok(AnyView::new(view))
2982}
2983
2984fn lower_effect(
2985    file: &str,
2986    node: &SeqNode,
2987    res: &Resolver<'_>,
2988    row: Option<&SeqRowData>,
2989) -> Result<AnyView, SeqError> {
2990    let track = find_arg(node, "track").ok_or_else(|| {
2991        SeqError::new(
2992            file,
2993            node.line,
2994            1,
2995            "Effect wymaga track: dynamic()".to_string(),
2996        )
2997    })?;
2998    let on_run = find_arg(node, "on_run").ok_or_else(|| {
2999        SeqError::new(
3000            file,
3001            node.line,
3002            1,
3003            "Effect wymaga on_run: { akcja() }".to_string(),
3004        )
3005    })?;
3006    let bindings = match res {
3007        Resolver::Static => {
3008            return Err(SeqError::new(
3009                file,
3010                node.line,
3011                1,
3012                "Effect wymaga hosta Rust",
3013            ));
3014        }
3015        Resolver::Host(b) => (*b).clone(),
3016    };
3017    let touch = track_touch(file, track.line, &bindings, row, &track.value)?;
3018    let fire: Rc<dyn Fn()> = match &on_run.value {
3019        SeqValue::Callback { action, args } => {
3020            if !args.is_empty() {
3021                return Err(SeqError::new(
3022                    file,
3023                    on_run.line,
3024                    1,
3025                    "on_run: no arguments (read status via track:)",
3026                ));
3027            }
3028            bindings.get_action0(action).ok_or_else(|| {
3029                SeqError::new(
3030                    file,
3031                    on_run.line,
3032                    1,
3033                    format!("unknown callback '{action}' (missing on host: on)"),
3034                )
3035            })?
3036        }
3037        SeqValue::CallSeq { name, args } => {
3038            if !args.is_empty() {
3039                return Err(SeqError::new(
3040                    file,
3041                    on_run.line,
3042                    1,
3043                    "on_run: no arguments (read status via track:)",
3044                ));
3045            }
3046            seq_fire(file, on_run.line, &bindings, name, Vec::new())?
3047        }
3048        _ => {
3049            return Err(SeqError::new(
3050                file,
3051                on_run.line,
3052                1,
3053                "on_run: { akcja(args) }",
3054            ));
3055        }
3056    };
3057    let seen = std::rc::Rc::new(std::cell::Cell::new(false));
3058    effect(move || {
3059        touch();
3060        if seen.get() {
3061            fire();
3062        } else {
3063            seen.set(true);
3064        }
3065    })
3066    .forget();
3067    Ok(AnyView::new(Column::new()))
3068}
3069
3070fn track_touch(
3071    file: &str,
3072    line: usize,
3073    b: &HostBindings,
3074    row: Option<&SeqRowData>,
3075    value: &SeqValue,
3076) -> Result<Rc<dyn Fn()>, SeqError> {
3077    match value {
3078        SeqValue::Bool(_) | SeqValue::Num(_) | SeqValue::Str(_) | SeqValue::Color(_) => {
3079            Ok(Rc::new(|| {}))
3080        }
3081        SeqValue::Ident(name) | SeqValue::Dynamic(name) => {
3082            if let Some(sig) = b.get_str(name) {
3083                Ok(Rc::new(move || {
3084                    sig.get();
3085                }))
3086            } else if let Some(m) = b.get_memo_str(name) {
3087                Ok(Rc::new(move || {
3088                    m.get();
3089                }))
3090            } else if let Some(sig) = b.get_bool(name) {
3091                Ok(Rc::new(move || {
3092                    sig.get();
3093                }))
3094            } else if let Some(m) = b.get_memo_bool(name) {
3095                Ok(Rc::new(move || {
3096                    m.get();
3097                }))
3098            } else if let Some(sig) = b.get_int(name) {
3099                Ok(Rc::new(move || {
3100                    sig.get();
3101                }))
3102            } else if let Some(m) = b.get_memo_int(name) {
3103                Ok(Rc::new(move || {
3104                    m.get();
3105                }))
3106            } else if let Some(sig) = b.get_float(name) {
3107                Ok(Rc::new(move || {
3108                    sig.get();
3109                }))
3110            } else if let Some(m) = b.get_memo_float(name) {
3111                Ok(Rc::new(move || {
3112                    m.get();
3113                }))
3114            } else if let Some(sig) = b.get_env_str(name) {
3115                Ok(Rc::new(move || {
3116                    sig.get();
3117                }))
3118            } else if let Some(sig) = b.get_env_bool(name) {
3119                Ok(Rc::new(move || {
3120                    sig.get();
3121                }))
3122            } else if let Some(sig) = b.get_env_int(name) {
3123                Ok(Rc::new(move || {
3124                    sig.get();
3125                }))
3126            } else if let Some(sig) = b.get_env_float(name) {
3127                Ok(Rc::new(move || {
3128                    sig.get();
3129                }))
3130            } else {
3131                Err(prop_err(file, line, name, "String/Bool/Int/Float"))
3132            }
3133        }
3134        SeqValue::Env(name) => {
3135            if let Some(sig) = b.get_env_str(name) {
3136                Ok(Rc::new(move || {
3137                    sig.get();
3138                }))
3139            } else if let Some(sig) = b.get_env_bool(name) {
3140                Ok(Rc::new(move || {
3141                    sig.get();
3142                }))
3143            } else if let Some(sig) = b.get_env_int(name) {
3144                Ok(Rc::new(move || {
3145                    sig.get();
3146                }))
3147            } else if let Some(sig) = b.get_env_float(name) {
3148                Ok(Rc::new(move || {
3149                    sig.get();
3150                }))
3151            } else {
3152                Err(SeqError::new(
3153                    file,
3154                    line,
3155                    1,
3156                    format!("unknown env.{name} (missing on host)"),
3157                ))
3158            }
3159        }
3160        SeqValue::ItemField(field) => match row_value(file, line, row, field)? {
3161            RowVal::Sig(FieldSignal::Str(sig)) => Ok(Rc::new(move || {
3162                sig.get();
3163            })),
3164            RowVal::Sig(FieldSignal::Bool(sig)) => Ok(Rc::new(move || {
3165                sig.get();
3166            })),
3167            RowVal::Sig(FieldSignal::Int(sig)) => Ok(Rc::new(move || {
3168                sig.get();
3169            })),
3170            RowVal::Sig(FieldSignal::Float(sig)) => Ok(Rc::new(move || {
3171                sig.get();
3172            })),
3173            RowVal::Id(_) => Ok(Rc::new(|| {})),
3174        },
3175        _ => Err(SeqError::new(
3176            file,
3177            line,
3178            1,
3179            "'track' to dynamic(nazwa)/env.nazwa/item.pole",
3180        )),
3181    }
3182}
3183
3184fn lower_checkable(
3185    file: &str,
3186    node: &SeqNode,
3187    res: &Resolver<'_>,
3188    row: Option<&SeqRowData>,
3189    is_switch: bool,
3190) -> Result<AnyView, SeqError> {
3191    let what = if is_switch { "Switch" } else { "Checkbox" };
3192    let checked_arg = find_arg(node, "checked").ok_or_else(|| {
3193        SeqError::new(
3194            file,
3195            node.line,
3196            1,
3197            format!("{what} wymaga checked: true/false/prop"),
3198        )
3199    })?;
3200    let sig = writable_bool(
3201        file,
3202        checked_arg.line,
3203        res,
3204        row,
3205        &checked_arg.value,
3206        "checked",
3207    )?;
3208    let label = opt_str(file, node, "label", res, row)?;
3209    let mut font_size: Option<f32> = None;
3210    let mut disabled = false;
3211    let mut margin: Option<MarginVal> = None;
3212    let mut margin_x: Option<f32> = None;
3213    let mut margin_y: Option<f32> = None;
3214    let mut margin_left: Option<f32> = None;
3215    let mut margin_right: Option<f32> = None;
3216    let mut margin_top: Option<f32> = None;
3217    let mut margin_bottom: Option<f32> = None;
3218    let mut toggle: Option<Rc<dyn Fn()>> = None;
3219    for arg in &node.args {
3220        match arg.name.as_str() {
3221            "checked" => {}
3222            "label" => {}
3223            "font_size" => font_size = Some(num(file, arg, res, row)?),
3224            "enabled" => disabled = !flag(file, arg, res, row)?,
3225            "margin" => margin = Some(margin_val(file, arg, res, row)?),
3226            "margin_x" => margin_x = Some(num(file, arg, res, row)?),
3227            "margin_y" => margin_y = Some(num(file, arg, res, row)?),
3228            "margin_left" => margin_left = Some(num(file, arg, res, row)?),
3229            "margin_right" => margin_right = Some(num(file, arg, res, row)?),
3230            "margin_top" => margin_top = Some(num(file, arg, res, row)?),
3231            "margin_bottom" => margin_bottom = Some(num(file, arg, res, row)?),
3232            "on_toggle" => match res {
3233                Resolver::Static => {
3234                    return Err(SeqError::new(
3235                        file,
3236                        arg.line,
3237                        1,
3238                        "on_toggle wymaga hosta Rust",
3239                    ));
3240                }
3241                Resolver::Host(b) => match &arg.value {
3242                    SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
3243                        let live = LiveVal::Bool(sig.clone());
3244                        toggle = Some(wire_live_action(
3245                            file,
3246                            arg.line,
3247                            b,
3248                            &arg.value,
3249                            &live,
3250                            "on_toggle",
3251                        )?);
3252                    }
3253                    _ => {
3254                        return Err(SeqError::new(
3255                            file,
3256                            arg.line,
3257                            1,
3258                            "on_toggle: { akcja() } albo { akcja(value) }",
3259                        ));
3260                    }
3261                },
3262            },
3263            "flex" => {}
3264            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
3265            | "indent_bottom" => {}
3266            _ => unreachable_checked(file, node, arg)?,
3267        }
3268    }
3269    if is_switch {
3270        let mut v = Switch::new(sig);
3271        if let Some(label) = label {
3272            v = v.label(label);
3273        }
3274        if let Some(size) = font_size {
3275            v = v.font_size(size);
3276        }
3277        v = v.disabled(disabled);
3278        if let Some(fire) = toggle {
3279            v = v.on_toggle(move |_| fire());
3280        }
3281        v = apply_switch_margin(
3282            v,
3283            margin,
3284            margin_x,
3285            margin_y,
3286            margin_left,
3287            margin_right,
3288            margin_top,
3289            margin_bottom,
3290        );
3291        Ok(AnyView::new(v))
3292    } else {
3293        let mut v = Checkbox::new(sig);
3294        if let Some(label) = label {
3295            v = v.label(label);
3296        }
3297        if let Some(size) = font_size {
3298            v = v.font_size(size);
3299        }
3300        v = v.disabled(disabled);
3301        if let Some(fire) = toggle {
3302            v = v.on_toggle(move |_| fire());
3303        }
3304        v = apply_checkbox_margin(
3305            v,
3306            margin,
3307            margin_x,
3308            margin_y,
3309            margin_left,
3310            margin_right,
3311            margin_top,
3312            margin_bottom,
3313        );
3314        Ok(AnyView::new(v))
3315    }
3316}
3317
3318#[allow(clippy::too_many_arguments)]
3319fn apply_checkbox_margin(
3320    mut v: Checkbox,
3321    margin: Option<MarginVal>,
3322    margin_x: Option<f32>,
3323    margin_y: Option<f32>,
3324    margin_left: Option<f32>,
3325    margin_right: Option<f32>,
3326    margin_top: Option<f32>,
3327    margin_bottom: Option<f32>,
3328) -> Checkbox {
3329    if let Some(m) = margin {
3330        v = match m {
3331            MarginVal::All(n) => v.margin(n),
3332            MarginVal::Xy(x, y) => v.margin_xy(x, y),
3333        };
3334    }
3335    if let Some(x) = margin_x {
3336        v = v.margin_x(x);
3337    }
3338    if let Some(y) = margin_y {
3339        v = v.margin_y(y);
3340    }
3341    if let Some(x) = margin_left {
3342        v = v.margin_left(x);
3343    }
3344    if let Some(x) = margin_right {
3345        v = v.margin_right(x);
3346    }
3347    if let Some(y) = margin_top {
3348        v = v.margin_top(y);
3349    }
3350    if let Some(y) = margin_bottom {
3351        v = v.margin_bottom(y);
3352    }
3353    v
3354}
3355
3356#[allow(clippy::too_many_arguments)]
3357fn apply_switch_margin(
3358    mut v: Switch,
3359    margin: Option<MarginVal>,
3360    margin_x: Option<f32>,
3361    margin_y: Option<f32>,
3362    margin_left: Option<f32>,
3363    margin_right: Option<f32>,
3364    margin_top: Option<f32>,
3365    margin_bottom: Option<f32>,
3366) -> Switch {
3367    if let Some(m) = margin {
3368        v = match m {
3369            MarginVal::All(n) => v.margin(n),
3370            MarginVal::Xy(x, y) => v.margin_xy(x, y),
3371        };
3372    }
3373    if let Some(x) = margin_x {
3374        v = v.margin_x(x);
3375    }
3376    if let Some(y) = margin_y {
3377        v = v.margin_y(y);
3378    }
3379    if let Some(x) = margin_left {
3380        v = v.margin_left(x);
3381    }
3382    if let Some(x) = margin_right {
3383        v = v.margin_right(x);
3384    }
3385    if let Some(y) = margin_top {
3386        v = v.margin_top(y);
3387    }
3388    if let Some(y) = margin_bottom {
3389        v = v.margin_bottom(y);
3390    }
3391    v
3392}
3393
3394fn lower_checkbox(
3395    file: &str,
3396    _theme: &Theme,
3397    node: &SeqNode,
3398    res: &Resolver<'_>,
3399    row: Option<&SeqRowData>,
3400) -> Result<AnyView, SeqError> {
3401    lower_checkable(file, node, res, row, false)
3402}
3403
3404fn lower_switch(
3405    file: &str,
3406    _theme: &Theme,
3407    node: &SeqNode,
3408    res: &Resolver<'_>,
3409    row: Option<&SeqRowData>,
3410) -> Result<AnyView, SeqError> {
3411    lower_checkable(file, node, res, row, true)
3412}
3413
3414fn lower_textfield(
3415    file: &str,
3416    _theme: &Theme,
3417    node: &SeqNode,
3418    res: &Resolver<'_>,
3419    row: Option<&SeqRowData>,
3420) -> Result<AnyView, SeqError> {
3421    let value_arg = find_arg(node, "value").ok_or_else(|| {
3422        SeqError::new(
3423            file,
3424            node.line,
3425            1,
3426            "TextField wymaga value: prop".to_string(),
3427        )
3428    })?;
3429    let sig = writable_str(file, value_arg.line, res, row, &value_arg.value, "value")?;
3430    let mut v = TextField::new(sig.clone());
3431    for arg in &node.args {
3432        match arg.name.as_str() {
3433            "value" => {}
3434            "placeholder" => {
3435                if let Some(text) = opt_str(file, node, "placeholder", res, row)? {
3436                    v = v.placeholder(text);
3437                }
3438            }
3439            "font_size" => v = v.font_size(num(file, arg, res, row)?),
3440            "width" => v = v.width(len(file, arg, res, row)?),
3441            "height" => v = v.height(len(file, arg, res, row)?),
3442            "min_width" => v = v.min_width(num(file, arg, res, row)?),
3443            "max_width" => v = v.max_width(num(file, arg, res, row)?),
3444            "max_height" => v = v.max_height(num(file, arg, res, row)?),
3445            "margin" => match margin_val(file, arg, res, row)? {
3446                MarginVal::All(n) => v = v.margin(n),
3447                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
3448            },
3449            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
3450            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
3451            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
3452            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
3453            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
3454            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
3455            "on_submit" => match res {
3456                Resolver::Static => {
3457                    return Err(SeqError::new(
3458                        file,
3459                        arg.line,
3460                        1,
3461                        "on_submit wymaga hosta Rust",
3462                    ));
3463                }
3464                Resolver::Host(b) => match &arg.value {
3465                    SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
3466                        let live = LiveVal::Str(sig.clone());
3467                        let fire =
3468                            wire_live_action(file, arg.line, b, &arg.value, &live, "on_submit")?;
3469                        v = v.on_submit(move |_| fire());
3470                    }
3471                    _ => {
3472                        return Err(SeqError::new(
3473                            file,
3474                            arg.line,
3475                            1,
3476                            "on_submit: { akcja() } albo { akcja(value) }",
3477                        ));
3478                    }
3479                },
3480            },
3481            "flex" => {}
3482            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
3483            | "indent_bottom" => {}
3484            _ => unreachable_checked(file, node, arg)?,
3485        }
3486    }
3487    Ok(AnyView::new(v))
3488}
3489
3490#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
3491fn count_val(
3492    file: &str,
3493    node: &SeqNode,
3494    res: &Resolver<'_>,
3495    row: Option<&SeqRowData>,
3496    name: &str,
3497) -> Result<Option<usize>, SeqError> {
3498    let Some(arg) = find_arg(node, name) else {
3499        return Ok(None);
3500    };
3501    let err = || SeqError::new(file, arg.line, 1, format!("'{name}' is an integer >= 0"));
3502    match &arg.value {
3503        SeqValue::Num(n) => {
3504            if n.is_finite() && *n >= 0.0 && n.fract() == 0.0 {
3505                Ok(Some(*n as usize))
3506            } else {
3507                Err(err())
3508            }
3509        }
3510        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
3511            Resolver::Static => Err(SeqError::new(
3512                file,
3513                arg.line,
3514                1,
3515                format!("'{name}' z propertu wymaga hosta"),
3516            )),
3517            Resolver::Host(b) => b
3518                .get_int(prop)
3519                .map(|sig| sig.get_untracked())
3520                .or_else(|| b.get_memo_int(prop).map(|m| m.get_untracked()))
3521                .or_else(|| b.get_env_int(prop).map(|sig| sig.get_untracked()))
3522                .map(|v| Some(v.max(0) as usize))
3523                .ok_or_else(|| prop_err(file, arg.line, prop, "Int")),
3524        },
3525        SeqValue::Env(env_name) => match res {
3526            Resolver::Static => Err(SeqError::new(
3527                file,
3528                arg.line,
3529                1,
3530                format!("'{name}' from environment requires host"),
3531            )),
3532            Resolver::Host(b) => b
3533                .get_env_int(env_name)
3534                .map(|sig| Some(sig.get_untracked().max(0) as usize))
3535                .ok_or_else(|| {
3536                    SeqError::new(
3537                        file,
3538                        arg.line,
3539                        1,
3540                        format!("unknown env.{env_name} or wrong type (here you need: Int)"),
3541                    )
3542                }),
3543        },
3544        SeqValue::ItemField(field) => match res {
3545            Resolver::Static => Err(SeqError::new(
3546                file,
3547                arg.line,
3548                1,
3549                format!("'{name}' z wiersza wymaga hosta"),
3550            )),
3551            Resolver::Host(_) => match row_value(file, arg.line, row, field)? {
3552                RowVal::Sig(FieldSignal::Int(sig)) => Ok(Some(sig.get_untracked().max(0) as usize)),
3553                RowVal::Id(id) => Ok(Some(id.max(0) as usize)),
3554                _ => Err(SeqError::new(
3555                    file,
3556                    arg.line,
3557                    1,
3558                    format!("pole wiersza '{field}' to nie Int"),
3559                )),
3560            },
3561        },
3562        _ => Err(err()),
3563    }
3564}
3565
3566fn label_arg(
3567    file: &str,
3568    node: &SeqNode,
3569    res: &Resolver<'_>,
3570    row: Option<&SeqRowData>,
3571    kind: &str,
3572) -> Result<String, SeqError> {
3573    opt_str(file, node, "label", res, row)?
3574        .ok_or_else(|| SeqError::new(file, node.line, 1, format!("{kind} wymaga label: \"...\"")))
3575}
3576
3577fn str_list_arg(file: &str, node: &SeqNode, name: &str) -> Result<Vec<String>, SeqError> {
3578    let arg = find_arg(node, name).ok_or_else(|| {
3579        SeqError::new(
3580            file,
3581            node.line,
3582            1,
3583            format!("{0} wymaga {name}: [\"...\", ...]", node.kind),
3584        )
3585    })?;
3586    match &arg.value {
3587        SeqValue::Array(items) if !items.is_empty() => items
3588            .iter()
3589            .map(|it| match it {
3590                SeqValue::Str(s) => Ok(s.clone()),
3591                _ => Err(SeqError::new(
3592                    file,
3593                    arg.line,
3594                    1,
3595                    format!("'{name}' to [\"...\", ...] (same napisy)"),
3596                )),
3597            })
3598            .collect(),
3599        _ => Err(SeqError::new(
3600            file,
3601            arg.line,
3602            1,
3603            format!("'{name}' to niepusta [\"...\", ...]"),
3604        )),
3605    }
3606}
3607
3608fn lower_textarea(
3609    file: &str,
3610    _theme: &Theme,
3611    node: &SeqNode,
3612    res: &Resolver<'_>,
3613    row: Option<&SeqRowData>,
3614) -> Result<AnyView, SeqError> {
3615    let value_arg = find_arg(node, "value").ok_or_else(|| {
3616        SeqError::new(
3617            file,
3618            node.line,
3619            1,
3620            "TextArea wymaga value: prop".to_string(),
3621        )
3622    })?;
3623    let sig = writable_str(file, value_arg.line, res, row, &value_arg.value, "value")?;
3624    let mut v = TextArea::new(sig.clone());
3625    for arg in &node.args {
3626        match arg.name.as_str() {
3627            "value" => {}
3628            "placeholder" => {
3629                if let Some(text) = opt_str(file, node, "placeholder", res, row)? {
3630                    v = v.placeholder(text);
3631                }
3632            }
3633            "max_length" => {
3634                if let Some(n) = count_val(file, node, res, row, "max_length")? {
3635                    v = v.max_length(n);
3636                }
3637            }
3638            "min_lines" => {
3639                if let Some(n) = count_val(file, node, res, row, "min_lines")? {
3640                    v = v.min_lines(n.max(1));
3641                }
3642            }
3643            "max_lines" => {
3644                if let Some(n) = count_val(file, node, res, row, "max_lines")? {
3645                    v = v.max_lines(n.max(1));
3646                }
3647            }
3648            "font_size" => v = v.font_size(num(file, arg, res, row)?),
3649            "width" => v = v.width(len(file, arg, res, row)?),
3650            "height" => v = v.height(len(file, arg, res, row)?),
3651            "min_width" => v = v.min_width(num(file, arg, res, row)?),
3652            "max_width" => v = v.max_width(num(file, arg, res, row)?),
3653            "min_height" => v = v.min_height(num(file, arg, res, row)?),
3654            "max_height" => v = v.max_height(num(file, arg, res, row)?),
3655            "flex" => {}
3656            "margin" => match margin_val(file, arg, res, row)? {
3657                MarginVal::All(n) => v = v.margin(n),
3658                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
3659            },
3660            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
3661            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
3662            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
3663            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
3664            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
3665            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
3666            "on_edit_end" => match res {
3667                Resolver::Static => {
3668                    return Err(SeqError::new(
3669                        file,
3670                        arg.line,
3671                        1,
3672                        "on_edit_end wymaga hosta Rust",
3673                    ));
3674                }
3675                Resolver::Host(b) => match &arg.value {
3676                    SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
3677                        let live = LiveVal::Str(sig.clone());
3678                        let fire =
3679                            wire_live_action(file, arg.line, b, &arg.value, &live, "on_edit_end")?;
3680                        v = v.on_edit_end(move |_| fire());
3681                    }
3682                    _ => {
3683                        return Err(SeqError::new(
3684                            file,
3685                            arg.line,
3686                            1,
3687                            "on_edit_end: { akcja() } albo { akcja(value) }",
3688                        ));
3689                    }
3690                },
3691            },
3692            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
3693            | "indent_bottom" => {}
3694            _ => unreachable_checked(file, node, arg)?,
3695        }
3696    }
3697    Ok(AnyView::new(v))
3698}
3699
3700fn lower_form_field(
3701    file: &str,
3702    _theme: &Theme,
3703    node: &SeqNode,
3704    res: &Resolver<'_>,
3705    row: Option<&SeqRowData>,
3706) -> Result<AnyView, SeqError> {
3707    let value_arg = find_arg(node, "value")
3708        .ok_or_else(|| SeqError::new(file, node.line, 1, "Field wymaga value: prop".to_string()))?;
3709    let sig = writable_str(file, value_arg.line, res, row, &value_arg.value, "value")?;
3710    let label = label_arg(file, node, res, row, "Field")?;
3711    let mut v = Field::new(sig.clone(), label);
3712    for arg in &node.args {
3713        match arg.name.as_str() {
3714            "value" | "label" => {}
3715            "placeholder" => {
3716                if let Some(text) = opt_str(file, node, "placeholder", res, row)? {
3717                    v = v.placeholder(text);
3718                }
3719            }
3720            _ => unreachable_checked(file, node, arg)?,
3721        }
3722    }
3723    Ok(AnyView::new(v))
3724}
3725
3726fn lower_number_field(
3727    file: &str,
3728    _theme: &Theme,
3729    node: &SeqNode,
3730    res: &Resolver<'_>,
3731    row: Option<&SeqRowData>,
3732) -> Result<AnyView, SeqError> {
3733    let value_arg = find_arg(node, "value").ok_or_else(|| {
3734        SeqError::new(
3735            file,
3736            node.line,
3737            1,
3738            "NumberField wymaga value: prop".to_string(),
3739        )
3740    })?;
3741    let sig = bridge_f32(file, value_arg.line, res, row, &value_arg.value, "value")?;
3742    let label = label_arg(file, node, res, row, "NumberField")?;
3743    let mut v = NumberField::new(sig.clone(), label);
3744    let mut min = f32::NEG_INFINITY;
3745    let mut max = f32::INFINITY;
3746    for arg in &node.args {
3747        match arg.name.as_str() {
3748            "value" | "label" => {}
3749            "min" => min = num(file, arg, res, row)?,
3750            "max" => max = num(file, arg, res, row)?,
3751            "step" => v = v.step(num(file, arg, res, row)?),
3752            "precision" => {
3753                if let Some(n) = count_val(file, node, res, row, "precision")? {
3754                    v = v.precision(n);
3755                }
3756            }
3757            _ => unreachable_checked(file, node, arg)?,
3758        }
3759    }
3760    if min > max {
3761        return Err(SeqError::new(
3762            file,
3763            node.line,
3764            1,
3765            "NumberField wymaga min <= max",
3766        ));
3767    }
3768    v = v.min(min).max(max);
3769    Ok(AnyView::new(v))
3770}
3771
3772fn lower_date_field(
3773    file: &str,
3774    _theme: &Theme,
3775    node: &SeqNode,
3776    res: &Resolver<'_>,
3777    row: Option<&SeqRowData>,
3778) -> Result<AnyView, SeqError> {
3779    let value_arg = find_arg(node, "value").ok_or_else(|| {
3780        SeqError::new(
3781            file,
3782            node.line,
3783            1,
3784            "DateField wymaga value: prop".to_string(),
3785        )
3786    })?;
3787    let sig = writable_str(file, value_arg.line, res, row, &value_arg.value, "value")?;
3788    let label = label_arg(file, node, res, row, "DateField")?;
3789    let v = DateField::new(sig.clone(), label);
3790    for arg in &node.args {
3791        match arg.name.as_str() {
3792            "value" | "label" => {}
3793            _ => unreachable_checked(file, node, arg)?,
3794        }
3795    }
3796    Ok(AnyView::new(v))
3797}
3798
3799fn lower_multi_select(
3800    file: &str,
3801    _theme: &Theme,
3802    node: &SeqNode,
3803    res: &Resolver<'_>,
3804    row: Option<&SeqRowData>,
3805) -> Result<AnyView, SeqError> {
3806    let options = str_list_arg(file, node, "options")?;
3807    let label = label_arg(file, node, res, row, "MultiSelect")?;
3808    let mut v = MultiSelect::new(options.clone(), label);
3809    if let Some(n) = count_val(file, node, res, row, "min_selected")? {
3810        if n > options.len() {
3811            return Err(SeqError::new(
3812                file,
3813                node.line,
3814                1,
3815                format!(
3816                    "min_selected ({n}) over number of options ({})",
3817                    options.len()
3818                ),
3819            ));
3820        }
3821        v = v.min_selected(n);
3822    }
3823    for arg in &node.args {
3824        match arg.name.as_str() {
3825            "options" | "label" | "min_selected" => {}
3826            _ => unreachable_checked(file, node, arg)?,
3827        }
3828    }
3829    Ok(AnyView::new(v))
3830}
3831
3832fn lower_drag_value(
3833    file: &str,
3834    _theme: &Theme,
3835    node: &SeqNode,
3836    res: &Resolver<'_>,
3837    row: Option<&SeqRowData>,
3838) -> Result<AnyView, SeqError> {
3839    let value_arg = find_arg(node, "value").ok_or_else(|| {
3840        SeqError::new(
3841            file,
3842            node.line,
3843            1,
3844            "DragValue wymaga value: prop".to_string(),
3845        )
3846    })?;
3847    let sig = bridge_f32(file, value_arg.line, res, row, &value_arg.value, "value")?;
3848    let mut v = DragValue::new(sig.clone());
3849    let mut min = f32::NEG_INFINITY;
3850    let mut max = f32::INFINITY;
3851    for arg in &node.args {
3852        match arg.name.as_str() {
3853            "value" => {}
3854            "min" => min = num(file, arg, res, row)?,
3855            "max" => max = num(file, arg, res, row)?,
3856            "step" => v = v.step(num(file, arg, res, row)?),
3857            "precision" => {
3858                if let Some(n) = count_val(file, node, res, row, "precision")? {
3859                    v = v.precision(n);
3860                }
3861            }
3862            "width" => v = v.width(len(file, arg, res, row)?),
3863            "height" => v = v.height(len(file, arg, res, row)?),
3864            "min_width" => v = v.min_width(num(file, arg, res, row)?),
3865            "max_width" => v = v.max_width(num(file, arg, res, row)?),
3866            "min_height" => v = v.min_height(num(file, arg, res, row)?),
3867            "max_height" => v = v.max_height(num(file, arg, res, row)?),
3868            "flex" => {}
3869            "margin" => match margin_val(file, arg, res, row)? {
3870                MarginVal::All(n) => v = v.margin(n),
3871                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
3872            },
3873            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
3874            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
3875            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
3876            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
3877            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
3878            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
3879            "on_commit" => match res {
3880                Resolver::Static => {
3881                    return Err(SeqError::new(
3882                        file,
3883                        arg.line,
3884                        1,
3885                        "on_commit wymaga hosta Rust",
3886                    ));
3887                }
3888                Resolver::Host(b) => match &arg.value {
3889                    SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
3890                        let live = LiveVal::F32(sig.clone());
3891                        let fire =
3892                            wire_live_action(file, arg.line, b, &arg.value, &live, "on_commit")?;
3893                        v = v.on_commit(move |_| fire());
3894                    }
3895                    _ => {
3896                        return Err(SeqError::new(
3897                            file,
3898                            arg.line,
3899                            1,
3900                            "on_commit: { akcja() } albo { akcja(value) }",
3901                        ));
3902                    }
3903                },
3904            },
3905            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
3906            | "indent_bottom" => {}
3907            _ => unreachable_checked(file, node, arg)?,
3908        }
3909    }
3910    if min > max {
3911        return Err(SeqError::new(
3912            file,
3913            node.line,
3914            1,
3915            "DragValue wymaga min <= max",
3916        ));
3917    }
3918    v = v.min(min).max(max);
3919    Ok(AnyView::new(v))
3920}
3921
3922fn lower_slider(
3923    file: &str,
3924    _theme: &Theme,
3925    node: &SeqNode,
3926    res: &Resolver<'_>,
3927    row: Option<&SeqRowData>,
3928) -> Result<AnyView, SeqError> {
3929    let value_arg = find_arg(node, "value").ok_or_else(|| {
3930        SeqError::new(file, node.line, 1, "Slider wymaga value: prop".to_string())
3931    })?;
3932    let sig = bridge_f32(file, value_arg.line, res, row, &value_arg.value, "value")?;
3933    let mut min = 0.0;
3934    let mut max = 1.0;
3935    if let Some(arg) = find_arg(node, "min") {
3936        min = num(file, arg, res, row)?;
3937    }
3938    if let Some(arg) = find_arg(node, "max") {
3939        max = num(file, arg, res, row)?;
3940    }
3941    if !(min.is_finite() && max.is_finite() && min < max) {
3942        return Err(SeqError::new(
3943            file,
3944            node.line,
3945            1,
3946            "Slider requires min < max (finished)",
3947        ));
3948    }
3949    let mut v = Slider1D::new(sig.clone(), min, max);
3950    for arg in &node.args {
3951        match arg.name.as_str() {
3952            "value" | "min" | "max" => {}
3953            "width" => v = v.width(len(file, arg, res, row)?),
3954            "height" => v = v.height(len(file, arg, res, row)?),
3955            "margin" => match margin_val(file, arg, res, row)? {
3956                MarginVal::All(n) => v = v.margin(n),
3957                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
3958            },
3959            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
3960            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
3961            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
3962            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
3963            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
3964            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
3965            "on_change" => match res {
3966                Resolver::Static => {
3967                    return Err(SeqError::new(
3968                        file,
3969                        arg.line,
3970                        1,
3971                        "on_change wymaga hosta Rust",
3972                    ));
3973                }
3974                Resolver::Host(b) => match &arg.value {
3975                    SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
3976                        let live = LiveVal::F32(sig.clone());
3977                        let fire =
3978                            wire_live_action(file, arg.line, b, &arg.value, &live, "on_change")?;
3979                        v = v.on_commit(move |_| fire());
3980                    }
3981                    _ => {
3982                        return Err(SeqError::new(
3983                            file,
3984                            arg.line,
3985                            1,
3986                            "on_change: { akcja() } albo { akcja(value) }",
3987                        ));
3988                    }
3989                },
3990            },
3991            "flex" => {}
3992            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
3993            | "indent_bottom" => {}
3994            _ => unreachable_checked(file, node, arg)?,
3995        }
3996    }
3997    Ok(AnyView::new(v))
3998}
3999
4000fn lower_dropdown(
4001    file: &str,
4002    _theme: &Theme,
4003    node: &SeqNode,
4004    res: &Resolver<'_>,
4005    row: Option<&SeqRowData>,
4006) -> Result<AnyView, SeqError> {
4007    let sel_arg = find_arg(node, "selected").ok_or_else(|| {
4008        SeqError::new(
4009            file,
4010            node.line,
4011            1,
4012            "Dropdown wymaga selected: prop".to_string(),
4013        )
4014    })?;
4015    let sig = bridge_usize(file, sel_arg.line, res, row, &sel_arg.value, "selected")?;
4016    let items_arg = find_arg(node, "items").ok_or_else(|| {
4017        SeqError::new(
4018            file,
4019            node.line,
4020            1,
4021            "Dropdown wymaga items: [\"A\", \"B\"]".to_string(),
4022        )
4023    })?;
4024    let options = match &items_arg.value {
4025        SeqValue::Array(items) => items
4026            .iter()
4027            .map(|it| match it {
4028                SeqValue::Str(s) => Ok(s.clone()),
4029                _ => Err(SeqError::new(
4030                    file,
4031                    items_arg.line,
4032                    1,
4033                    "items: to [\"tekst\", ...]",
4034                )),
4035            })
4036            .collect::<Result<Vec<_>, _>>()?,
4037        _ => {
4038            return Err(SeqError::new(
4039                file,
4040                items_arg.line,
4041                1,
4042                "items: to [\"tekst\", ...]",
4043            ));
4044        }
4045    };
4046    let mut v = DropdownButton::new(sig.clone(), options);
4047    for arg in &node.args {
4048        match arg.name.as_str() {
4049            "selected" | "items" => {}
4050            "placeholder" => {
4051                if let Some(text) = opt_str(file, node, "placeholder", res, row)? {
4052                    v = v.placeholder(text);
4053                }
4054            }
4055            "font_size" => v = v.font_size(num(file, arg, res, row)?),
4056            "min_width" => v = v.min_width(num(file, arg, res, row)?),
4057            "max_width" => v = v.max_width(num(file, arg, res, row)?),
4058            "min_height" => v = v.min_height(num(file, arg, res, row)?),
4059            "max_height" => v = v.max_height(num(file, arg, res, row)?),
4060            "enabled" => v = v.disabled(!flag(file, arg, res, row)?),
4061            "margin" => match margin_val(file, arg, res, row)? {
4062                MarginVal::All(n) => v = v.margin(n),
4063                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
4064            },
4065            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
4066            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
4067            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
4068            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
4069            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
4070            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
4071            "on_select" => match res {
4072                Resolver::Static => {
4073                    return Err(SeqError::new(
4074                        file,
4075                        arg.line,
4076                        1,
4077                        "on_select wymaga hosta Rust",
4078                    ));
4079                }
4080                Resolver::Host(b) => match &arg.value {
4081                    SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
4082                        let live = LiveVal::Usize(sig.clone());
4083                        let fire =
4084                            wire_live_action(file, arg.line, b, &arg.value, &live, "on_select")?;
4085                        v = v.on_select(move |_| fire());
4086                    }
4087                    _ => {
4088                        return Err(SeqError::new(
4089                            file,
4090                            arg.line,
4091                            1,
4092                            "on_select: { akcja() } albo { akcja(value) }",
4093                        ));
4094                    }
4095                },
4096            },
4097            "flex" => {}
4098            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
4099            | "indent_bottom" => {}
4100            _ => unreachable_checked(file, node, arg)?,
4101        }
4102    }
4103    Ok(AnyView::new(v))
4104}
4105
4106fn lower_progress(
4107    file: &str,
4108    _theme: &Theme,
4109    node: &SeqNode,
4110    res: &Resolver<'_>,
4111    row: Option<&SeqRowData>,
4112) -> Result<AnyView, SeqError> {
4113    let value_arg = find_arg(node, "value").ok_or_else(|| {
4114        SeqError::new(
4115            file,
4116            node.line,
4117            1,
4118            "Progress wymaga value: prop".to_string(),
4119        )
4120    })?;
4121    let sig = live_f32(file, value_arg.line, res, row, &value_arg.value, "value")?;
4122    let mut v = ProgressBar::new(sig);
4123    for arg in &node.args {
4124        match arg.name.as_str() {
4125            "value" => {}
4126            "height" => v = v.height(num(file, arg, res, row)?),
4127            "track" => v = v.track(color(file, _theme, arg)?),
4128            "fill" => v = v.fill(color(file, _theme, arg)?),
4129            "flex" => {}
4130            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
4131            | "indent_bottom" => {}
4132            _ => unreachable_checked(file, node, arg)?,
4133        }
4134    }
4135    Ok(AnyView::new(v))
4136}
4137
4138fn writable_bool(
4139    file: &str,
4140    line: usize,
4141    res: &Resolver<'_>,
4142    row: Option<&SeqRowData>,
4143    value: &SeqValue,
4144    what: &str,
4145) -> Result<Signal<bool>, SeqError> {
4146    match value {
4147        SeqValue::Bool(b) => Ok(Signal::new(*b)),
4148        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
4149            Resolver::Static => Err(SeqError::new(
4150                file,
4151                line,
4152                1,
4153                format!("'{what}' z propertu wymaga hosta"),
4154            )),
4155            Resolver::Host(b) => {
4156                if let Some(sig) = b.get_bool(prop) {
4157                    Ok(sig)
4158                } else if b.get_memo_bool(prop).is_some() {
4159                    Err(SeqError::new(
4160                        file,
4161                        line,
4162                        1,
4163                        format!("'{what}' z memo jest tylko do odczytu — podepnij prop Bool"),
4164                    ))
4165                } else {
4166                    Err(prop_err(file, line, prop, "Bool"))
4167                }
4168            }
4169        },
4170        SeqValue::Env(name) => match res {
4171            Resolver::Static => Err(SeqError::new(
4172                file,
4173                line,
4174                1,
4175                format!("'{what}' from environment requires host"),
4176            )),
4177            Resolver::Host(b) => b.get_env_bool(name).ok_or_else(|| {
4178                SeqError::new(
4179                    file,
4180                    line,
4181                    1,
4182                    format!("unknown env.{name} or wrong type (here you need: Bool)"),
4183                )
4184            }),
4185        },
4186        SeqValue::ItemField(field) => match res {
4187            Resolver::Static => Err(SeqError::new(
4188                file,
4189                line,
4190                1,
4191                format!("'{what}' z wiersza wymaga hosta"),
4192            )),
4193            Resolver::Host(_) => match row_value(file, line, row, field)? {
4194                RowVal::Sig(FieldSignal::Bool(sig)) => Ok(sig),
4195                _ => Err(SeqError::new(
4196                    file,
4197                    line,
4198                    1,
4199                    format!("pole wiersza '{field}' to nie Bool"),
4200                )),
4201            },
4202        },
4203        _ => Err(SeqError::new(
4204            file,
4205            line,
4206            1,
4207            format!("'{what}' to true/false albo nazwa propertu"),
4208        )),
4209    }
4210}
4211
4212fn writable_str(
4213    file: &str,
4214    line: usize,
4215    res: &Resolver<'_>,
4216    row: Option<&SeqRowData>,
4217    value: &SeqValue,
4218    what: &str,
4219) -> Result<Signal<String>, SeqError> {
4220    match value {
4221        SeqValue::Str(s) => Ok(Signal::new(s.clone())),
4222        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
4223            Resolver::Static => Err(SeqError::new(
4224                file,
4225                line,
4226                1,
4227                format!("'{what}' z propertu wymaga hosta"),
4228            )),
4229            Resolver::Host(b) => {
4230                if let Some(sig) = b.get_str(prop) {
4231                    Ok(sig)
4232                } else if b.get_memo_str(prop).is_some() {
4233                    Err(SeqError::new(
4234                        file,
4235                        line,
4236                        1,
4237                        format!("'{what}' z memo jest tylko do odczytu — podepnij prop String"),
4238                    ))
4239                } else {
4240                    Err(prop_err(file, line, prop, "String"))
4241                }
4242            }
4243        },
4244        SeqValue::Env(name) => match res {
4245            Resolver::Static => Err(SeqError::new(
4246                file,
4247                line,
4248                1,
4249                format!("'{what}' from environment requires host"),
4250            )),
4251            Resolver::Host(b) => b.get_env_str(name).ok_or_else(|| {
4252                SeqError::new(
4253                    file,
4254                    line,
4255                    1,
4256                    format!("unknown env.{name} or wrong type (here you need: String)"),
4257                )
4258            }),
4259        },
4260        SeqValue::ItemField(field) => match res {
4261            Resolver::Static => Err(SeqError::new(
4262                file,
4263                line,
4264                1,
4265                format!("'{what}' z wiersza wymaga hosta"),
4266            )),
4267            Resolver::Host(_) => match row_value(file, line, row, field)? {
4268                RowVal::Sig(FieldSignal::Str(sig)) => Ok(sig),
4269                _ => Err(SeqError::new(
4270                    file,
4271                    line,
4272                    1,
4273                    format!("pole wiersza '{field}' to tekst (String), nie '{field}'"),
4274                )),
4275            },
4276        },
4277        _ => Err(SeqError::new(
4278            file,
4279            line,
4280            1,
4281            format!("'{what}' to napis albo nazwa propertu"),
4282        )),
4283    }
4284}
4285
4286#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
4287fn bridge_f32(
4288    file: &str,
4289    line: usize,
4290    res: &Resolver<'_>,
4291    row: Option<&SeqRowData>,
4292    value: &SeqValue,
4293    what: &str,
4294) -> Result<Signal<f32>, SeqError> {
4295    match value {
4296        SeqValue::Num(n) => Ok(Signal::new(*n)),
4297        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
4298            Resolver::Static => Err(SeqError::new(
4299                file,
4300                line,
4301                1,
4302                format!("'{what}' z propertu wymaga hosta"),
4303            )),
4304            Resolver::Host(b) => {
4305                if let Some(host) = b.get_float(prop) {
4306                    Ok(sync_f64_f32(&host))
4307                } else if let Some(host) = b.get_int(prop) {
4308                    Ok(sync_i64_f32(&host))
4309                } else if b.get_memo_float(prop).is_some() || b.get_memo_int(prop).is_some() {
4310                    Err(SeqError::new(
4311                        file,
4312                        line,
4313                        1,
4314                        format!("'{what}' z memo jest tylko do odczytu — podepnij prop Float/Int"),
4315                    ))
4316                } else {
4317                    Err(prop_err(file, line, prop, "Float/Int"))
4318                }
4319            }
4320        },
4321        SeqValue::Env(name) => match res {
4322            Resolver::Static => Err(SeqError::new(
4323                file,
4324                line,
4325                1,
4326                format!("'{what}' from environment requires host"),
4327            )),
4328            Resolver::Host(b) => {
4329                if let Some(host) = b.get_env_float(name) {
4330                    Ok(sync_f64_f32(&host))
4331                } else if let Some(host) = b.get_env_int(name) {
4332                    Ok(sync_i64_f32(&host))
4333                } else {
4334                    Err(SeqError::new(
4335                        file,
4336                        line,
4337                        1,
4338                        format!("unknown env.{name} or wrong type (here you need: Float/Int)"),
4339                    ))
4340                }
4341            }
4342        },
4343        SeqValue::ItemField(field) => match res {
4344            Resolver::Static => Err(SeqError::new(
4345                file,
4346                line,
4347                1,
4348                format!("'{what}' z wiersza wymaga hosta"),
4349            )),
4350            Resolver::Host(_) => match row_value(file, line, row, field)? {
4351                RowVal::Sig(FieldSignal::Float(host)) => Ok(sync_f64_f32(&host)),
4352                RowVal::Sig(FieldSignal::Int(host)) => Ok(sync_i64_f32(&host)),
4353                _ => Err(SeqError::new(
4354                    file,
4355                    line,
4356                    1,
4357                    format!("pole wiersza '{field}' to nie liczba (Float/Int)"),
4358                )),
4359            },
4360        },
4361        _ => Err(SeqError::new(
4362            file,
4363            line,
4364            1,
4365            format!("'{what}' to liczba albo nazwa propertu"),
4366        )),
4367    }
4368}
4369
4370#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
4371fn sync_f64_f32(host: &Signal<f64>) -> Signal<f32> {
4372    let local = Signal::new(host.get_untracked() as f32);
4373    let (h2, l2) = (host.clone(), local.clone());
4374    effect(move || l2.set(h2.get() as f32)).forget();
4375    let (h3, l3) = (host.clone(), local.clone());
4376    effect(move || h3.set(l3.get() as f64)).forget();
4377    local
4378}
4379
4380#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
4381fn sync_i64_f32(host: &Signal<i64>) -> Signal<f32> {
4382    let local = Signal::new(host.get_untracked() as f32);
4383    let (h2, l2) = (host.clone(), local.clone());
4384    effect(move || l2.set(h2.get() as f32)).forget();
4385    let (h3, l3) = (host.clone(), local.clone());
4386    effect(move || h3.set(l3.get() as i64)).forget();
4387    local
4388}
4389
4390#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
4391fn live_f32(
4392    file: &str,
4393    line: usize,
4394    res: &Resolver<'_>,
4395    row: Option<&SeqRowData>,
4396    value: &SeqValue,
4397    what: &str,
4398) -> Result<Signal<f32>, SeqError> {
4399    match value {
4400        SeqValue::Num(n) => Ok(Signal::new(*n)),
4401        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
4402            Resolver::Static => Err(SeqError::new(
4403                file,
4404                line,
4405                1,
4406                format!("'{what}' z propertu wymaga hosta"),
4407            )),
4408            Resolver::Host(b) => {
4409                if let Some(host) = b.get_float(prop) {
4410                    let local = Signal::new(host.get_untracked() as f32);
4411                    let l2 = local.clone();
4412                    effect(move || l2.set(host.get() as f32)).forget();
4413                    Ok(local)
4414                } else if let Some(host) = b.get_int(prop) {
4415                    let local = Signal::new(host.get_untracked() as f32);
4416                    let l2 = local.clone();
4417                    effect(move || l2.set(host.get() as f32)).forget();
4418                    Ok(local)
4419                } else if let Some(m) = b.get_memo_float(prop) {
4420                    let local = Signal::new(m.get_untracked() as f32);
4421                    let l2 = local.clone();
4422                    effect(move || l2.set(m.get() as f32)).forget();
4423                    Ok(local)
4424                } else if let Some(m) = b.get_memo_int(prop) {
4425                    let local = Signal::new(m.get_untracked() as f32);
4426                    let l2 = local.clone();
4427                    effect(move || l2.set(m.get() as f32)).forget();
4428                    Ok(local)
4429                } else {
4430                    Err(prop_err(file, line, prop, "Float/Int"))
4431                }
4432            }
4433        },
4434        SeqValue::Env(name) => match res {
4435            Resolver::Static => Err(SeqError::new(
4436                file,
4437                line,
4438                1,
4439                format!("'{what}' from environment requires host"),
4440            )),
4441            Resolver::Host(b) => {
4442                if let Some(host) = b.get_env_float(name) {
4443                    let local = Signal::new(host.get_untracked() as f32);
4444                    let l2 = local.clone();
4445                    effect(move || l2.set(host.get() as f32)).forget();
4446                    Ok(local)
4447                } else if let Some(host) = b.get_env_int(name) {
4448                    let local = Signal::new(host.get_untracked() as f32);
4449                    let l2 = local.clone();
4450                    effect(move || l2.set(host.get() as f32)).forget();
4451                    Ok(local)
4452                } else {
4453                    Err(SeqError::new(
4454                        file,
4455                        line,
4456                        1,
4457                        format!("unknown env.{name} or wrong type (here you need: Float/Int)"),
4458                    ))
4459                }
4460            }
4461        },
4462        SeqValue::ItemField(field) => match res {
4463            Resolver::Static => Err(SeqError::new(
4464                file,
4465                line,
4466                1,
4467                format!("'{what}' z wiersza wymaga hosta"),
4468            )),
4469            Resolver::Host(_) => match row_value(file, line, row, field)? {
4470                RowVal::Sig(FieldSignal::Float(host)) => {
4471                    let local = Signal::new(host.get_untracked() as f32);
4472                    let l2 = local.clone();
4473                    effect(move || l2.set(host.get() as f32)).forget();
4474                    Ok(local)
4475                }
4476                RowVal::Sig(FieldSignal::Int(host)) => {
4477                    let local = Signal::new(host.get_untracked() as f32);
4478                    let l2 = local.clone();
4479                    effect(move || l2.set(host.get() as f32)).forget();
4480                    Ok(local)
4481                }
4482                _ => Err(SeqError::new(
4483                    file,
4484                    line,
4485                    1,
4486                    format!("pole wiersza '{field}' to nie liczba (Float/Int)"),
4487                )),
4488            },
4489        },
4490        _ => Err(SeqError::new(
4491            file,
4492            line,
4493            1,
4494            format!("'{what}' to liczba albo nazwa propertu"),
4495        )),
4496    }
4497}
4498
4499#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
4500fn bridge_usize(
4501    file: &str,
4502    line: usize,
4503    res: &Resolver<'_>,
4504    row: Option<&SeqRowData>,
4505    value: &SeqValue,
4506    what: &str,
4507) -> Result<Signal<usize>, SeqError> {
4508    match value {
4509        SeqValue::Num(n) => {
4510            if n.is_finite() && *n >= 0.0 {
4511                Ok(Signal::new(*n as usize))
4512            } else {
4513                Err(SeqError::new(
4514                    file,
4515                    line,
4516                    1,
4517                    format!("'{what}' to liczba >= 0"),
4518                ))
4519            }
4520        }
4521        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
4522            Resolver::Static => Err(SeqError::new(
4523                file,
4524                line,
4525                1,
4526                format!("'{what}' z propertu wymaga hosta"),
4527            )),
4528            Resolver::Host(b) => {
4529                if let Some(host) = b.get_int(prop) {
4530                    let local = Signal::new(host.get_untracked().max(0) as usize);
4531                    let (h2, l2) = (host.clone(), local.clone());
4532                    effect(move || l2.set(h2.get().max(0) as usize)).forget();
4533                    let (h3, l3) = (host.clone(), local.clone());
4534                    effect(move || h3.set(l3.get() as i64)).forget();
4535                    Ok(local)
4536                } else if b.get_memo_int(prop).is_some() {
4537                    Err(SeqError::new(
4538                        file,
4539                        line,
4540                        1,
4541                        format!("'{what}' z memo jest tylko do odczytu — podepnij prop Int"),
4542                    ))
4543                } else {
4544                    Err(prop_err(file, line, prop, "Int"))
4545                }
4546            }
4547        },
4548        SeqValue::Env(name) => match res {
4549            Resolver::Static => Err(SeqError::new(
4550                file,
4551                line,
4552                1,
4553                format!("'{what}' from environment requires host"),
4554            )),
4555            Resolver::Host(b) => {
4556                if let Some(host) = b.get_env_int(name) {
4557                    let local = Signal::new(host.get_untracked().max(0) as usize);
4558                    let (h2, l2) = (host.clone(), local.clone());
4559                    effect(move || l2.set(h2.get().max(0) as usize)).forget();
4560                    let (h3, l3) = (host.clone(), local.clone());
4561                    effect(move || h3.set(l3.get() as i64)).forget();
4562                    Ok(local)
4563                } else {
4564                    Err(SeqError::new(
4565                        file,
4566                        line,
4567                        1,
4568                        format!("unknown env.{name} or wrong type (here you need: Int)"),
4569                    ))
4570                }
4571            }
4572        },
4573        SeqValue::ItemField(field) => match res {
4574            Resolver::Static => Err(SeqError::new(
4575                file,
4576                line,
4577                1,
4578                format!("'{what}' z wiersza wymaga hosta"),
4579            )),
4580            Resolver::Host(_) => match row_value(file, line, row, field)? {
4581                RowVal::Sig(FieldSignal::Int(host)) => {
4582                    let local = Signal::new(host.get_untracked().max(0) as usize);
4583                    let (h2, l2) = (host.clone(), local.clone());
4584                    effect(move || l2.set(h2.get().max(0) as usize)).forget();
4585                    let (h3, l3) = (host.clone(), local.clone());
4586                    effect(move || h3.set(l3.get() as i64)).forget();
4587                    Ok(local)
4588                }
4589                _ => Err(SeqError::new(
4590                    file,
4591                    line,
4592                    1,
4593                    format!("pole wiersza '{field}' to nie Int"),
4594                )),
4595            },
4596        },
4597        _ => Err(SeqError::new(
4598            file,
4599            line,
4600            1,
4601            format!("'{what}' to liczba albo nazwa propertu"),
4602        )),
4603    }
4604}
4605
4606#[derive(Clone)]
4607enum LiveVal {
4608    Bool(Signal<bool>),
4609    Str(Signal<String>),
4610    Usize(Signal<usize>),
4611    F32(Signal<f32>),
4612}
4613
4614impl LiveVal {
4615    #[allow(clippy::cast_possible_truncation)]
4616    fn current(&self) -> Value {
4617        match self {
4618            Self::Bool(s) => Value::Bool(s.get_untracked()),
4619            Self::Str(s) => Value::Str(Rc::new(s.get_untracked())),
4620            Self::Usize(s) => Value::Int(s.get_untracked() as i64),
4621            Self::F32(s) => Value::Float(f64::from(s.get_untracked())),
4622        }
4623    }
4624}
4625
4626#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
4627fn wire_live_action(
4628    file: &str,
4629    line: usize,
4630    b: &HostBindings,
4631    value: &SeqValue,
4632    live: &LiveVal,
4633    what: &str,
4634) -> Result<Rc<dyn Fn()>, SeqError> {
4635    if let SeqValue::CallSeq { name, args } = value {
4636        if args.is_empty() {
4637            return seq_fire(file, line, b, name, Vec::new());
4638        }
4639        if args.len() == 1 && matches!(&args[0], SeqValue::Ident(n) if n == "value") {
4640            return seq_fire_live(file, line, b, name, live);
4641        }
4642        return Err(SeqError::new(
4643            file,
4644            line,
4645            1,
4646            format!("'{what}' to {{ fn() }} albo {{ fn(value) }}"),
4647        ));
4648    }
4649    let SeqValue::Callback { action, args } = value else {
4650        return Err(SeqError::new(
4651            file,
4652            line,
4653            1,
4654            format!("'{what}': {{ akcja() }}"),
4655        ));
4656    };
4657    if args.is_empty() {
4658        return b.get_action0(action).ok_or_else(|| {
4659            SeqError::new(
4660                file,
4661                line,
4662                1,
4663                format!("unknown callback '{action}' (missing on host)"),
4664            )
4665        });
4666    }
4667    if args.len() == 1 && matches!(&args[0], SeqValue::Ident(name) if name == "value") {
4668        return match live {
4669            LiveVal::Bool(sig) => {
4670                let f = b.get_action_bool(action).ok_or_else(|| {
4671                    SeqError::new(
4672                        file,
4673                        line,
4674                        1,
4675                        format!("callback '{action}' called from Bool and the host does not have it (on_bool)"),
4676                    )
4677                })?;
4678                let sig = sig.clone();
4679                Ok(Rc::new(move || f(sig.get())))
4680            }
4681            LiveVal::Str(sig) => {
4682                let f = b.get_action_str(action).ok_or_else(|| {
4683                    SeqError::new(
4684                        file,
4685                        line,
4686                        1,
4687                        format!("callback '{action}' called with text, but the host does not have it (on_str)"),
4688                    )
4689                })?;
4690                let sig = sig.clone();
4691                Ok(Rc::new(move || f(sig.get())))
4692            }
4693            LiveVal::Usize(sig) => {
4694                let f = b.get_action_int(action).ok_or_else(|| {
4695                    SeqError::new(
4696                        file,
4697                        line,
4698                        1,
4699                        format!(
4700                            "callback '{action}' called from Int and host doesn't have it (on_int)"
4701                        ),
4702                    )
4703                })?;
4704                let sig = sig.clone();
4705                Ok(Rc::new(move || f(sig.get_untracked() as i64)))
4706            }
4707            LiveVal::F32(sig) => {
4708                let f = b.get_action_float(action).ok_or_else(|| {
4709                    SeqError::new(
4710                        file,
4711                        line,
4712                        1,
4713                        format!("callback '{action}' called from Float and host does not have it (on_float)"),
4714                    )
4715                })?;
4716                let sig = sig.clone();
4717                Ok(Rc::new(move || f(f64::from(sig.get()))))
4718            }
4719        };
4720    }
4721    Err(SeqError::new(
4722        file,
4723        line,
4724        1,
4725        format!("'{what}' to {{ akcja() }} albo {{ akcja(value) }}"),
4726    ))
4727}
4728
4729fn display_of(
4730    file: &str,
4731    line: usize,
4732    b: &HostBindings,
4733    name: &str,
4734) -> Result<Rc<dyn Fn() -> String>, SeqError> {
4735    if let Some(sig) = b.get_str(name) {
4736        Ok(Rc::new(move || sig.get()))
4737    } else if let Some(m) = b.get_memo_str(name) {
4738        Ok(Rc::new(move || m.get()))
4739    } else if let Some(sig) = b.get_bool(name) {
4740        Ok(Rc::new(move || sig.get().to_string()))
4741    } else if let Some(m) = b.get_memo_bool(name) {
4742        Ok(Rc::new(move || m.get().to_string()))
4743    } else if let Some(sig) = b.get_int(name) {
4744        Ok(Rc::new(move || sig.get().to_string()))
4745    } else if let Some(m) = b.get_memo_int(name) {
4746        Ok(Rc::new(move || m.get().to_string()))
4747    } else if let Some(sig) = b.get_float(name) {
4748        Ok(Rc::new(move || sig.get().to_string()))
4749    } else if let Some(m) = b.get_memo_float(name) {
4750        Ok(Rc::new(move || m.get().to_string()))
4751    } else if let Some(sig) = b.get_env_str(name) {
4752        Ok(Rc::new(move || sig.get()))
4753    } else if let Some(sig) = b.get_env_bool(name) {
4754        Ok(Rc::new(move || sig.get().to_string()))
4755    } else if let Some(sig) = b.get_env_int(name) {
4756        Ok(Rc::new(move || sig.get().to_string()))
4757    } else if let Some(sig) = b.get_env_float(name) {
4758        Ok(Rc::new(move || sig.get().to_string()))
4759    } else {
4760        Err(SeqError::new(
4761            file,
4762            line,
4763            1,
4764            format!("unknown prop '{name}' (missing on host)"),
4765        ))
4766    }
4767}
4768
4769fn display_env_of(
4770    file: &str,
4771    line: usize,
4772    b: &HostBindings,
4773    name: &str,
4774) -> Result<Rc<dyn Fn() -> String>, SeqError> {
4775    if let Some(sig) = b.get_env_str(name) {
4776        Ok(Rc::new(move || sig.get()))
4777    } else if let Some(sig) = b.get_env_bool(name) {
4778        Ok(Rc::new(move || sig.get().to_string()))
4779    } else if let Some(sig) = b.get_env_int(name) {
4780        Ok(Rc::new(move || sig.get().to_string()))
4781    } else if let Some(sig) = b.get_env_float(name) {
4782        Ok(Rc::new(move || sig.get().to_string()))
4783    } else {
4784        Err(SeqError::new(
4785            file,
4786            line,
4787            1,
4788            format!("unknown env.{name} (missing on host: HostBindings::env_..)"),
4789        ))
4790    }
4791}
4792
4793fn display_field(
4794    file: &str,
4795    line: usize,
4796    row: Option<&SeqRowData>,
4797    field: &str,
4798) -> Result<Rc<dyn Fn() -> String>, SeqError> {
4799    match row_value(file, line, row, field)? {
4800        RowVal::Sig(FieldSignal::Str(sig)) => Ok(Rc::new(move || sig.get())),
4801        RowVal::Sig(FieldSignal::Bool(sig)) => Ok(Rc::new(move || sig.get().to_string())),
4802        RowVal::Sig(FieldSignal::Int(sig)) => Ok(Rc::new(move || sig.get().to_string())),
4803        RowVal::Sig(FieldSignal::Float(sig)) => Ok(Rc::new(move || sig.get().to_string())),
4804        RowVal::Id(id) => Ok(Rc::new(move || id.to_string())),
4805    }
4806}
4807
4808enum ArgVal {
4809    Int(i64),
4810    Float(f64),
4811    Str(String),
4812    Bool(bool),
4813}
4814
4815impl ArgVal {
4816    fn to_value(&self) -> Value {
4817        match self {
4818            Self::Int(i) => Value::Int(*i),
4819            Self::Float(f) => Value::Float(*f),
4820            Self::Str(s) => Value::Str(Rc::new(s.clone())),
4821            Self::Bool(b) => Value::Bool(*b),
4822        }
4823    }
4824}
4825
4826fn arg_val(
4827    file: &str,
4828    line: usize,
4829    arg: &SeqValue,
4830    row: Option<&SeqRowData>,
4831) -> Result<ArgVal, SeqError> {
4832    match arg {
4833        SeqValue::Num(n) => {
4834            if n.fract() == 0.0 {
4835                #[allow(clippy::cast_possible_truncation)]
4836                Ok(ArgVal::Int(*n as i64))
4837            } else {
4838                Ok(ArgVal::Float(f64::from(*n)))
4839            }
4840        }
4841        SeqValue::Str(s) => Ok(ArgVal::Str(s.clone())),
4842        SeqValue::Bool(x) => Ok(ArgVal::Bool(*x)),
4843        SeqValue::ItemField(field) => match row_value(file, line, row, field)? {
4844            RowVal::Id(id) => Ok(ArgVal::Int(id)),
4845            RowVal::Sig(FieldSignal::Int(sig)) => Ok(ArgVal::Int(sig.get_untracked())),
4846            RowVal::Sig(FieldSignal::Float(sig)) => Ok(ArgVal::Float(sig.get_untracked())),
4847            RowVal::Sig(FieldSignal::Str(sig)) => Ok(ArgVal::Str(sig.get_untracked())),
4848            RowVal::Sig(FieldSignal::Bool(sig)) => Ok(ArgVal::Bool(sig.get_untracked())),
4849        },
4850        _ => Err(SeqError::new(
4851            file,
4852            line,
4853            1,
4854            "argument to liczba/tekst/Bool/value/item.pole".to_string(),
4855        )),
4856    }
4857}
4858
4859fn seq_target(
4860    file: &str,
4861    line: usize,
4862    b: &HostBindings,
4863    name: &str,
4864) -> Result<(Rc<crate::eval::SeqRuntime>, HostBindings, String), SeqError> {
4865    let rt = b.get_seq().ok_or_else(|| {
4866        SeqError::new(
4867            file,
4868            line,
4869            1,
4870            format!("funkcja '{name}' wymaga hosta (lower_with z runtime .seq)"),
4871        )
4872    })?;
4873    if !rt.has_fn(name) {
4874        return Err(SeqError::new(
4875            file,
4876            line,
4877            1,
4878            format!("nieznana funkcja '{name}'"),
4879        ));
4880    }
4881    Ok((rt, b.clone(), name.to_string()))
4882}
4883
4884fn seq_fire(
4885    file: &str,
4886    line: usize,
4887    b: &HostBindings,
4888    name: &str,
4889    const_args: Vec<Value>,
4890) -> Result<Rc<dyn Fn()>, SeqError> {
4891    let (rt, bb, n) = seq_target(file, line, b, name)?;
4892    Ok(Rc::new(move || {
4893        if let Err(e) = rt.call(&bb, &n, const_args.clone()) {
4894            eprintln!("seq: {e}");
4895        }
4896    }))
4897}
4898
4899fn seq_fire_live(
4900    file: &str,
4901    line: usize,
4902    b: &HostBindings,
4903    name: &str,
4904    live: &LiveVal,
4905) -> Result<Rc<dyn Fn()>, SeqError> {
4906    let (rt, bb, n) = seq_target(file, line, b, name)?;
4907    let live = live.clone();
4908    Ok(Rc::new(move || {
4909        let v = live.current();
4910        if let Err(e) = rt.call(&bb, &n, vec![v]) {
4911            eprintln!("seq: {e}");
4912        }
4913    }))
4914}
4915
4916fn seq_fire_pt(
4917    file: &str,
4918    line: usize,
4919    b: &HostBindings,
4920    name: &str,
4921) -> Result<Rc<dyn Fn(f64, f64)>, SeqError> {
4922    let (rt, bb, n) = seq_target(file, line, b, name)?;
4923    Ok(Rc::new(move |x, y| {
4924        if let Err(e) = rt.call(&bb, &n, vec![Value::Float(x), Value::Float(y)]) {
4925            eprintln!("seq: {e}");
4926        }
4927    }))
4928}
4929
4930fn seq_fire_name(
4931    file: &str,
4932    line: usize,
4933    b: &HostBindings,
4934    name: &str,
4935) -> Result<Rc<dyn Fn(f64, f64, String)>, SeqError> {
4936    let (rt, bb, n) = seq_target(file, line, b, name)?;
4937    Ok(Rc::new(move |_x, _y, hit: String| {
4938        if let Err(e) = rt.call(&bb, &n, vec![Value::Str(Rc::new(hit))]) {
4939            eprintln!("seq: {e}");
4940        }
4941    }))
4942}
4943
4944fn seq_fire_pt_named(
4945    file: &str,
4946    line: usize,
4947    b: &HostBindings,
4948    name: &str,
4949) -> Result<Rc<dyn Fn(f64, f64, String)>, SeqError> {
4950    let (rt, bb, n) = seq_target(file, line, b, name)?;
4951    Ok(Rc::new(move |x, y, hit: String| {
4952        if let Err(e) = rt.call(
4953            &bb,
4954            &n,
4955            vec![Value::Float(x), Value::Float(y), Value::Str(Rc::new(hit))],
4956        ) {
4957            eprintln!("seq: {e}");
4958        }
4959    }))
4960}
4961
4962fn seq_fire_wheel(
4963    file: &str,
4964    line: usize,
4965    b: &HostBindings,
4966    name: &str,
4967) -> Result<crate::bindings::WheelAction, SeqError> {
4968    let (rt, bb, n) = seq_target(file, line, b, name)?;
4969    Ok(Rc::new(move |x, y, d| {
4970        if let Err(e) = rt.call(
4971            &bb,
4972            &n,
4973            vec![Value::Float(x), Value::Float(y), Value::Float(d)],
4974        ) {
4975            eprintln!("seq: {e}");
4976        }
4977    }))
4978}
4979
4980fn wire_action(
4981    file: &str,
4982    line: usize,
4983    b: &HostBindings,
4984    value: &SeqValue,
4985    row: Option<&SeqRowData>,
4986) -> Result<Rc<dyn Fn()>, SeqError> {
4987    if let SeqValue::CallSeq { name, args } = value {
4988        if args.len() > 1 {
4989            return Err(SeqError::new(
4990                file,
4991                line,
4992                1,
4993                format!("function '{name}' with 2+ arguments - later"),
4994            ));
4995        }
4996        if args.is_empty() {
4997            return seq_fire(file, line, b, name, Vec::new());
4998        }
4999        let arg = arg_val(file, line, &args[0], row)?;
5000        return seq_fire(file, line, b, name, vec![arg.to_value()]);
5001    }
5002    let SeqValue::Callback { action, args } = value else {
5003        return Err(SeqError::new(file, line, 1, "{ akcja(args) }"));
5004    };
5005    if args.len() > 1 {
5006        return Err(SeqError::new(
5007            file,
5008            line,
5009            1,
5010            format!("callback '{action}' with 2+ arguments - later"),
5011        ));
5012    }
5013    if args.is_empty() {
5014        let f = b.get_action0(action).ok_or_else(|| {
5015            SeqError::new(
5016                file,
5017                line,
5018                1,
5019                format!("unknown callback '{action}' (missing on host)"),
5020            )
5021        })?;
5022        return Ok(Rc::new(move || f()));
5023    }
5024    let arg = arg_val(file, line, &args[0], row)?;
5025    match arg {
5026        ArgVal::Int(i) => {
5027            if let Some(f) = b.get_action_int(action) {
5028                return Ok(Rc::new(move || f(i)));
5029            }
5030
5031            #[allow(clippy::cast_precision_loss)]
5032            let x = i as f64;
5033            if let Some(f) = b.get_action_float(action) {
5034                return Ok(Rc::new(move || f(x)));
5035            }
5036            Err(SeqError::new(
5037                file,
5038                line,
5039                1,
5040                format!(
5041                    "callback '{action}' called from Int and host doesn't have it (on_int/on_float)"
5042                ),
5043            ))
5044        }
5045        ArgVal::Float(x) => {
5046            let f = b.get_action_float(action).ok_or_else(|| {
5047                SeqError::new(
5048                    file,
5049                    line,
5050                    1,
5051                    format!(
5052                        "callback '{action}' called from Float and host does not have it (on_float)"
5053                    ),
5054                )
5055            })?;
5056            Ok(Rc::new(move || f(x)))
5057        }
5058        ArgVal::Str(s) => {
5059            let f = b.get_action_str(action).ok_or_else(|| {
5060                SeqError::new(
5061                    file,
5062                    line,
5063                    1,
5064                    format!("callback '{action}' called with text, but the host does not have it (on_str)"),
5065                )
5066            })?;
5067            Ok(Rc::new(move || f(s.clone())))
5068        }
5069        ArgVal::Bool(x) => {
5070            let f = b.get_action_bool(action).ok_or_else(|| {
5071                SeqError::new(
5072                    file,
5073                    line,
5074                    1,
5075                    format!("callback '{action}' called from Bool and the host does not have it (on_bool)"),
5076                )
5077            })?;
5078            Ok(Rc::new(move || f(x)))
5079        }
5080    }
5081}
5082
5083fn lower_list(
5084    file: &str,
5085    theme: &Theme,
5086    styles: &BuiltStyles,
5087    node: &SeqNode,
5088    res: &Resolver<'_>,
5089) -> Result<AnyView, SeqError> {
5090    let items = find_arg(node, "items").ok_or_else(|| {
5091        SeqError::new(
5092            file,
5093            node.line,
5094            1,
5095            "List wymaga items: nazwa_propertu".to_string(),
5096        )
5097    })?;
5098    let items_prop = match &items.value {
5099        SeqValue::Ident(name) => name.clone(),
5100        _ => {
5101            return Err(SeqError::new(
5102                file,
5103                items.line,
5104                1,
5105                "items: to nazwa propertu z hosta (items: warstwy)",
5106            ));
5107        }
5108    };
5109    if let Some(key) = find_arg(node, "key") {
5110        match &key.value {
5111            SeqValue::Ident(name) if name == "id" => {}
5112            _ => {
5113                return Err(SeqError::new(
5114                    file,
5115                    key.line,
5116                    1,
5117                    "key: tylko id w inkrementcie 3",
5118                ));
5119            }
5120        }
5121    }
5122    let bindings = match res {
5123        Resolver::Static => {
5124            return Err(SeqError::new(
5125                file,
5126                node.line,
5127                1,
5128                "List(items:) wymaga hosta Rust (HostBindings::rows())",
5129            ));
5130        }
5131        Resolver::Host(b) => (*b).clone(),
5132    };
5133    let get_rows = bindings.get_rows(&items_prop).ok_or_else(|| {
5134        SeqError::new(
5135            file,
5136            items.line,
5137            1,
5138            format!("nieznany prop '{items_prop}' (HostBindings::rows())"),
5139        )
5140    })?;
5141    let template = node.children.clone();
5142    let file_owned = file.to_string();
5143    let theme_owned = *theme;
5144    let styles_owned = styles.clone();
5145    let list = sequoia_view::List::new(move || get_rows())
5146        .key(|row: &SeqRowData| {
5147            #[allow(clippy::cast_sign_loss)]
5148            let _ = row.id as u64;
5149            row.id as u64
5150        })
5151        .item(move |row: &SeqRowData| {
5152            let res = Resolver::Host(&bindings);
5153            match lower_row_template(
5154                &file_owned,
5155                &theme_owned,
5156                &styles_owned,
5157                &res,
5158                row,
5159                &template,
5160            ) {
5161                Ok(v) => v,
5162                Err(e) => AnyView::new(Text::new(format!("seq: {e}"))),
5163            }
5164        });
5165    Ok(AnyView::new(list))
5166}
5167
5168#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
5169fn lower_virtual_list(
5170    file: &str,
5171    theme: &Theme,
5172    styles: &BuiltStyles,
5173    node: &SeqNode,
5174    res: &Resolver<'_>,
5175    row: Option<&SeqRowData>,
5176) -> Result<AnyView, SeqError> {
5177    let items = find_arg(node, "items").ok_or_else(|| {
5178        SeqError::new(
5179            file,
5180            node.line,
5181            1,
5182            "VirtualList wymaga items: nazwa_propertu".to_string(),
5183        )
5184    })?;
5185    let items_prop = match &items.value {
5186        SeqValue::Ident(name) => name.clone(),
5187        _ => {
5188            return Err(SeqError::new(
5189                file,
5190                items.line,
5191                1,
5192                "items: to nazwa propertu z hosta (items: warstwy)",
5193            ));
5194        }
5195    };
5196    if let Some(key) = find_arg(node, "key") {
5197        match &key.value {
5198            SeqValue::Ident(name) if name == "id" => {}
5199            _ => {
5200                return Err(SeqError::new(
5201                    file,
5202                    key.line,
5203                    1,
5204                    "key: tylko id w inkrementcie 3",
5205                ));
5206            }
5207        }
5208    }
5209    let mut item_height: Option<f32> = None;
5210    if let Some(arg) = find_arg(node, "item_height") {
5211        let h = num(file, arg, res, row)?;
5212        if !h.is_finite() || h <= 0.0 {
5213            return Err(SeqError::new(
5214                file,
5215                arg.line,
5216                1,
5217                "'item_height' to dodatnia liczba",
5218            ));
5219        }
5220        item_height = Some(h);
5221    }
5222
5223    let mut overscan: Option<usize> = None;
5224    if let Some(arg) = find_arg(node, "overscan") {
5225        let n = num(file, arg, res, row)?;
5226        if !n.is_finite() || n < 0.0 {
5227            return Err(SeqError::new(
5228                file,
5229                arg.line,
5230                1,
5231                "'overscan' to liczba >= 0",
5232            ));
5233        }
5234        overscan = Some(n as usize);
5235    }
5236    let bindings = match res {
5237        Resolver::Static => {
5238            return Err(SeqError::new(
5239                file,
5240                node.line,
5241                1,
5242                "VirtualList(items:) wymaga hosta Rust (HostBindings::rows())",
5243            ));
5244        }
5245        Resolver::Host(b) => (*b).clone(),
5246    };
5247    let get_rows = bindings.get_rows(&items_prop).ok_or_else(|| {
5248        SeqError::new(
5249            file,
5250            items.line,
5251            1,
5252            format!("nieznany prop '{items_prop}' (HostBindings::rows())"),
5253        )
5254    })?;
5255    let template = node.children.clone();
5256    let file_owned = file.to_string();
5257    let theme_owned = *theme;
5258    let styles_owned = styles.clone();
5259    let mut list = VirtualList::new(move || get_rows())
5260        .key(|row: &SeqRowData| {
5261            #[allow(clippy::cast_sign_loss)]
5262            let _ = row.id as u64;
5263            row.id as u64
5264        })
5265        .item(move |row: &SeqRowData| {
5266            let res = Resolver::Host(&bindings);
5267            match lower_row_template(
5268                &file_owned,
5269                &theme_owned,
5270                &styles_owned,
5271                &res,
5272                row,
5273                &template,
5274            ) {
5275                Ok(v) => v,
5276                Err(e) => AnyView::new(Text::new(format!("seq: {e}"))),
5277            }
5278        });
5279    if let Some(h) = item_height {
5280        list = list.item_height(h);
5281    }
5282    if let Some(n) = overscan {
5283        list = list.overscan(n);
5284    }
5285    Ok(AnyView::new(list))
5286}
5287
5288fn lower_row_template(
5289    file: &str,
5290    theme: &Theme,
5291    styles: &BuiltStyles,
5292    res: &Resolver<'_>,
5293    row: &SeqRowData,
5294    template: &[SeqNode],
5295) -> Result<AnyView, SeqError> {
5296    match template {
5297        [] => Ok(AnyView::new(Column::new())),
5298        [single] => lower_node(file, theme, styles, single, res, Some(row)),
5299        _ => {
5300            let mut col = Column::new();
5301            for n in template {
5302                col = col.child(lower_node(file, theme, styles, n, res, Some(row))?);
5303            }
5304            Ok(AnyView::new(col))
5305        }
5306    }
5307}
5308
5309#[derive(Clone)]
5310enum NumSrc {
5311    Lit(f32),
5312    Dyn(Rc<dyn Fn() -> f32>),
5313}
5314
5315impl NumSrc {
5316    fn val(&self) -> f32 {
5317        match self {
5318            Self::Lit(v) => *v,
5319            Self::Dyn(f) => f(),
5320        }
5321    }
5322
5323    fn touch(&self) {
5324        if let Self::Dyn(f) = self {
5325            f();
5326        }
5327    }
5328
5329    fn touch_rc(&self) -> Rc<dyn Fn()> {
5330        let this = self.clone();
5331        Rc::new(move || this.touch())
5332    }
5333}
5334
5335#[derive(Clone)]
5336enum BoolSrc {
5337    Lit(bool),
5338    Dyn(Rc<dyn Fn() -> bool>),
5339}
5340
5341impl BoolSrc {
5342    fn val(&self) -> bool {
5343        match self {
5344            Self::Lit(v) => *v,
5345            Self::Dyn(f) => f(),
5346        }
5347    }
5348
5349    fn touch(&self) {
5350        if let Self::Dyn(f) = self {
5351            f();
5352        }
5353    }
5354
5355    fn touch_rc(&self) -> Rc<dyn Fn()> {
5356        let this = self.clone();
5357        Rc::new(move || this.touch())
5358    }
5359}
5360
5361#[derive(Clone)]
5362struct PtSrc {
5363    x: NumSrc,
5364    y: NumSrc,
5365}
5366
5367impl PtSrc {
5368    fn val(&self) -> Point {
5369        Point::new(self.x.val(), self.y.val())
5370    }
5371
5372    fn touch(&self) {
5373        self.x.touch();
5374        self.y.touch();
5375    }
5376
5377    fn touch_rc(&self) -> Rc<dyn Fn()> {
5378        let this = self.clone();
5379        Rc::new(move || this.touch())
5380    }
5381}
5382
5383#[derive(Clone)]
5384struct RectSrc {
5385    x: NumSrc,
5386    y: NumSrc,
5387    w: NumSrc,
5388    h: NumSrc,
5389}
5390
5391impl RectSrc {
5392    fn val(&self) -> Rect {
5393        Rect::from_xywh(self.x.val(), self.y.val(), self.w.val(), self.h.val())
5394    }
5395
5396    fn touch(&self) {
5397        self.x.touch();
5398        self.y.touch();
5399        self.w.touch();
5400        self.h.touch();
5401    }
5402
5403    fn touch_rc(&self) -> Rc<dyn Fn()> {
5404        let this = self.clone();
5405        Rc::new(move || this.touch())
5406    }
5407}
5408
5409#[derive(Clone)]
5410struct ShapeDraw {
5411    draw: Rc<dyn Fn(&mut Painter)>,
5412    touch: Rc<dyn Fn()>,
5413    hits: Vec<HitTest>,
5414}
5415
5416type HitTest = (String, Rc<dyn Fn(Point) -> bool>);
5417
5418fn dist_seg(p: Point, a: Point, b: Point) -> f32 {
5419    let abx = b.x - a.x;
5420    let aby = b.y - a.y;
5421    let len2 = abx * abx + aby * aby;
5422    if len2 <= f32::EPSILON {
5423        return ((p.x - a.x).powi(2) + (p.y - a.y).powi(2)).sqrt();
5424    }
5425    let t = (((p.x - a.x) * abx + (p.y - a.y) * aby) / len2).clamp(0.0, 1.0);
5426    let dx = p.x - (a.x + abx * t);
5427    let dy = p.y - (a.y + aby * t);
5428    (dx * dx + dy * dy).sqrt()
5429}
5430
5431fn point_in_poly(p: Point, pts: &[Point]) -> bool {
5432    let mut inside = false;
5433    let n = pts.len();
5434    let mut j = n.saturating_sub(1);
5435    for i in 0..n {
5436        let a = pts[i];
5437        let b = pts[j];
5438        if (a.y > p.y) != (b.y > p.y) && p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x {
5439            inside = !inside;
5440        }
5441        j = i;
5442    }
5443    inside
5444}
5445
5446fn cubic(p0: Point, c0: Point, c1: Point, p1: Point, t: f32) -> Point {
5447    let u = 1.0 - t;
5448    Point::new(
5449        u * u * u * p0.x + 3.0 * u * u * t * c0.x + 3.0 * u * t * t * c1.x + t * t * t * p1.x,
5450        u * u * u * p0.y + 3.0 * u * u * t * c0.y + 3.0 * u * t * t * c1.y + t * t * t * p1.y,
5451    )
5452}
5453
5454fn dyn_f32(
5455    file: &str,
5456    line: usize,
5457    res: &Resolver<'_>,
5458    row: Option<&SeqRowData>,
5459    value: &SeqValue,
5460    what: &str,
5461) -> Result<NumSrc, SeqError> {
5462    match value {
5463        SeqValue::Num(n) => {
5464            if n.is_finite() {
5465                Ok(NumSrc::Lit(*n))
5466            } else {
5467                Err(SeqError::new(
5468                    file,
5469                    line,
5470                    1,
5471                    format!("'{what}' must be finished"),
5472                ))
5473            }
5474        }
5475        SeqValue::Ident(prop) => match res {
5476            Resolver::Static => Err(SeqError::new(
5477                file,
5478                line,
5479                1,
5480                format!("'{what}' z propertu wymaga hosta — inkrement 2"),
5481            )),
5482            Resolver::Host(b) => {
5483                if let Some(cell) = b.get_tick_angle(prop) {
5484                    return Ok(NumSrc::Dyn(Rc::new(move || cell.get())));
5485                }
5486                if let Some(sig) = b.get_float(prop) {
5487                    Ok(NumSrc::Dyn(Rc::new(move || sig.get() as f32)))
5488                } else if let Some(sig) = b.get_int(prop) {
5489                    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
5490                    let _ = ();
5491                    Ok(NumSrc::Dyn(Rc::new(move || sig.get() as f32)))
5492                } else if let Some(m) = b.get_memo_float(prop) {
5493                    Ok(NumSrc::Dyn(Rc::new(move || m.get() as f32)))
5494                } else if let Some(m) = b.get_memo_int(prop) {
5495                    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
5496                    let _ = ();
5497                    Ok(NumSrc::Dyn(Rc::new(move || m.get() as f32)))
5498                } else {
5499                    Err(prop_err(file, line, prop, "Float/Int"))
5500                }
5501            }
5502        },
5503        SeqValue::ItemField(field) => match res {
5504            Resolver::Static => Err(SeqError::new(
5505                file,
5506                line,
5507                1,
5508                format!("'{what}' z wiersza wymaga hosta"),
5509            )),
5510            Resolver::Host(_) => match row_value(file, line, row, field)? {
5511                RowVal::Sig(FieldSignal::Int(sig)) => {
5512                    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
5513                    let _ = ();
5514                    Ok(NumSrc::Dyn(Rc::new(move || sig.get() as f32)))
5515                }
5516                RowVal::Sig(FieldSignal::Float(sig)) => {
5517                    Ok(NumSrc::Dyn(Rc::new(move || sig.get() as f32)))
5518                }
5519                RowVal::Id(id) => {
5520                    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
5521                    let _ = ();
5522                    Ok(NumSrc::Lit(id as f32))
5523                }
5524                _ => Err(SeqError::new(
5525                    file,
5526                    line,
5527                    1,
5528                    format!("pole wiersza '{field}' to nie liczba (Int/Float/id)"),
5529                )),
5530            },
5531        },
5532        SeqValue::Dynamic(name) => match res {
5533            Resolver::Static => Err(SeqError::new(
5534                file,
5535                line,
5536                1,
5537                format!("dynamic({name}) wymaga hosta Rust"),
5538            )),
5539            Resolver::Host(b) => {
5540                if let Some(cell) = b.get_tick_angle(name) {
5541                    return Ok(NumSrc::Dyn(Rc::new(move || cell.get())));
5542                }
5543                let sig = b.get_float(name).or_else(|| None);
5544                match sig {
5545                    Some(sig) => Ok(NumSrc::Dyn(Rc::new(move || sig.get() as f32))),
5546                    None => match b.get_int(name) {
5547                        Some(sig) => {
5548                            #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
5549                            let _ = ();
5550                            Ok(NumSrc::Dyn(Rc::new(move || sig.get() as f32)))
5551                        }
5552                        None => match b.get_memo_float(name) {
5553                            Some(m) => Ok(NumSrc::Dyn(Rc::new(move || m.get() as f32))),
5554                            None => match b.get_memo_int(name) {
5555                                Some(m) => {
5556                                    #[allow(
5557                                        clippy::cast_possible_truncation,
5558                                        clippy::cast_precision_loss
5559                                    )]
5560                                    let _ = ();
5561                                    Ok(NumSrc::Dyn(Rc::new(move || m.get() as f32)))
5562                                }
5563                                None => Err(prop_err(file, line, name, "Float/Int")),
5564                            },
5565                        },
5566                    },
5567                }
5568            }
5569        },
5570        SeqValue::Env(name) => match res {
5571            Resolver::Static => Err(SeqError::new(
5572                file,
5573                line,
5574                1,
5575                format!("env.{name} wymaga hosta Rust"),
5576            )),
5577            Resolver::Host(b) => {
5578                if let Some(sig) = b.get_env_float(name) {
5579                    Ok(NumSrc::Dyn(Rc::new(move || sig.get() as f32)))
5580                } else if let Some(sig) = b.get_env_int(name) {
5581                    #[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
5582                    let _ = ();
5583                    Ok(NumSrc::Dyn(Rc::new(move || sig.get() as f32)))
5584                } else {
5585                    Err(SeqError::new(
5586                        file,
5587                        line,
5588                        1,
5589                        format!("unknown env.{name} or wrong type (here you need: Float/Int)"),
5590                    ))
5591                }
5592            }
5593        },
5594        _ => Err(SeqError::new(
5595            file,
5596            line,
5597            1,
5598            format!("'{what}' to liczba albo dynamic(nazwa)"),
5599        )),
5600    }
5601}
5602
5603fn dyn_bool(
5604    file: &str,
5605    line: usize,
5606    res: &Resolver<'_>,
5607    row: Option<&SeqRowData>,
5608    value: &SeqValue,
5609    what: &str,
5610) -> Result<BoolSrc, SeqError> {
5611    match value {
5612        SeqValue::Bool(b) => Ok(BoolSrc::Lit(*b)),
5613        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
5614            Resolver::Static => Err(SeqError::new(
5615                file,
5616                line,
5617                1,
5618                format!("'{what}' z propertu wymaga hosta"),
5619            )),
5620            Resolver::Host(b) => b
5621                .get_bool(prop)
5622                .map(|sig| BoolSrc::Dyn(Rc::new(move || sig.get())))
5623                .or_else(|| {
5624                    b.get_memo_bool(prop)
5625                        .map(|m| BoolSrc::Dyn(Rc::new(move || m.get())))
5626                })
5627                .ok_or_else(|| prop_err(file, line, prop, "Bool")),
5628        },
5629        SeqValue::Env(name) => match res {
5630            Resolver::Static => Err(SeqError::new(
5631                file,
5632                line,
5633                1,
5634                format!("'{what}' from environment requires host"),
5635            )),
5636            Resolver::Host(b) => b
5637                .get_env_bool(name)
5638                .map(|sig| BoolSrc::Dyn(Rc::new(move || sig.get())))
5639                .ok_or_else(|| {
5640                    SeqError::new(
5641                        file,
5642                        line,
5643                        1,
5644                        format!("unknown env.{name} or wrong type (here you need: Bool)"),
5645                    )
5646                }),
5647        },
5648        SeqValue::ItemField(field) => match res {
5649            Resolver::Static => Err(SeqError::new(
5650                file,
5651                line,
5652                1,
5653                format!("'{what}' z wiersza wymaga hosta"),
5654            )),
5655            Resolver::Host(_) => match row_value(file, line, row, field)? {
5656                RowVal::Sig(FieldSignal::Bool(sig)) => Ok(BoolSrc::Dyn(Rc::new(move || sig.get()))),
5657                _ => Err(SeqError::new(
5658                    file,
5659                    line,
5660                    1,
5661                    format!("pole wiersza '{field}' to nie Bool"),
5662                )),
5663            },
5664        },
5665        _ => Err(SeqError::new(
5666            file,
5667            line,
5668            1,
5669            format!("'{what}' to true/false albo dynamic(nazwa)"),
5670        )),
5671    }
5672}
5673
5674fn pt_src(
5675    file: &str,
5676    line: usize,
5677    res: &Resolver<'_>,
5678    row: Option<&SeqRowData>,
5679    value: &SeqValue,
5680    what: &str,
5681) -> Result<PtSrc, SeqError> {
5682    match value {
5683        SeqValue::Tuple(items) if items.len() == 2 => Ok(PtSrc {
5684            x: dyn_f32(file, line, res, row, &items[0], what)?,
5685            y: dyn_f32(file, line, res, row, &items[1], what)?,
5686        }),
5687        _ => Err(SeqError::new(
5688            file,
5689            line,
5690            1,
5691            format!("'{what}' to krotka (x, y)"),
5692        )),
5693    }
5694}
5695
5696fn rect_src(
5697    file: &str,
5698    line: usize,
5699    res: &Resolver<'_>,
5700    row: Option<&SeqRowData>,
5701    value: &SeqValue,
5702    what: &str,
5703) -> Result<RectSrc, SeqError> {
5704    match value {
5705        SeqValue::Tuple(items) if items.len() == 4 => Ok(RectSrc {
5706            x: dyn_f32(file, line, res, row, &items[0], what)?,
5707            y: dyn_f32(file, line, res, row, &items[1], what)?,
5708            w: dyn_f32(file, line, res, row, &items[2], what)?,
5709            h: dyn_f32(file, line, res, row, &items[3], what)?,
5710        }),
5711        _ => Err(SeqError::new(
5712            file,
5713            line,
5714            1,
5715            format!("'{what}' to krotka (x, y, w, h)"),
5716        )),
5717    }
5718}
5719
5720fn points_src(
5721    file: &str,
5722    line: usize,
5723    res: &Resolver<'_>,
5724    row: Option<&SeqRowData>,
5725    value: &SeqValue,
5726    what: &str,
5727) -> Result<Vec<PtSrc>, SeqError> {
5728    match value {
5729        SeqValue::Array(items) if !items.is_empty() => items
5730            .iter()
5731            .map(|it| pt_src(file, line, res, row, it, what))
5732            .collect(),
5733        _ => Err(SeqError::new(
5734            file,
5735            line,
5736            1,
5737            format!("'{what}' to [(x, y), ...] (min. 1 punkt)"),
5738        )),
5739    }
5740}
5741
5742fn uv_src_of(
5743    file: &str,
5744    res: &Resolver<'_>,
5745    row: Option<&SeqRowData>,
5746    node: &SeqNode,
5747) -> Result<[NumSrc; 4], SeqError> {
5748    match find_arg(node, "uv") {
5749        None => Ok([
5750            NumSrc::Lit(0.0),
5751            NumSrc::Lit(0.0),
5752            NumSrc::Lit(1.0),
5753            NumSrc::Lit(1.0),
5754        ]),
5755        Some(arg) => match &arg.value {
5756            SeqValue::Tuple(items) if items.len() == 4 => Ok([
5757                dyn_f32(file, arg.line, res, row, &items[0], "uv")?,
5758                dyn_f32(file, arg.line, res, row, &items[1], "uv")?,
5759                dyn_f32(file, arg.line, res, row, &items[2], "uv")?,
5760                dyn_f32(file, arg.line, res, row, &items[3], "uv")?,
5761            ]),
5762            _ => Err(SeqError::new(
5763                file,
5764                arg.line,
5765                1,
5766                "'uv' to krotka (u0, v0, u1, v1)",
5767            )),
5768        },
5769    }
5770}
5771
5772fn shape_color(file: &str, theme: &Theme, node: &SeqNode) -> Result<Color, SeqError> {
5773    for name in ["fill", "color"] {
5774        if let Some(arg) = find_arg(node, name) {
5775            return color(file, theme, arg);
5776        }
5777    }
5778    Err(SeqError::new(
5779        file,
5780        node.line,
5781        1,
5782        format!("{} wymaga fill: albo color:", node.kind),
5783    ))
5784}
5785
5786fn shape_visible(
5787    file: &str,
5788    res: &Resolver<'_>,
5789    row: Option<&SeqRowData>,
5790    node: &SeqNode,
5791) -> Result<BoolSrc, SeqError> {
5792    match find_arg(node, "visible") {
5793        None => Ok(BoolSrc::Lit(true)),
5794        Some(arg) => dyn_bool(file, arg.line, res, row, &arg.value, "visible"),
5795    }
5796}
5797
5798fn shape_name(
5799    file: &str,
5800    node: &SeqNode,
5801    res: &Resolver<'_>,
5802    row: Option<&SeqRowData>,
5803) -> Result<Option<String>, SeqError> {
5804    match find_arg(node, "name") {
5805        None => Ok(None),
5806        Some(arg) => match &arg.value {
5807            SeqValue::Str(s) => Ok(Some(s.clone())),
5808            SeqValue::ItemField(field) => match res {
5809                Resolver::Static => Err(SeqError::new(
5810                    file,
5811                    arg.line,
5812                    1,
5813                    "name: z wiersza wymaga hosta",
5814                )),
5815                Resolver::Host(_) => match row_value(file, arg.line, row, field)? {
5816                    RowVal::Id(id) => Ok(Some(id.to_string())),
5817                    RowVal::Sig(FieldSignal::Str(sig)) => Ok(Some(sig.get_untracked())),
5818                    RowVal::Sig(FieldSignal::Int(sig)) => Ok(Some(sig.get_untracked().to_string())),
5819                    RowVal::Sig(FieldSignal::Float(sig)) => {
5820                        Ok(Some(sig.get_untracked().to_string()))
5821                    }
5822                    RowVal::Sig(FieldSignal::Bool(sig)) => {
5823                        Ok(Some(sig.get_untracked().to_string()))
5824                    }
5825                },
5826            },
5827            _ => Err(SeqError::new(
5828                file,
5829                arg.line,
5830                1,
5831                "name: is a string in quotation marks or item.field",
5832            )),
5833        },
5834    }
5835}
5836
5837fn shape_num(
5838    file: &str,
5839    res: &Resolver<'_>,
5840    row: Option<&SeqRowData>,
5841    node: &SeqNode,
5842    name: &str,
5843) -> Result<NumSrc, SeqError> {
5844    match find_arg(node, name) {
5845        None => Err(SeqError::new(
5846            file,
5847            node.line,
5848            1,
5849            format!("{} wymaga {name}:", node.kind),
5850        )),
5851        Some(arg) => dyn_f32(file, arg.line, res, row, &arg.value, name),
5852    }
5853}
5854
5855fn shape_pt(
5856    file: &str,
5857    res: &Resolver<'_>,
5858    row: Option<&SeqRowData>,
5859    node: &SeqNode,
5860    name: &str,
5861) -> Result<PtSrc, SeqError> {
5862    match find_arg(node, name) {
5863        None => Err(SeqError::new(
5864            file,
5865            node.line,
5866            1,
5867            format!("{} wymaga {name}: (x, y)", node.kind),
5868        )),
5869        Some(arg) => pt_src(file, arg.line, res, row, &arg.value, name),
5870    }
5871}
5872
5873fn rect_src_of(
5874    file: &str,
5875    res: &Resolver<'_>,
5876    row: Option<&SeqRowData>,
5877    node: &SeqNode,
5878) -> Result<RectSrc, SeqError> {
5879    if let Some(arg) = find_arg(node, "rect") {
5880        return rect_src(file, arg.line, res, row, &arg.value, "rect");
5881    }
5882    Ok(RectSrc {
5883        x: shape_num(file, res, row, node, "x")?,
5884        y: shape_num(file, res, row, node, "y")?,
5885        w: shape_num(file, res, row, node, "w")?,
5886        h: shape_num(file, res, row, node, "h")?,
5887    })
5888}
5889
5890fn opt_num(
5891    file: &str,
5892    res: &Resolver<'_>,
5893    row: Option<&SeqRowData>,
5894    node: &SeqNode,
5895    name: &str,
5896) -> Result<NumSrc, SeqError> {
5897    match find_arg(node, name) {
5898        None => Ok(NumSrc::Lit(0.0)),
5899        Some(arg) => dyn_f32(file, arg.line, res, row, &arg.value, name),
5900    }
5901}
5902
5903fn opt_pt(
5904    file: &str,
5905    res: &Resolver<'_>,
5906    row: Option<&SeqRowData>,
5907    node: &SeqNode,
5908    name: &str,
5909) -> Result<Option<PtSrc>, SeqError> {
5910    match find_arg(node, name) {
5911        None => Ok(None),
5912        Some(arg) => pt_src(file, arg.line, res, row, &arg.value, name).map(Some),
5913    }
5914}
5915
5916fn shape_str(file: &str, node: &SeqNode, name: &str) -> Result<String, SeqError> {
5917    match find_arg(node, name) {
5918        Some(arg) => match &arg.value {
5919            SeqValue::Str(s) => Ok(s.clone()),
5920            _ => Err(SeqError::new(
5921                file,
5922                arg.line,
5923                1,
5924                format!("{} wymaga {name}: \"...\"", node.kind),
5925            )),
5926        },
5927        None => Err(SeqError::new(
5928            file,
5929            node.line,
5930            1,
5931            format!("{} wymaga {name}: \"...\"", node.kind),
5932        )),
5933    }
5934}
5935
5936fn points_src_of(
5937    file: &str,
5938    res: &Resolver<'_>,
5939    row: Option<&SeqRowData>,
5940    node: &SeqNode,
5941) -> Result<Vec<PtSrc>, SeqError> {
5942    match find_arg(node, "points") {
5943        None => Err(SeqError::new(
5944            file,
5945            node.line,
5946            1,
5947            format!("{} wymaga points: [(x, y), ...]", node.kind),
5948        )),
5949        Some(arg) => points_src(file, arg.line, res, row, &arg.value, "points"),
5950    }
5951}
5952fn touches(parts: &[Rc<dyn Fn()>]) -> Rc<dyn Fn()> {
5953    let parts = parts.to_vec();
5954    Rc::new(move || {
5955        for p in &parts {
5956            p();
5957        }
5958    })
5959}
5960
5961trait Touch {
5962    fn touch_rc(&self) -> Rc<dyn Fn()>;
5963}
5964
5965impl Touch for NumSrc {
5966    fn touch_rc(&self) -> Rc<dyn Fn()> {
5967        NumSrc::touch_rc(self)
5968    }
5969}
5970
5971impl Touch for PtSrc {
5972    fn touch_rc(&self) -> Rc<dyn Fn()> {
5973        PtSrc::touch_rc(self)
5974    }
5975}
5976
5977impl Touch for RectSrc {
5978    fn touch_rc(&self) -> Rc<dyn Fn()> {
5979        RectSrc::touch_rc(self)
5980    }
5981}
5982
5983impl Touch for Vec<PtSrc> {
5984    fn touch_rc(&self) -> Rc<dyn Fn()> {
5985        let this = self.clone();
5986        Rc::new(move || {
5987            for p in &this {
5988                p.touch();
5989            }
5990        })
5991    }
5992}
5993
5994impl Touch for Rc<dyn Fn()> {
5995    fn touch_rc(&self) -> Rc<dyn Fn()> {
5996        self.clone()
5997    }
5998}
5999
6000fn touch2(a: &impl Touch, b: &impl Touch) -> Rc<dyn Fn()> {
6001    touches(&[a.touch_rc(), b.touch_rc()])
6002}
6003
6004fn touch3(a: &impl Touch, b: &impl Touch, c: &impl Touch) -> Rc<dyn Fn()> {
6005    touches(&[a.touch_rc(), b.touch_rc(), c.touch_rc()])
6006}
6007
6008fn touch4(a: &impl Touch, b: &impl Touch, c: &impl Touch, d: &impl Touch) -> Rc<dyn Fn()> {
6009    touches(&[a.touch_rc(), b.touch_rc(), c.touch_rc(), d.touch_rc()])
6010}
6011
6012fn touch4e(a: &impl Touch, b: &impl Touch, c: &impl Touch, d: &impl Touch) -> Rc<dyn Fn()> {
6013    touch4(a, b, c, d)
6014}
6015
6016#[allow(clippy::too_many_arguments)]
6017fn touch5(
6018    a: &impl Touch,
6019    b: &impl Touch,
6020    c: &impl Touch,
6021    d: &impl Touch,
6022    e: &impl Touch,
6023) -> Rc<dyn Fn()> {
6024    touches(&[
6025        a.touch_rc(),
6026        b.touch_rc(),
6027        c.touch_rc(),
6028        d.touch_rc(),
6029        e.touch_rc(),
6030    ])
6031}
6032
6033fn touch_pts(pts: &Vec<PtSrc>, vis: &Rc<dyn Fn()>) -> Rc<dyn Fn()> {
6034    touches(&[pts.touch_rc(), vis.clone()])
6035}
6036
6037fn touch_pts_w(pts: &Vec<PtSrc>, w: &NumSrc, vis: &Rc<dyn Fn()>) -> Rc<dyn Fn()> {
6038    touches(&[pts.touch_rc(), w.touch_rc(), vis.clone()])
6039}
6040
6041fn only_color(file: &str, theme: &Theme, node: &SeqNode) -> Result<Color, SeqError> {
6042    shape_color(file, theme, node)
6043}
6044
6045fn dist_polyline(p: Point, pts: &[Point]) -> f32 {
6046    if pts.is_empty() {
6047        return f32::INFINITY;
6048    }
6049    if pts.len() == 1 {
6050        let dx = p.x - pts[0].x;
6051        let dy = p.y - pts[0].y;
6052        return (dx * dx + dy * dy).sqrt();
6053    }
6054    pts.windows(2)
6055        .map(|w| dist_seg(p, w[0], w[1]))
6056        .fold(f32::INFINITY, f32::min)
6057}
6058
6059#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
6060fn sample_cubic(pts: &[Point]) -> Vec<Point> {
6061    const N: u32 = 16;
6062    let (p0, c0, c1, p1) = (pts[0], pts[1], pts[2], pts[3]);
6063    (0..=N)
6064        .map(|i| cubic(p0, c0, c1, p1, i as f32 / N as f32))
6065        .collect()
6066}
6067
6068#[derive(Clone)]
6069enum GestureWire {
6070    Null(Rc<dyn Fn()>),
6071
6072    Pt(Rc<dyn Fn(f64, f64)>),
6073
6074    PtNamed(Rc<dyn Fn(f64, f64, String)>),
6075
6076    Wheel(crate::bindings::WheelAction),
6077}
6078
6079fn fire_gesture(wire: &GestureWire, x: f64, y: f64, hit: Option<&str>) {
6080    match wire {
6081        GestureWire::Null(f) => f(),
6082        GestureWire::Pt(f) => f(x, y),
6083        GestureWire::PtNamed(f) => f(x, y, hit.unwrap_or("").to_string()),
6084        GestureWire::Wheel(f) => f(x, y, 0.0),
6085    }
6086}
6087
6088fn wire_gesture(
6089    file: &str,
6090    line: usize,
6091    b: &HostBindings,
6092    value: &SeqValue,
6093) -> Result<GestureWire, SeqError> {
6094    let (action, args) = match value {
6095        SeqValue::Callback { action, args } => (action.as_str(), args.as_slice()),
6096        SeqValue::CallSeq { name, args } => {
6097            return match args.len() {
6098                0 => seq_fire(file, line, b, name, Vec::new()).map(GestureWire::Null),
6099                1 => seq_fire_name(file, line, b, name).map(GestureWire::PtNamed),
6100                2 => seq_fire_pt(file, line, b, name).map(GestureWire::Pt),
6101                3 => {
6102                    let third = match &args[2] {
6103                        SeqValue::Ident(n) => n.as_str(),
6104                        _ => {
6105                            return Err(SeqError::new(
6106                                file,
6107                                line,
6108                                1,
6109                                format!("'{name}': trzeci argument gestu to `name` albo `d`"),
6110                            ));
6111                        }
6112                    };
6113                    if third == "d" {
6114                        seq_fire_wheel(file, line, b, name).map(GestureWire::Wheel)
6115                    } else {
6116                        seq_fire_pt_named(file, line, b, name).map(GestureWire::PtNamed)
6117                    }
6118                }
6119                _ => Err(SeqError::new(
6120                    file,
6121                    line,
6122                    1,
6123                    format!("'{name}': gest to fn(), fn(name) albo fn(x, y[, name|d])"),
6124                )),
6125            };
6126        }
6127        _ => {
6128            return Err(SeqError::new(
6129                file,
6130                line,
6131                1,
6132                "gest: { akcja(x, y) } albo { fn(x, y) }",
6133            ));
6134        }
6135    };
6136    let ident_args = args
6137        .iter()
6138        .map(|a| match a {
6139            SeqValue::Ident(name) => Ok(name.clone()),
6140            _ => Err(SeqError::new(
6141                file,
6142                line,
6143                1,
6144                format!("'{action}': argumenty gestu to nazwy (x, y[, name])"),
6145            )),
6146        })
6147        .collect::<Result<Vec<_>, _>>()?;
6148    match ident_args.len() {
6149        0 => b
6150            .get_action0(action)
6151            .map(GestureWire::Null)
6152            .ok_or_else(|| unknown_gesture(file, line, action)),
6153        2 => b
6154            .get_action_point(action)
6155            .map(GestureWire::Pt)
6156            .ok_or_else(|| unknown_gesture(file, line, action)),
6157        3 => {
6158            if let Some(f) = b.get_action_point_named(action) {
6159                Ok(GestureWire::PtNamed(f))
6160            } else if let Some(f) = b.get_action_wheel(action) {
6161                Ok(GestureWire::Wheel(f))
6162            } else {
6163                Err(unknown_gesture(file, line, action))
6164            }
6165        }
6166        _ => Err(SeqError::new(
6167            file,
6168            line,
6169            1,
6170            format!("'{action}': gest to 0, 2 albo 3 argumenty (x, y[, name|d])"),
6171        )),
6172    }
6173}
6174
6175fn unknown_gesture(file: &str, line: usize, action: &str) -> SeqError {
6176    SeqError::new(
6177        file,
6178        line,
6179        1,
6180        format!(
6181            "unknown callback '{action}' (missing on host: on/on_point/on_point_named/on_wheel)"
6182        ),
6183    )
6184}
6185
6186fn canvas_cursor(file: &str, line: usize, name: &str) -> Result<CursorIcon, SeqError> {
6187    match name {
6188        "default" => Ok(CursorIcon::Default),
6189        "pointer" => Ok(CursorIcon::Pointer),
6190        "crosshair" => Ok(CursorIcon::Crosshair),
6191        "grab" => Ok(CursorIcon::Grab),
6192        "grabbing" => Ok(CursorIcon::Grabbing),
6193        "text" => Ok(CursorIcon::Text),
6194        other => Err(SeqError::new(
6195            file,
6196            line,
6197            1,
6198            format!("cursor to default/pointer/crosshair/grab/grabbing/text, nie '{other}'"),
6199        )),
6200    }
6201}
6202
6203fn lower_split(
6204    file: &str,
6205    theme: &Theme,
6206    styles: &BuiltStyles,
6207    node: &SeqNode,
6208    res: &Resolver<'_>,
6209    row: Option<&SeqRowData>,
6210) -> Result<AnyView, SeqError> {
6211    let value_arg = find_arg(node, "value").ok_or_else(|| {
6212        SeqError::new(
6213            file,
6214            node.line,
6215            1,
6216            "SplitPanel wymaga value: prop".to_string(),
6217        )
6218    })?;
6219    let sig = bridge_f32(file, value_arg.line, res, row, &value_arg.value, "value")?;
6220    let mut v = SplitPanel::new(sig);
6221    for arg in &node.args {
6222        match arg.name.as_str() {
6223            "value" => {}
6224            "axis" => {
6225                v = match ident(file, arg)?.as_str() {
6226                    "horizontal" => v.horizontal(),
6227                    "vertical" => v.vertical(),
6228                    other => {
6229                        return Err(SeqError::new(
6230                            file,
6231                            arg.line,
6232                            1,
6233                            format!("axis to horizontal/vertical, nie '{other}'"),
6234                        ));
6235                    }
6236                };
6237            }
6238            "handle" => v = v.handle_width(num(file, arg, res, row)?),
6239            "min_first" => v = v.min_first(num(file, arg, res, row)?),
6240            "min_second" => v = v.min_second(num(file, arg, res, row)?),
6241            "handle_color" => v = v.handle_color(color(file, theme, arg)?),
6242            "width" => v = v.width(len(file, arg, res, row)?),
6243            "height" => v = v.height(len(file, arg, res, row)?),
6244            "padding" => match padding_val(file, arg, res, row)? {
6245                PaddingVal::All(n) => v = v.padding(n),
6246                PaddingVal::Xy(x, y) => v = v.padding_xy(x, y),
6247            },
6248            "padding_x" => v = v.padding_x(num(file, arg, res, row)?),
6249            "padding_y" => v = v.padding_y(num(file, arg, res, row)?),
6250            "background" => v = v.background(color(file, theme, arg)?),
6251            "flex" => v = v.flex(num(file, arg, res, row)?),
6252            "min_width" => v = v.min_width(num(file, arg, res, row)?),
6253            "max_width" => v = v.max_width(num(file, arg, res, row)?),
6254            "min_height" => v = v.min_height(num(file, arg, res, row)?),
6255            "max_height" => v = v.max_height(num(file, arg, res, row)?),
6256            "margin" => match margin_val(file, arg, res, row)? {
6257                MarginVal::All(n) => v = v.margin(n),
6258                MarginVal::Xy(x, y) => v = v.margin_xy(x, y),
6259            },
6260            "margin_x" => v = v.margin_x(num(file, arg, res, row)?),
6261            "margin_y" => v = v.margin_y(num(file, arg, res, row)?),
6262            "margin_left" => v = v.margin_left(num(file, arg, res, row)?),
6263            "margin_right" => v = v.margin_right(num(file, arg, res, row)?),
6264            "margin_top" => v = v.margin_top(num(file, arg, res, row)?),
6265            "margin_bottom" => v = v.margin_bottom(num(file, arg, res, row)?),
6266            "overflow" => v = v.overflow(overflow_val(file, arg)?),
6267            "hit" => v = v.hit_testable(flag(file, arg, res, row)?),
6268            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
6269            | "indent_bottom" => {}
6270            _ => unreachable_checked(file, node, arg)?,
6271        }
6272    }
6273    if node.children.len() != 2 {
6274        return Err(SeqError::new(
6275            file,
6276            node.line,
6277            1,
6278            "SplitPanel requires exactly 2 children: first { } and second { }",
6279        ));
6280    }
6281
6282    for c in &node.children {
6283        let _ = child_flex_weight(file, res, row, c)?;
6284    }
6285    let first = lower_node(file, theme, styles, &node.children[0], res, row)?;
6286    let second = lower_node(file, theme, styles, &node.children[1], res, row)?;
6287    v = v.first(first);
6288    v = v.second(second);
6289    Ok(AnyView::new(v))
6290}
6291
6292const DRAG_THRESHOLD_SQ: f32 = 64.0;
6293
6294#[allow(clippy::too_many_lines)]
6295fn lower_canvas(
6296    file: &str,
6297    theme: &Theme,
6298    node: &SeqNode,
6299    res: &Resolver<'_>,
6300    row: Option<&SeqRowData>,
6301) -> Result<AnyView, SeqError> {
6302    let mut v = Canvas::new();
6303    let mut click: Option<GestureWire> = None;
6304    let mut drag: Option<GestureWire> = None;
6305    let mut wheel: Option<crate::bindings::WheelAction> = None;
6306    let mut pinch: Option<crate::bindings::PinchAction> = None;
6307    let mut press: Option<GestureWire> = None;
6308
6309    let tick_name: Option<String> = match find_arg(node, "tick") {
6310        None => {
6311            if find_arg(node, "running").is_some() || find_arg(node, "speed").is_some() {
6312                return Err(SeqError::new(
6313                    file,
6314                    node.line,
6315                    1,
6316                    "running:/speed: require tick: name".to_string(),
6317                ));
6318            }
6319            None
6320        }
6321        Some(arg) => match &arg.value {
6322            SeqValue::Ident(name) => Some(name.clone()),
6323            _ => {
6324                return Err(SeqError::new(
6325                    file,
6326                    arg.line,
6327                    1,
6328                    "tick: to nazwa (tick: kat)".to_string(),
6329                ));
6330            }
6331        },
6332    };
6333    let mut owned_bindings: Option<HostBindings> = None;
6334    let mut tick_state: Option<TickState> = None;
6335    if let Some(name) = tick_name {
6336        match res {
6337            Resolver::Static => {
6338                return Err(SeqError::new(
6339                    file,
6340                    node.line,
6341                    1,
6342                    "tick: wymaga hosta Rust".to_string(),
6343                ));
6344            }
6345            Resolver::Host(b) => {
6346                let angle = Rc::new(Cell::new(0.0f32));
6347                let mut cloned = (*b).clone();
6348                cloned = cloned.tick_angle(&name, angle.clone());
6349                let running = match find_arg(node, "running") {
6350                    None => BoolSrc::Lit(true),
6351                    Some(arg) => dyn_bool(file, arg.line, res, row, &arg.value, "running")?,
6352                };
6353                let speed = match find_arg(node, "speed") {
6354                    None => NumSrc::Lit(45.0),
6355                    Some(arg) => dyn_f32(file, arg.line, res, row, &arg.value, "speed")?,
6356                };
6357                owned_bindings = Some(cloned);
6358                tick_state = Some(TickState {
6359                    angle,
6360                    last: Rc::new(std::cell::RefCell::new(None)),
6361                    running,
6362                    speed,
6363                });
6364            }
6365        }
6366    }
6367
6368    let tick_res;
6369    let res: &Resolver<'_> = match owned_bindings.as_ref() {
6370        Some(b) => {
6371            tick_res = Resolver::Host(b);
6372            &tick_res
6373        }
6374        None => res,
6375    };
6376    for arg in &node.args {
6377        match arg.name.as_str() {
6378            "width" => v = v.width(len(file, arg, res, row)?),
6379            "height" => v = v.height(len(file, arg, res, row)?),
6380            "flex" => v = v.flex(num(file, arg, res, row)?),
6381            "min_width" => v = v.min_width(num(file, arg, res, row)?),
6382            "max_width" => v = v.max_width(num(file, arg, res, row)?),
6383            "min_height" => v = v.min_height(num(file, arg, res, row)?),
6384            "max_height" => v = v.max_height(num(file, arg, res, row)?),
6385            "background" => v = v.background(color(file, theme, arg)?),
6386            "transparent" => {
6387                if flag(file, arg, res, row)? {
6388                    v = v.no_background();
6389                }
6390            }
6391            "cursor" => v = v.cursor(canvas_cursor(file, arg.line, &ident(file, arg)?)?),
6392            "focusable" => v = v.focusable(flag(file, arg, res, row)?),
6393            "grid" => v = v.grid(num(file, arg, res, row)?),
6394            "grid_color" => {
6395                let step = match find_arg(node, "grid") {
6396                    Some(g) => num(file, g, res, row)?,
6397                    None => {
6398                        return Err(SeqError::new(file, arg.line, 1, "grid_color wymaga grid:"));
6399                    }
6400                };
6401                v = v.grid_colored(step, color(file, theme, arg)?);
6402            }
6403            "axes" => {
6404                if flag(file, arg, res, row)? {
6405                    v = v.axes();
6406                }
6407            }
6408            "axes_color" => v = v.axes_colored(color(file, theme, arg)?),
6409            "clip" => v = v.clip(flag(file, arg, res, row)?),
6410            "allow_overflow" => v = v.allow_overflow(num(file, arg, res, row)?),
6411            "camera" => match &arg.value {
6412                SeqValue::Tuple(items) if items.len() == 2 => {
6413                    let pos = pt_src(file, arg.line, res, row, &items[0], "camera.pos")?;
6414                    let zoom = dyn_f32(file, arg.line, res, row, &items[1], "camera.zoom")?;
6415
6416                    match (&pos.x, &pos.y, &zoom) {
6417                        (NumSrc::Lit(x), NumSrc::Lit(y), NumSrc::Lit(z)) => {
6418                            v = v.camera(Vec2 { x: *x, y: *y }, *z);
6419                        }
6420                        _ => {
6421                            return Err(SeqError::new(
6422                                file,
6423                                arg.line,
6424                                1,
6425                                "dynamic camera later (only numbers today)",
6426                            ));
6427                        }
6428                    }
6429                }
6430                _ => {
6431                    return Err(SeqError::new(file, arg.line, 1, "camera: ((x, y), zoom)"));
6432                }
6433            },
6434            "capture" => v = v.capture(flag(file, arg, res, row)?),
6435
6436            "tick" | "speed" | "running" => {}
6437            "on_click_at" | "on_drag" | "on_wheel" | "on_press" | "on_pinch" => match res {
6438                Resolver::Static => {
6439                    return Err(SeqError::new(
6440                        file,
6441                        arg.line,
6442                        1,
6443                        format!("{} wymaga hosta Rust", arg.name),
6444                    ));
6445                }
6446                Resolver::Host(b) => match &arg.value {
6447                    SeqValue::Callback { action, args } => {
6448                        if arg.name == "on_wheel" {
6449                            let idents = args
6450                                .iter()
6451                                .map(|a| match a {
6452                                    SeqValue::Ident(n) => Ok(n.clone()),
6453                                    _ => Err(SeqError::new(
6454                                        file,
6455                                        arg.line,
6456                                        1,
6457                                        "on_wheel: { akcja(x, y, d) }",
6458                                    )),
6459                                })
6460                                .collect::<Result<Vec<_>, _>>()?;
6461                            if idents.len() != 3 {
6462                                return Err(SeqError::new(
6463                                    file,
6464                                    arg.line,
6465                                    1,
6466                                    "on_wheel: { akcja(x, y, d) }",
6467                                ));
6468                            }
6469                            wheel = Some(b.get_action_wheel(action).ok_or_else(|| {
6470                                SeqError::new(
6471                                    file,
6472                                    arg.line,
6473                                    1,
6474                                    format!(
6475                                        "unknown callback '{action}' (missing on host: on_wheel)"
6476                                    ),
6477                                )
6478                            })?);
6479                        } else if arg.name == "on_pinch" {
6480                            let idents = args
6481                                .iter()
6482                                .map(|a| match a {
6483                                    SeqValue::Ident(n) => Ok(n.clone()),
6484                                    _ => Err(SeqError::new(
6485                                        file,
6486                                        arg.line,
6487                                        1,
6488                                        "on_pinch: { akcja(x, y, s) }",
6489                                    )),
6490                                })
6491                                .collect::<Result<Vec<_>, _>>()?;
6492                            if idents.len() != 3 {
6493                                return Err(SeqError::new(
6494                                    file,
6495                                    arg.line,
6496                                    1,
6497                                    "on_pinch: { akcja(x, y, s) }",
6498                                ));
6499                            }
6500                            pinch = Some(b.get_action_pinch(action).ok_or_else(|| {
6501                                SeqError::new(
6502                                    file,
6503                                    arg.line,
6504                                    1,
6505                                    format!(
6506                                        "unknown callback '{action}' (missing on host: on_pinch)"
6507                                    ),
6508                                )
6509                            })?);
6510                        } else {
6511                            let w = wire_gesture(file, arg.line, b, &arg.value)?;
6512                            if arg.name == "on_click_at" {
6513                                click = Some(w);
6514                            } else if arg.name == "on_press" {
6515                                press = Some(w);
6516                            } else {
6517                                drag = Some(w);
6518                            }
6519                        }
6520                    }
6521                    SeqValue::CallSeq { name, args } => {
6522                        if !args.iter().all(|a| matches!(a, SeqValue::Ident(_))) {
6523                            return Err(SeqError::new(
6524                                file,
6525                                arg.line,
6526                                1,
6527                                format!("{}: {{ fn(x, y[, name|d]) }}", arg.name),
6528                            ));
6529                        }
6530                        match arg.name.as_str() {
6531                            "on_wheel" => match args.len() {
6532                                0 => {
6533                                    let f = seq_fire(file, arg.line, b, name, Vec::new())?;
6534                                    wheel = Some(Rc::new(move |_x, _y, _d| f()));
6535                                }
6536                                3 => wheel = Some(seq_fire_wheel(file, arg.line, b, name)?),
6537                                _ => {
6538                                    return Err(SeqError::new(
6539                                        file,
6540                                        arg.line,
6541                                        1,
6542                                        format!("on_wheel: {{ {name}(x, y, d) }}"),
6543                                    ));
6544                                }
6545                            },
6546                            "on_pinch" => match args.len() {
6547                                0 => {
6548                                    let f = seq_fire(file, arg.line, b, name, Vec::new())?;
6549                                    pinch = Some(Rc::new(move |_x, _y, _s| f()));
6550                                }
6551                                3 => pinch = Some(seq_fire_wheel(file, arg.line, b, name)?),
6552                                _ => {
6553                                    return Err(SeqError::new(
6554                                        file,
6555                                        arg.line,
6556                                        1,
6557                                        format!("on_pinch: {{ {name}(x, y, s) }}"),
6558                                    ));
6559                                }
6560                            },
6561                            _ => {
6562                                let w = wire_gesture(file, arg.line, b, &arg.value)?;
6563                                if arg.name == "on_click_at" {
6564                                    click = Some(w);
6565                                } else if arg.name == "on_press" {
6566                                    press = Some(w);
6567                                } else {
6568                                    drag = Some(w);
6569                                }
6570                            }
6571                        }
6572                    }
6573                    _ => {
6574                        return Err(SeqError::new(
6575                            file,
6576                            arg.line,
6577                            1,
6578                            format!("{}: {{ akcja(x, y[, name]) }}", arg.name),
6579                        ));
6580                    }
6581                },
6582            },
6583            "indent" | "indent_x" | "indent_y" | "indent_left" | "indent_right" | "indent_top"
6584            | "indent_bottom" => {}
6585            _ => unreachable_checked(file, node, arg)?,
6586        }
6587    }
6588
6589    let mut layers: Vec<DrawLayer> = Vec::new();
6590    for c in &node.children {
6591        layers.push(lower_layer(file, theme, c, res, row)?);
6592    }
6593    let draw_layers = layers.clone();
6594
6595    let track_tick = tick_state
6596        .as_ref()
6597        .map(|t| touches(&[t.running.touch_rc(), t.speed.touch_rc()]));
6598    v = v.on_draw(move |g: &mut Painter| {
6599        if let Some(t) = tick_state.as_ref() {
6600            let now = std::time::Instant::now();
6601            if t.running.val() {
6602                let prev = t.last.borrow_mut().replace(now);
6603                let dt = prev
6604                    .map(|p| now.duration_since(p).as_secs_f32())
6605                    .unwrap_or(0.0)
6606                    .clamp(0.0, MAX_ANIM_DT);
6607                t.angle.set((t.angle.get() + dt * t.speed.val()) % 360.0);
6608                g.ctx().request_frame();
6609            } else {
6610                *t.last.borrow_mut() = None;
6611            }
6612        }
6613        DrawLayer::draw_all(&draw_layers, g);
6614    });
6615    {
6616        let track_layers = layers.clone();
6617        v = v.track(move || {
6618            DrawLayer::touch_all(&track_layers);
6619            if let Some(touch) = track_tick.as_ref() {
6620                touch();
6621            }
6622        });
6623    }
6624    if click.is_some() || drag.is_some() || wheel.is_some() || press.is_some() || pinch.is_some() {
6625        let pick_layers = layers.clone();
6626        let down: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
6627        let dragged = Rc::new(Cell::new(false));
6628
6629        let down_name: Rc<std::cell::RefCell<Option<String>>> =
6630            Rc::new(std::cell::RefCell::new(None));
6631        v = v.on_pointer(move |at: Point, ev: &PointerEvent, _ctx: &mut EventCtx| {
6632            match ev {
6633                PointerEvent::Down {
6634                    button: PointerButton::Left,
6635                    pos,
6636                    ..
6637                } => {
6638                    down.set(Some((pos.x, pos.y)));
6639                    dragged.set(false);
6640                    let picked = DrawLayer::pick(&pick_layers, *pos);
6641                    *down_name.borrow_mut() = picked.clone();
6642                    if let Some(p) = press.as_ref() {
6643                        fire_gesture(p, f64::from(pos.x), f64::from(pos.y), picked.as_deref());
6644                    }
6645                }
6646                PointerEvent::Move { pos, .. } => {
6647                    if let Some((x0, y0)) = down.get() {
6648                        let dx = pos.x - x0;
6649                        let dy = pos.y - y0;
6650                        if dx * dx + dy * dy > DRAG_THRESHOLD_SQ {
6651                            dragged.set(true);
6652                            if let Some(d) = drag.as_ref() {
6653                                fire_gesture(
6654                                    d,
6655                                    f64::from(pos.x),
6656                                    f64::from(pos.y),
6657                                    down_name.borrow().as_deref(),
6658                                );
6659                            }
6660                        }
6661                    }
6662                }
6663                PointerEvent::Up {
6664                    button: PointerButton::Left,
6665                    pos,
6666                    ..
6667                } => {
6668                    let name = down_name.take();
6669                    if down.take().is_some()
6670                        && !dragged.replace(false)
6671                        && let Some(c) = click.as_ref()
6672                    {
6673                        fire_gesture(c, f64::from(pos.x), f64::from(pos.y), name.as_deref());
6674                    }
6675                }
6676                PointerEvent::Wheel { delta, pos, .. } => {
6677                    if let Some(w) = wheel.as_ref() {
6678                        w(f64::from(pos.x), f64::from(pos.y), f64::from(delta.y));
6679                    }
6680                }
6681                PointerEvent::Pinch { delta, pos, .. } => {
6682                    if let Some(p) = pinch.as_ref() {
6683                        p(f64::from(pos.x), f64::from(pos.y), f64::from(*delta));
6684                    }
6685                }
6686
6687                _ => {}
6688            }
6689            let _ = at;
6690        });
6691    }
6692    Ok(AnyView::new(v))
6693}
6694
6695#[derive(Clone)]
6696enum DrawLayer {
6697    Static(ShapeDraw),
6698    Dyn(DynShapes),
6699    Group {
6700        origin: PtSrc,
6701        rot: NumSrc,
6702        has_rot: bool,
6703        scale: NumSrc,
6704        pivot: Option<PtSrc>,
6705        touch_extra: Rc<dyn Fn()>,
6706        layers: Vec<DrawLayer>,
6707    },
6708    Clip {
6709        rect: RectSrc,
6710        radius: NumSrc,
6711        has_radius: bool,
6712        visible: BoolSrc,
6713        touch_extra: Rc<dyn Fn()>,
6714        layers: Vec<DrawLayer>,
6715    },
6716}
6717
6718type RowTemplate = Rc<dyn Fn(&SeqRowData) -> Vec<DrawLayer>>;
6719
6720#[derive(Clone)]
6721struct DynShapes {
6722    rows: crate::bindings::RowsFn,
6723    render: RowTemplate,
6724}
6725
6726impl DrawLayer {
6727    fn draw_all(layers: &[DrawLayer], g: &mut Painter) {
6728        for layer in layers {
6729            match layer {
6730                DrawLayer::Static(s) => (s.draw)(g),
6731                DrawLayer::Dyn(d) => {
6732                    let mut all = Vec::new();
6733                    for row in (d.rows)() {
6734                        all.extend((d.render)(&row));
6735                    }
6736                    Self::draw_all(&all, g);
6737                }
6738                DrawLayer::Group {
6739                    origin,
6740                    rot,
6741                    has_rot,
6742                    scale,
6743                    pivot,
6744                    layers,
6745                    ..
6746                } => {
6747                    let o = origin.val();
6748
6749                    let mut painter_tr = sequoia_core::Transform::translate(o.x, o.y);
6750                    let z = scale.val();
6751                    if z != 1.0 {
6752                        painter_tr = painter_tr.then(sequoia_core::Transform::scale(z, z));
6753                    }
6754
6755                    let list_local: Option<sequoia_core::Transform> = if *has_rot
6756                        && rot.val() != 0.0
6757                    {
6758                        let base = sequoia_core::Transform::translate(o.x, o.y);
6759                        let rotated = match pivot {
6760                            Some(pv) => {
6761                                sequoia_core::Transform::rotate_around_degrees(rot.val(), pv.val())
6762                                    .then(base)
6763                            }
6764                            None => base.then(sequoia_core::Transform::rotate_degrees(rot.val())),
6765                        };
6766                        Some(if z != 1.0 {
6767                            rotated.then(sequoia_core::Transform::scale(z, z))
6768                        } else {
6769                            rotated
6770                        })
6771                    } else {
6772                        None
6773                    };
6774                    g.with_transform(painter_tr, |g| {
6775                        if let Some(lt) = list_local {
6776                            let og = g.origin_global();
6777                            let global = sequoia_core::Transform::translate(-og.x, -og.y)
6778                                .then(lt)
6779                                .then(sequoia_core::Transform::translate(og.x, og.y));
6780                            g.push_list_transform(global);
6781                            Self::draw_all(layers, g);
6782                            g.pop_list_transform();
6783                        } else {
6784                            Self::draw_all(layers, g);
6785                        }
6786                    });
6787                }
6788                DrawLayer::Clip {
6789                    rect,
6790                    radius,
6791                    has_radius,
6792                    visible,
6793                    layers,
6794                    ..
6795                } => {
6796                    if !visible.val() {
6797                        continue;
6798                    }
6799                    if *has_radius {
6800                        let rr = radius.val();
6801                        g.clipped_rounded(rect.val(), rr, |g| {
6802                            Self::draw_all(layers, g);
6803                        });
6804                    } else {
6805                        g.clipped(rect.val(), |g| {
6806                            Self::draw_all(layers, g);
6807                        });
6808                    }
6809                }
6810            }
6811        }
6812    }
6813
6814    fn touch_all(layers: &[DrawLayer]) {
6815        for layer in layers {
6816            match layer {
6817                DrawLayer::Static(s) => (s.touch)(),
6818                DrawLayer::Dyn(d) => {
6819                    let mut all = Vec::new();
6820                    for row in (d.rows)() {
6821                        all.extend((d.render)(&row));
6822                    }
6823                    Self::touch_all(&all);
6824                }
6825                DrawLayer::Group {
6826                    touch_extra,
6827                    layers,
6828                    ..
6829                }
6830                | DrawLayer::Clip {
6831                    touch_extra,
6832                    layers,
6833                    ..
6834                } => {
6835                    (touch_extra)();
6836                    Self::touch_all(layers);
6837                }
6838            }
6839        }
6840    }
6841
6842    fn pick(layers: &[DrawLayer], p: Point) -> Option<String> {
6843        Self::pick_in(layers, p, 0.0, 0.0, 1.0)
6844    }
6845
6846    fn pick_in(layers: &[DrawLayer], p: Point, ax: f32, ay: f32, az: f32) -> Option<String> {
6847        if az == 0.0 {
6848            return None;
6849        }
6850        let local = Point::new((p.x - ax) / az, (p.y - ay) / az);
6851        for layer in layers.iter().rev() {
6852            match layer {
6853                DrawLayer::Static(s) => {
6854                    for (name, test) in s.hits.iter().rev() {
6855                        if test(local) {
6856                            return Some(name.clone());
6857                        }
6858                    }
6859                }
6860                DrawLayer::Dyn(d) => {
6861                    let rows = (d.rows)();
6862                    let mut all = Vec::new();
6863                    for row in rows.iter().rev() {
6864                        all.extend((d.render)(row));
6865                    }
6866                    if let Some(hit) = Self::pick_in(&all, p, ax, ay, az) {
6867                        return Some(hit);
6868                    }
6869                }
6870                DrawLayer::Group {
6871                    origin,
6872                    scale,
6873                    layers,
6874                    ..
6875                } => {
6876                    let o = origin.val();
6877                    let z = scale.val();
6878                    if let Some(hit) =
6879                        Self::pick_in(layers, p, ax + o.x * az, ay + o.y * az, az * z)
6880                    {
6881                        return Some(hit);
6882                    }
6883                }
6884                DrawLayer::Clip { layers, .. } => {
6885                    if let Some(hit) = Self::pick_in(layers, p, ax, ay, az) {
6886                        return Some(hit);
6887                    }
6888                }
6889            }
6890        }
6891        None
6892    }
6893}
6894
6895fn lower_layer(
6896    file: &str,
6897    theme: &Theme,
6898    node: &SeqNode,
6899    res: &Resolver<'_>,
6900    row: Option<&SeqRowData>,
6901) -> Result<DrawLayer, SeqError> {
6902    match node.kind.as_str() {
6903        "List" => lower_shape_list(file, theme, node, res),
6904        "Group" => lower_group(file, theme, node, res, row),
6905        "Clip" => lower_clip(file, theme, node, res, row),
6906        _ => Ok(DrawLayer::Static(lower_shape(file, theme, node, res, row)?)),
6907    }
6908}
6909
6910fn lower_group(
6911    file: &str,
6912    theme: &Theme,
6913    node: &SeqNode,
6914    res: &Resolver<'_>,
6915    row: Option<&SeqRowData>,
6916) -> Result<DrawLayer, SeqError> {
6917    let origin = opt_pt(file, res, row, node, "translate")?.unwrap_or(PtSrc {
6918        x: NumSrc::Lit(0.0),
6919        y: NumSrc::Lit(0.0),
6920    });
6921    let rot = opt_num(file, res, row, node, "rotate")?;
6922    let has_rot = find_arg(node, "rotate").is_some();
6923    let scale = match find_arg(node, "scale") {
6924        None => NumSrc::Lit(1.0),
6925        Some(arg) => dyn_f32(file, arg.line, res, row, &arg.value, "scale")?,
6926    };
6927
6928    let pivot = opt_pt(file, res, row, node, "pivot")?;
6929    let mut layers = Vec::new();
6930    let mut touch_list = vec![origin.touch_rc(), rot.touch_rc(), scale.touch_rc()];
6931    if let Some(pv) = &pivot {
6932        touch_list.push(pv.touch_rc());
6933    }
6934    for c in &node.children {
6935        layers.push(lower_layer(file, theme, c, res, row)?);
6936    }
6937    Ok(DrawLayer::Group {
6938        origin,
6939        rot,
6940        has_rot,
6941        scale,
6942        pivot,
6943        touch_extra: touches(&touch_list),
6944        layers,
6945    })
6946}
6947fn lower_shape_list(
6948    file: &str,
6949    theme: &Theme,
6950    node: &SeqNode,
6951    res: &Resolver<'_>,
6952) -> Result<DrawLayer, SeqError> {
6953    let items = find_arg(node, "items").ok_or_else(|| {
6954        SeqError::new(
6955            file,
6956            node.line,
6957            1,
6958            "List wymaga items: nazwa_propertu".to_string(),
6959        )
6960    })?;
6961    let items_prop = match &items.value {
6962        SeqValue::Ident(name) => name.clone(),
6963        _ => {
6964            return Err(SeqError::new(
6965                file,
6966                items.line,
6967                1,
6968                "items: to nazwa propertu z hosta (items: wezly)",
6969            ));
6970        }
6971    };
6972    if let Some(key) = find_arg(node, "key") {
6973        match &key.value {
6974            SeqValue::Ident(name) if name == "id" => {}
6975            _ => {
6976                return Err(SeqError::new(
6977                    file,
6978                    key.line,
6979                    1,
6980                    "key: tylko id w inkrementcie 3",
6981                ));
6982            }
6983        }
6984    }
6985    let bindings = match res {
6986        Resolver::Static => {
6987            return Err(SeqError::new(
6988                file,
6989                node.line,
6990                1,
6991                "List(items:) wymaga hosta Rust (HostBindings::rows())",
6992            ));
6993        }
6994        Resolver::Host(b) => (*b).clone(),
6995    };
6996    let get_rows = bindings.get_rows(&items_prop).ok_or_else(|| {
6997        SeqError::new(
6998            file,
6999            items.line,
7000            1,
7001            format!("nieznany prop '{items_prop}' (HostBindings::rows())"),
7002        )
7003    })?;
7004    let template = node.children.clone();
7005    let file_owned = file.to_string();
7006    let theme_owned = *theme;
7007
7008    if let Some(first) = get_rows().first() {
7009        let res = Resolver::Host(&bindings);
7010        for n in &template {
7011            lower_layer(&file_owned, &theme_owned, n, &res, Some(first))?;
7012        }
7013    }
7014    let render = Rc::new(move |row: &SeqRowData| {
7015        let res = Resolver::Host(&bindings);
7016        template
7017            .iter()
7018            .filter_map(|n| lower_layer(&file_owned, &theme_owned, n, &res, Some(row)).ok())
7019            .collect::<Vec<_>>()
7020    });
7021    Ok(DrawLayer::Dyn(DynShapes {
7022        rows: get_rows,
7023        render,
7024    }))
7025}
7026
7027#[derive(Clone)]
7028enum TextSrc {
7029    Lit(String),
7030    Dyn(Rc<dyn Fn() -> String>),
7031}
7032
7033impl TextSrc {
7034    fn val(&self) -> String {
7035        match self {
7036            Self::Lit(s) => s.clone(),
7037            Self::Dyn(f) => f(),
7038        }
7039    }
7040
7041    fn touch_rc(&self) -> Rc<dyn Fn()> {
7042        let this = self.clone();
7043        Rc::new(move || {
7044            this.val();
7045        })
7046    }
7047}
7048
7049struct TickState {
7050    angle: Rc<Cell<f32>>,
7051    last: Rc<std::cell::RefCell<Option<std::time::Instant>>>,
7052    running: BoolSrc,
7053    speed: NumSrc,
7054}
7055
7056#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
7057fn lower_shape(
7058    file: &str,
7059    theme: &Theme,
7060    node: &SeqNode,
7061    res: &Resolver<'_>,
7062    row: Option<&SeqRowData>,
7063) -> Result<ShapeDraw, SeqError> {
7064    let visible = shape_visible(file, res, row, node)?;
7065    let named = shape_name(file, node, res, row)?;
7066    let touch_visible: Rc<dyn Fn()> = {
7067        let visible = visible.clone();
7068        Rc::new(move || visible.touch())
7069    };
7070
7071    let gate = move |draw: Rc<dyn Fn(&mut Painter)>| {
7072        let visible = visible.clone();
7073        Rc::new(move |g: &mut Painter| {
7074            if visible.val() {
7075                draw(g);
7076            }
7077        })
7078    };
7079
7080    let mk_hits = |test: Rc<dyn Fn(Point) -> bool>| -> Vec<HitTest> {
7081        named.clone().map(|name| (name, test)).into_iter().collect()
7082    };
7083    match node.kind.as_str() {
7084        "Grid" => {
7085            let step = shape_num(file, res, row, node, "step")?;
7086            let color = shape_color(file, theme, node).unwrap_or(theme.border);
7087            let t = touches(&[step.touch_rc(), touch_visible.clone()]);
7088            Ok(ShapeDraw {
7089                draw: gate(Rc::new(move |g: &mut Painter| {
7090                    g.grid(step.val(), color);
7091                })),
7092                touch: t,
7093                hits: Vec::new(),
7094            })
7095        }
7096        "Rect" => {
7097            let r = rect_src_of(file, res, row, node)?;
7098            let radius = opt_num(file, res, row, node, "radius")?;
7099            let color = shape_color(file, theme, node)?;
7100            let t = touches(&[r.touch_rc(), radius.touch_rc(), touch_visible.clone()]);
7101            let hr = r.clone();
7102            Ok(ShapeDraw {
7103                draw: gate(Rc::new(move |g: &mut Painter| {
7104                    g.rounded(r.val(), radius.val(), Fill::solid(color));
7105                })),
7106                touch: t,
7107                hits: mk_hits(Rc::new(move |p: Point| hr.val().contains(p))),
7108            })
7109        }
7110        "Circle" => {
7111            let c = shape_pt(file, res, row, node, "c")?;
7112            let r = shape_num(file, res, row, node, "r")?;
7113            let color = shape_color(file, theme, node)?;
7114
7115            let pr = match find_arg(node, "pick_r") {
7116                None => r.clone(),
7117                Some(arg) => dyn_f32(file, arg.line, res, row, &arg.value, "pick_r")?,
7118            };
7119            let t = touches(&[
7120                c.touch_rc(),
7121                r.touch_rc(),
7122                pr.touch_rc(),
7123                touch_visible.clone(),
7124            ]);
7125            let (hc, hr, hp) = (c.clone(), r.clone(), pr.clone());
7126            Ok(ShapeDraw {
7127                draw: gate(Rc::new(move |g: &mut Painter| {
7128                    g.circle(c.val(), r.val(), Fill::solid(color));
7129                })),
7130                touch: t,
7131                hits: mk_hits(Rc::new(move |p: Point| {
7132                    let c = hc.val();
7133                    let dx = p.x - c.x;
7134                    let dy = p.y - c.y;
7135                    let rr = hp.val().max(hr.val());
7136                    dx * dx + dy * dy <= rr * rr
7137                })),
7138            })
7139        }
7140        "Ellipse" => {
7141            let c = shape_pt(file, res, row, node, "c")?;
7142            let rx = shape_num(file, res, row, node, "rx")?;
7143            let ry = shape_num(file, res, row, node, "ry")?;
7144            let color = shape_color(file, theme, node)?;
7145            let t = touch4e(&c, &rx, &ry, &touch_visible);
7146            let (hc, hx, hy) = (c.clone(), rx.clone(), ry.clone());
7147            Ok(ShapeDraw {
7148                draw: gate(Rc::new(move |g: &mut Painter| {
7149                    g.ellipse(c.val(), rx.val(), ry.val(), color);
7150                })),
7151                touch: t,
7152                hits: mk_hits(Rc::new(move |p: Point| {
7153                    let c = hc.val();
7154                    let (rx, ry) = (hx.val(), hy.val());
7155                    if rx <= 0.0 || ry <= 0.0 {
7156                        return false;
7157                    }
7158                    let dx = (p.x - c.x) / rx;
7159                    let dy = (p.y - c.y) / ry;
7160                    dx * dx + dy * dy <= 1.0
7161                })),
7162            })
7163        }
7164        "Ring" => {
7165            let c = shape_pt(file, res, row, node, "c")?;
7166            let r = shape_num(file, res, row, node, "r")?;
7167            let w = shape_num(file, res, row, node, "width")?;
7168            let color = only_color(file, theme, node)?;
7169            let t = touch4e(&c, &r, &w, &touch_visible);
7170            let (hc, hr, hw) = (c.clone(), r.clone(), w.clone());
7171            Ok(ShapeDraw {
7172                draw: gate(Rc::new(move |g: &mut Painter| {
7173                    g.ring(c.val(), r.val(), w.val(), color);
7174                })),
7175                touch: t,
7176                hits: mk_hits(Rc::new(move |p: Point| {
7177                    let c = hc.val();
7178                    let dx = p.x - c.x;
7179                    let dy = p.y - c.y;
7180                    ((dx * dx + dy * dy).sqrt() - hr.val()).abs() <= hw.val()
7181                })),
7182            })
7183        }
7184        "Line" => {
7185            let a = shape_pt(file, res, row, node, "a")?;
7186            let b = shape_pt(file, res, row, node, "b")?;
7187            let w = shape_num(file, res, row, node, "width")?;
7188            let color = only_color(file, theme, node)?;
7189            let t = touch4(&a, &b, &w, &touch_visible);
7190            let (ha, hb, hw) = (a.clone(), b.clone(), w.clone());
7191            Ok(ShapeDraw {
7192                draw: gate(Rc::new(move |g: &mut Painter| {
7193                    g.line(
7194                        a.val(),
7195                        b.val(),
7196                        sequoia_core::BrushStroke::new(w.val()),
7197                        color,
7198                    );
7199                })),
7200                touch: t,
7201                hits: mk_hits(Rc::new(move |p: Point| {
7202                    dist_seg(p, ha.val(), hb.val()) <= (hw.val() / 2.0).max(3.0)
7203                })),
7204            })
7205        }
7206        "Arrow" => {
7207            let a = shape_pt(file, res, row, node, "a")?;
7208            let b = shape_pt(file, res, row, node, "b")?;
7209            let w = shape_num(file, res, row, node, "width")?;
7210            let head = shape_num(file, res, row, node, "head")?;
7211            let color = only_color(file, theme, node)?;
7212            let t = touch5(&a, &b, &w, &head, &touch_visible);
7213            let (ha, hb, hw) = (a.clone(), b.clone(), w.clone());
7214            Ok(ShapeDraw {
7215                draw: gate(Rc::new(move |g: &mut Painter| {
7216                    let h = head.val();
7217                    g.arrow(a.val(), b.val(), w.val(), h, h, color);
7218                })),
7219                touch: t,
7220                hits: mk_hits(Rc::new(move |p: Point| {
7221                    dist_seg(p, ha.val(), hb.val()) <= (hw.val() / 2.0).max(3.0)
7222                })),
7223            })
7224        }
7225        "Capsule" => {
7226            let a = shape_pt(file, res, row, node, "a")?;
7227            let b = shape_pt(file, res, row, node, "b")?;
7228            let r = shape_num(file, res, row, node, "r")?;
7229            let color = shape_color(file, theme, node)?;
7230            let t = touch4e(&a, &b, &r, &touch_visible);
7231            let (ha, hb, hr) = (a.clone(), b.clone(), r.clone());
7232            Ok(ShapeDraw {
7233                draw: gate(Rc::new(move |g: &mut Painter| {
7234                    g.capsule(a.val(), b.val(), r.val(), color);
7235                })),
7236                touch: t,
7237                hits: mk_hits(Rc::new(move |p: Point| {
7238                    dist_seg(p, ha.val(), hb.val()) <= hr.val()
7239                })),
7240            })
7241        }
7242        "Polygon" => {
7243            let pts = points_src_of(file, res, row, node)?;
7244            let color = shape_color(file, theme, node)?;
7245            let t = touch_pts(&pts, &touch_visible);
7246            let hpts = pts.clone();
7247            Ok(ShapeDraw {
7248                draw: gate(Rc::new(move |g: &mut Painter| {
7249                    let v: Vec<Point> = pts.iter().map(PtSrc::val).collect();
7250                    g.polygon(&v, color);
7251                })),
7252                touch: t,
7253                hits: mk_hits(Rc::new(move |p: Point| {
7254                    let v: Vec<Point> = hpts.iter().map(PtSrc::val).collect();
7255                    point_in_poly(p, &v)
7256                })),
7257            })
7258        }
7259        "Polyline" => {
7260            let pts = points_src_of(file, res, row, node)?;
7261            let w = shape_num(file, res, row, node, "width")?;
7262            let color = only_color(file, theme, node)?;
7263            let t = touch_pts_w(&pts, &w, &touch_visible);
7264            let (hpts, hw) = (pts.clone(), w.clone());
7265            Ok(ShapeDraw {
7266                draw: gate(Rc::new(move |g: &mut Painter| {
7267                    let v: Vec<Point> = pts.iter().map(PtSrc::val).collect();
7268                    g.polyline(&v, sequoia_core::BrushStroke::new(w.val()), color);
7269                })),
7270                touch: t,
7271                hits: mk_hits(Rc::new(move |p: Point| {
7272                    let v: Vec<Point> = hpts.iter().map(PtSrc::val).collect();
7273                    dist_polyline(p, &v) <= (hw.val() / 2.0).max(3.0)
7274                })),
7275            })
7276        }
7277        "Spline" => {
7278            let pts = points_src_of(file, res, row, node)?;
7279            if pts.len() != 4 {
7280                return Err(SeqError::new(
7281                    file,
7282                    node.line,
7283                    1,
7284                    "A spline is exactly 4 points (p0, c0, c1, p1)",
7285                ));
7286            }
7287            let w = shape_num(file, res, row, node, "width")?;
7288            let color = only_color(file, theme, node)?;
7289            let t = touch_pts_w(&pts, &w, &touch_visible);
7290            let (hpts, hw) = (pts.clone(), w.clone());
7291            Ok(ShapeDraw {
7292                draw: gate(Rc::new(move |g: &mut Painter| {
7293                    let v: Vec<Point> = pts.iter().map(PtSrc::val).collect();
7294                    g.polyline(
7295                        &sample_cubic(&v),
7296                        sequoia_core::BrushStroke::new(w.val()),
7297                        color,
7298                    );
7299                })),
7300                touch: t,
7301                hits: mk_hits(Rc::new(move |p: Point| {
7302                    let v: Vec<Point> = hpts.iter().map(PtSrc::val).collect();
7303                    dist_polyline(p, &sample_cubic(&v)) <= (hw.val() / 2.0).max(3.0)
7304                })),
7305            })
7306        }
7307        "Star" => {
7308            let c = shape_pt(file, res, row, node, "c")?;
7309            let outer = shape_num(file, res, row, node, "outer")?;
7310            let inner = shape_num(file, res, row, node, "inner")?;
7311            let n = shape_num(file, res, row, node, "n")?;
7312            let rot = opt_num(file, res, row, node, "rotation")?;
7313            let color = only_color(file, theme, node)?;
7314            let t = touch5(&c, &outer, &inner, &n, &touch_visible);
7315            let (hc, ho) = (c.clone(), outer.clone());
7316            Ok(ShapeDraw {
7317                draw: gate(Rc::new(move |g: &mut Painter| {
7318                    g.star(
7319                        c.val(),
7320                        outer.val(),
7321                        inner.val(),
7322                        n.val() as u32,
7323                        rot.val(),
7324                        color,
7325                    );
7326                })),
7327                touch: t,
7328                hits: mk_hits(Rc::new(move |p: Point| {
7329                    let c = hc.val();
7330                    let dx = p.x - c.x;
7331                    let dy = p.y - c.y;
7332                    dx * dx + dy * dy <= ho.val() * ho.val()
7333                })),
7334            })
7335        }
7336        "Ngon" => {
7337            let c = shape_pt(file, res, row, node, "c")?;
7338            let r = shape_num(file, res, row, node, "r")?;
7339            let sides = shape_num(file, res, row, node, "sides")?;
7340            let rot = opt_num(file, res, row, node, "rotation")?;
7341            let color = only_color(file, theme, node)?;
7342            let t = touch5(&c, &r, &sides, &rot, &touch_visible);
7343            let (hc, hr) = (c.clone(), r.clone());
7344            Ok(ShapeDraw {
7345                draw: gate(Rc::new(move |g: &mut Painter| {
7346                    g.regular_polygon(c.val(), r.val(), sides.val() as u32, rot.val(), color);
7347                })),
7348                touch: t,
7349                hits: mk_hits(Rc::new(move |p: Point| {
7350                    let c = hc.val();
7351                    let dx = p.x - c.x;
7352                    let dy = p.y - c.y;
7353                    dx * dx + dy * dy <= hr.val() * hr.val()
7354                })),
7355            })
7356        }
7357        "Cross" => {
7358            let c = shape_pt(file, res, row, node, "c")?;
7359            let half = shape_num(file, res, row, node, "half")?;
7360            let w = shape_num(file, res, row, node, "width")?;
7361            let color = only_color(file, theme, node)?;
7362            let t = touch4e(&c, &half, &w, &touch_visible);
7363            let (hc, hh, hw) = (c.clone(), half.clone(), w.clone());
7364            Ok(ShapeDraw {
7365                draw: gate(Rc::new(move |g: &mut Painter| {
7366                    g.cross(c.val(), half.val(), w.val(), color);
7367                })),
7368                touch: t,
7369                hits: mk_hits(Rc::new(move |p: Point| {
7370                    let c = hc.val();
7371                    let (dx, dy) = ((p.x - c.x).abs(), (p.y - c.y).abs());
7372                    let (h, w) = (hh.val(), hw.val() / 2.0);
7373                    (dx <= h && dy <= w) || (dy <= h && dx <= w)
7374                })),
7375            })
7376        }
7377        "Text2d" => {
7378            let color = only_color(file, theme, node)?;
7379            let at = shape_pt(file, res, row, node, "at")?;
7380            let size = opt_num(file, res, row, node, "size")?;
7381            let has_size = find_arg(node, "size").is_some();
7382            let t = touch3(&at, &size, &touch_visible);
7383
7384            let text: TextSrc = if let Some(t_arg) = find_arg(node, "t") {
7385                match &t_arg.value {
7386                    SeqValue::Str(key) => match res {
7387                        Resolver::Static => {
7388                            return Err(SeqError::new(
7389                                file,
7390                                t_arg.line,
7391                                1,
7392                                "t: wymaga hosta Rust (HostBindings::i18n)",
7393                            ));
7394                        }
7395                        Resolver::Host(b) => {
7396                            let i18n = b.get_i18n().ok_or_else(|| {
7397                                SeqError::new(file, t_arg.line, 1, "t: wymaga HostBindings::i18n()")
7398                            })?;
7399                            let key = key.clone();
7400                            TextSrc::Dyn(Rc::new(move || i18n.t(&key)))
7401                        }
7402                    },
7403                    _ => {
7404                        return Err(SeqError::new(
7405                            file,
7406                            t_arg.line,
7407                            1,
7408                            "t: is the key in quotation marks (t: \"save\")",
7409                        ));
7410                    }
7411                }
7412            } else {
7413                match find_arg(node, "str") {
7414                    None => {
7415                        return Err(SeqError::new(
7416                            file,
7417                            node.line,
7418                            1,
7419                            "Text2d wymaga str: \"...\"",
7420                        ));
7421                    }
7422                    Some(arg) => match &arg.value {
7423                        SeqValue::Str(s) => TextSrc::Lit(s.clone()),
7424                        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
7425                            Resolver::Static => {
7426                                return Err(SeqError::new(
7427                                    file,
7428                                    arg.line,
7429                                    1,
7430                                    "str: z propertu wymaga hosta",
7431                                ));
7432                            }
7433                            Resolver::Host(b) => TextSrc::Dyn(display_of(file, arg.line, b, prop)?),
7434                        },
7435                        SeqValue::Env(name) => match res {
7436                            Resolver::Static => {
7437                                return Err(SeqError::new(
7438                                    file,
7439                                    arg.line,
7440                                    1,
7441                                    "str: from environment requires host",
7442                                ));
7443                            }
7444                            Resolver::Host(b) => {
7445                                TextSrc::Dyn(display_env_of(file, arg.line, b, name)?)
7446                            }
7447                        },
7448                        SeqValue::ItemField(field) => match res {
7449                            Resolver::Static => {
7450                                return Err(SeqError::new(
7451                                    file,
7452                                    arg.line,
7453                                    1,
7454                                    "str: z wiersza wymaga hosta",
7455                                ));
7456                            }
7457                            Resolver::Host(_) => {
7458                                TextSrc::Dyn(display_field(file, arg.line, row, field)?)
7459                            }
7460                        },
7461                        _ => {
7462                            return Err(SeqError::new(
7463                                file,
7464                                arg.line,
7465                                1,
7466                                "str: is a string in quotation marks or dynamic(...)",
7467                            ));
7468                        }
7469                    },
7470                }
7471            };
7472            let t = touches(&[t, text.touch_rc()]);
7473            Ok(ShapeDraw {
7474                draw: gate(Rc::new(move |g: &mut Painter| {
7475                    g.text(
7476                        &text.val(),
7477                        at.val(),
7478                        if has_size { size.val() } else { 13.0 },
7479                        color,
7480                    );
7481                })),
7482                touch: t,
7483                hits: Vec::new(),
7484            })
7485        }
7486        "Svg" => {
7487            let d = shape_str(file, node, "d")?;
7488            let color = shape_color(file, theme, node)?;
7489            Ok(ShapeDraw {
7490                draw: gate(Rc::new(move |g: &mut Painter| {
7491                    g.ctx().draw_svg(&d, None, color);
7492                })),
7493                touch: touch_visible,
7494                hits: Vec::new(),
7495            })
7496        }
7497        "Dots" => {
7498            let step = shape_num(file, res, row, node, "step")?;
7499            let color = shape_color(file, theme, node).unwrap_or(theme.border);
7500            let radius = opt_num(file, res, row, node, "radius")?;
7501            let has_radius = find_arg(node, "radius").is_some();
7502            let t = touch3(&step, &radius, &touch_visible);
7503            Ok(ShapeDraw {
7504                draw: gate(Rc::new(move |g: &mut Painter| {
7505                    let st = step.val();
7506                    let r = if has_radius {
7507                        radius.val()
7508                    } else {
7509                        (st / 8.0).max(1.0)
7510                    };
7511                    g.dots(st, Vec2::ZERO, r, color);
7512                })),
7513                touch: t,
7514                hits: Vec::new(),
7515            })
7516        }
7517        "Axes" => {
7518            let color = shape_color(file, theme, node).unwrap_or(theme.border);
7519            let w = opt_num(file, res, row, node, "width")?;
7520            let has_w = find_arg(node, "width").is_some();
7521            let at = opt_pt(file, res, row, node, "at")?.unwrap_or(PtSrc {
7522                x: NumSrc::Lit(0.0),
7523                y: NumSrc::Lit(0.0),
7524            });
7525            let t = touch3(&w, &at, &touch_visible);
7526            Ok(ShapeDraw {
7527                draw: gate(Rc::new(move |g: &mut Painter| {
7528                    g.axes(at.val(), color, if has_w { w.val() } else { 1.0 });
7529                })),
7530                touch: t,
7531                hits: Vec::new(),
7532            })
7533        }
7534        "Frame" => {
7535            let r = rect_src_of(file, res, row, node)?;
7536            let w = shape_num(file, res, row, node, "width")?;
7537            let color = only_color(file, theme, node)?;
7538            let t = touch3(&r, &w, &touch_visible);
7539            Ok(ShapeDraw {
7540                draw: gate(Rc::new(move |g: &mut Painter| {
7541                    g.frame(r.val(), color, w.val());
7542                })),
7543                touch: t,
7544                hits: Vec::new(),
7545            })
7546        }
7547        "Marquee" => {
7548            let r = rect_src_of(file, res, row, node)?;
7549            let color = only_color(file, theme, node)?;
7550            let t = touch2(&r, &touch_visible);
7551            let hr = r.clone();
7552            Ok(ShapeDraw {
7553                draw: gate(Rc::new(move |g: &mut Painter| {
7554                    let q = r.val();
7555                    g.marquee(
7556                        Point::new(q.left(), q.top()),
7557                        Point::new(q.right(), q.bottom()),
7558                        Fill::solid(Color::TRANSPARENT),
7559                        color,
7560                    );
7561                })),
7562                touch: t,
7563                hits: mk_hits(Rc::new(move |p: Point| hr.val().contains(p))),
7564            })
7565        }
7566        "Handle" => {
7567            let at = shape_pt(file, res, row, node, "at")?;
7568            let r = opt_num(file, res, row, node, "r")?;
7569            let has_r = find_arg(node, "r").is_some();
7570            let color = only_color(file, theme, node)?;
7571            let t = touch3(&at, &r, &touch_visible);
7572            let hat = at.clone();
7573            let hr2 = r.clone();
7574            Ok(ShapeDraw {
7575                draw: gate(Rc::new(move |g: &mut Painter| {
7576                    g.knob(at.val(), if has_r { r.val() } else { 4.0 }, color);
7577                })),
7578                touch: t,
7579                hits: mk_hits(Rc::new(move |p: Point| {
7580                    let c = hat.val();
7581                    let rr = if has_r { hr2.val() } else { 4.0 };
7582                    let dx = p.x - c.x;
7583                    let dy = p.y - c.y;
7584                    dx * dx + dy * dy <= rr.max(6.0) * rr.max(6.0)
7585                })),
7586            })
7587        }
7588        "Image2d" => {
7589            let src: TextSrc = match find_arg(node, "src") {
7590                None => {
7591                    return Err(SeqError::new(
7592                        file,
7593                        node.line,
7594                        1,
7595                        "Image2d wymaga src: \"...\"",
7596                    ));
7597                }
7598                Some(arg) => match &arg.value {
7599                    SeqValue::Str(s) => TextSrc::Lit(s.clone()),
7600                    SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
7601                        Resolver::Static => {
7602                            return Err(SeqError::new(
7603                                file,
7604                                arg.line,
7605                                1,
7606                                "src: z propertu wymaga hosta",
7607                            ));
7608                        }
7609                        Resolver::Host(b) => TextSrc::Dyn(display_of(file, arg.line, b, prop)?),
7610                    },
7611                    SeqValue::Env(name) => match res {
7612                        Resolver::Static => {
7613                            return Err(SeqError::new(
7614                                file,
7615                                arg.line,
7616                                1,
7617                                "src: from environment requires host",
7618                            ));
7619                        }
7620                        Resolver::Host(b) => TextSrc::Dyn(display_env_of(file, arg.line, b, name)?),
7621                    },
7622                    SeqValue::ItemField(field) => match res {
7623                        Resolver::Static => {
7624                            return Err(SeqError::new(
7625                                file,
7626                                arg.line,
7627                                1,
7628                                "src: z wiersza wymaga hosta",
7629                            ));
7630                        }
7631                        Resolver::Host(_) => {
7632                            TextSrc::Dyn(display_field(file, arg.line, row, field)?)
7633                        }
7634                    },
7635                    _ => {
7636                        return Err(SeqError::new(
7637                            file,
7638                            arg.line,
7639                            1,
7640                            "src: is a string in quotation marks or dynamic(...)",
7641                        ));
7642                    }
7643                },
7644            };
7645            let r = rect_src_of(file, res, row, node)?;
7646            let uv = uv_src_of(file, res, row, node)?;
7647            let t = touches(&[
7648                r.touch_rc(),
7649                src.touch_rc(),
7650                uv[0].touch_rc(),
7651                uv[1].touch_rc(),
7652                uv[2].touch_rc(),
7653                uv[3].touch_rc(),
7654                touch_visible.clone(),
7655            ]);
7656            let hr = r.clone();
7657            Ok(ShapeDraw {
7658                draw: gate(Rc::new(move |g: &mut Painter| {
7659                    let path = src.val();
7660                    if path.is_empty() {
7661                        return;
7662                    }
7663                    g.image(
7664                        r.val(),
7665                        [uv[0].val(), uv[1].val(), uv[2].val(), uv[3].val()],
7666                        ImageSource::File(path),
7667                    );
7668                })),
7669                touch: t,
7670                hits: mk_hits(Rc::new(move |p: Point| hr.val().contains(p))),
7671            })
7672        }
7673        "Clip" | "Group" | "List" => Err(SeqError::new(
7674            file,
7675            node.line,
7676            1,
7677            format!(
7678                "'{}' is a Canvas layer, not a shape (internal error)",
7679                node.kind
7680            ),
7681        )),
7682        other => Err(SeqError::new(
7683            file,
7684            node.line,
7685            1,
7686            format!("unknown shape '{other}'"),
7687        )),
7688    }
7689}
7690
7691trait FlexBoxOps {
7692    fn set_width(self, w: Length) -> Self;
7693    fn set_height(self, h: Length) -> Self;
7694    fn set_padding(self, p: PaddingVal) -> Self;
7695    fn set_padding_x(self, x: f32) -> Self;
7696    fn set_padding_y(self, y: f32) -> Self;
7697    fn set_background(self, c: Color) -> Self;
7698    fn set_flex(self, f: f32) -> Self;
7699    fn set_gap(self, g: f32) -> Self;
7700    fn set_cross_gap(self, g: f32) -> Self;
7701    fn set_wrap(self, w: bool) -> Self;
7702    fn set_align(self, a: Align) -> Self;
7703    fn set_justify(self, j: Justify) -> Self;
7704    fn set_min_width(self, v: f32) -> Self;
7705    fn set_max_width(self, v: f32) -> Self;
7706    fn set_min_height(self, v: f32) -> Self;
7707    fn set_max_height(self, v: f32) -> Self;
7708    fn set_margin(self, m: MarginVal) -> Self;
7709    fn set_margin_x(self, x: f32) -> Self;
7710    fn set_margin_y(self, y: f32) -> Self;
7711    fn set_margin_left(self, v: f32) -> Self;
7712    fn set_margin_right(self, v: f32) -> Self;
7713    fn set_margin_top(self, v: f32) -> Self;
7714    fn set_margin_bottom(self, v: f32) -> Self;
7715    fn set_overflow(self, o: Overflow) -> Self;
7716    fn set_hit_testable(self, on: bool) -> Self;
7717    fn add_child(self, kid: AnyView) -> Self;
7718    fn add_child_flex(self, kid: AnyView, flex: f32) -> Self;
7719}
7720
7721macro_rules! impl_flex_box {
7722    ($t:ty) => {
7723        impl FlexBoxOps for $t {
7724            fn set_width(self, w: Length) -> Self {
7725                self.width(w)
7726            }
7727            fn set_height(self, h: Length) -> Self {
7728                self.height(h)
7729            }
7730            fn set_padding(self, p: PaddingVal) -> Self {
7731                match p {
7732                    PaddingVal::All(v) => self.padding(v),
7733                    PaddingVal::Xy(x, y) => self.padding_xy(x, y),
7734                }
7735            }
7736            fn set_padding_x(self, x: f32) -> Self {
7737                self.padding_x(x)
7738            }
7739            fn set_padding_y(self, y: f32) -> Self {
7740                self.padding_y(y)
7741            }
7742            fn set_background(self, c: Color) -> Self {
7743                self.background(c)
7744            }
7745            fn set_flex(self, f: f32) -> Self {
7746                self.flex(f)
7747            }
7748            fn set_gap(self, g: f32) -> Self {
7749                self.gap(g)
7750            }
7751            fn set_cross_gap(self, g: f32) -> Self {
7752                self.cross_gap(g)
7753            }
7754            fn set_wrap(self, w: bool) -> Self {
7755                self.wrap(w)
7756            }
7757            fn set_align(self, a: Align) -> Self {
7758                self.align_items(a)
7759            }
7760            fn set_justify(self, j: Justify) -> Self {
7761                self.justify_content(j)
7762            }
7763            fn set_min_width(self, v: f32) -> Self {
7764                self.min_width(v)
7765            }
7766            fn set_max_width(self, v: f32) -> Self {
7767                self.max_width(v)
7768            }
7769            fn set_min_height(self, v: f32) -> Self {
7770                self.min_height(v)
7771            }
7772            fn set_max_height(self, v: f32) -> Self {
7773                self.max_height(v)
7774            }
7775            fn set_margin(self, m: MarginVal) -> Self {
7776                match m {
7777                    MarginVal::All(v) => self.margin(v),
7778                    MarginVal::Xy(x, y) => self.margin_xy(x, y),
7779                }
7780            }
7781            fn set_margin_x(self, x: f32) -> Self {
7782                self.margin_x(x)
7783            }
7784            fn set_margin_y(self, y: f32) -> Self {
7785                self.margin_y(y)
7786            }
7787            fn set_margin_left(self, v: f32) -> Self {
7788                self.margin_left(v)
7789            }
7790            fn set_margin_right(self, v: f32) -> Self {
7791                self.margin_right(v)
7792            }
7793            fn set_margin_top(self, v: f32) -> Self {
7794                self.margin_top(v)
7795            }
7796            fn set_margin_bottom(self, v: f32) -> Self {
7797                self.margin_bottom(v)
7798            }
7799            fn set_overflow(self, o: Overflow) -> Self {
7800                self.overflow(o)
7801            }
7802            fn set_hit_testable(self, on: bool) -> Self {
7803                self.hit_testable(on)
7804            }
7805            fn add_child(self, kid: AnyView) -> Self {
7806                self.child(kid)
7807            }
7808            fn add_child_flex(self, kid: AnyView, flex: f32) -> Self {
7809                self.child_flex(kid, flex)
7810            }
7811        }
7812    };
7813}
7814
7815impl_flex_box!(Column);
7816impl_flex_box!(Row);
7817
7818fn has_click(node: &SeqNode) -> bool {
7819    find_arg(node, "on_click").is_some() || find_arg(node, "hover").is_some()
7820}
7821
7822fn is_click_boundary(node: &SeqNode) -> bool {
7823    match node.kind.as_str() {
7824        "Button" | "TextField" | "Checkbox" | "Switch" | "Slider" | "Dropdown" | "Canvas" => true,
7825        "Container" | "Row" | "Column" => has_click(node),
7826        _ => false,
7827    }
7828}
7829
7830fn can_be_hitless(kind: &str) -> bool {
7831    matches!(
7832        kind,
7833        "Column" | "Row" | "Container" | "Panel" | "Stack" | "Wrap" | "Text" | "Icon" | "Spacer"
7834    )
7835}
7836
7837fn click_through(node: &SeqNode) -> SeqNode {
7838    if is_click_boundary(node) {
7839        return node.clone();
7840    }
7841    let mut out = node.clone();
7842    if can_be_hitless(&node.kind) && !out.args.iter().any(|a| a.name == "hit") {
7843        out.args.push(SeqArg {
7844            name: "hit".to_string(),
7845            line: node.line,
7846            value: SeqValue::Bool(false),
7847        });
7848    }
7849    out.children = node.children.iter().map(click_through).collect();
7850    out
7851}
7852
7853fn lower_clickable_flex<V>(
7854    file: &str,
7855    theme: &Theme,
7856    styles: &BuiltStyles,
7857    node: &SeqNode,
7858    res: &Resolver<'_>,
7859    row: Option<&SeqRowData>,
7860    make: V,
7861) -> Result<AnyView, SeqError>
7862where
7863    V: FlexBoxOps + View + 'static,
7864{
7865    let mut outer = Container::new();
7866    if let Some(s) = box_style_of(file, styles, node)? {
7867        outer = outer.box_style(s);
7868    }
7869    for arg in &node.args {
7870        match arg.name.as_str() {
7871            "width" => outer = outer.width(len(file, arg, res, row)?),
7872            "height" => outer = outer.height(len(file, arg, res, row)?),
7873            "min_width" => outer = outer.min_width(num(file, arg, res, row)?),
7874            "max_width" => outer = outer.max_width(num(file, arg, res, row)?),
7875            "min_height" => outer = outer.min_height(num(file, arg, res, row)?),
7876            "max_height" => outer = outer.max_height(num(file, arg, res, row)?),
7877            "margin" => outer = apply_container_margin(outer, file, arg, res, row)?,
7878            "margin_x" => outer = outer.margin_x(num(file, arg, res, row)?),
7879            "margin_y" => outer = outer.margin_y(num(file, arg, res, row)?),
7880            "margin_left" => outer = outer.margin_left(num(file, arg, res, row)?),
7881            "margin_right" => outer = outer.margin_right(num(file, arg, res, row)?),
7882            "margin_top" => outer = outer.margin_top(num(file, arg, res, row)?),
7883            "margin_bottom" => outer = outer.margin_bottom(num(file, arg, res, row)?),
7884            "overflow" => outer = outer.overflow(overflow_val(file, arg)?),
7885            "background" => outer = outer.background(color(file, theme, arg)?),
7886            "hover" => outer = outer.hover(color(file, theme, arg)?),
7887            _ => {}
7888        }
7889    }
7890    if let Some(arg) = find_arg(node, "on_click") {
7891        match res {
7892            Resolver::Static => {
7893                return Err(SeqError::new(
7894                    file,
7895                    arg.line,
7896                    1,
7897                    "on_click wymaga hosta Rust",
7898                ));
7899            }
7900            Resolver::Host(b) => match &arg.value {
7901                SeqValue::Callback { .. } | SeqValue::CallSeq { .. } => {
7902                    let fire = wire_action(file, arg.line, b, &arg.value, row)?;
7903                    outer = outer.on_click(move || fire());
7904                }
7905                _ => {
7906                    return Err(SeqError::new(
7907                        file,
7908                        arg.line,
7909                        1,
7910                        "on_click: { akcja(args) }",
7911                    ));
7912                }
7913            },
7914        }
7915    }
7916
7917    let mut inner_args: Vec<SeqArg> = node
7918        .args
7919        .iter()
7920        .filter(|a| {
7921            !matches!(
7922                a.name.as_str(),
7923                "min_width"
7924                    | "max_width"
7925                    | "min_height"
7926                    | "max_height"
7927                    | "margin"
7928                    | "margin_x"
7929                    | "margin_y"
7930                    | "margin_left"
7931                    | "margin_right"
7932                    | "margin_top"
7933                    | "margin_bottom"
7934                    | "overflow"
7935                    | "background"
7936                    | "style"
7937                    | "on_click"
7938                    | "hover"
7939            )
7940        })
7941        .cloned()
7942        .collect();
7943
7944    if !inner_args.iter().any(|a| a.name == "hit") {
7945        inner_args.push(SeqArg {
7946            name: "hit".to_string(),
7947            line: node.line,
7948            value: SeqValue::Bool(false),
7949        });
7950    }
7951    let inner_node = SeqNode {
7952        kind: node.kind.clone(),
7953        line: node.line,
7954        label: node.label.clone(),
7955        args: inner_args,
7956        children: node.children.iter().map(click_through).collect(),
7957    };
7958    let inner = apply_flex_box(file, theme, &inner_node, make, res, row)?;
7959    let built = boxed_children(file, theme, styles, &inner_node, inner, res, row)?;
7960    outer = outer.child(built);
7961    Ok(AnyView::new(outer))
7962}
7963
7964fn indents_of(
7965    file: &str,
7966    res: &Resolver<'_>,
7967    row: Option<&SeqRowData>,
7968    node: &SeqNode,
7969) -> Result<Option<Indents>, SeqError> {
7970    let mut ind = Indents::NONE;
7971    let mut any = false;
7972    for arg in &node.args {
7973        match arg.name.as_str() {
7974            "indent" => {
7975                any = true;
7976                let n = san_indent(num(file, arg, res, row)?);
7977                ind.top = Some(n);
7978                ind.bottom = Some(n);
7979                ind.left = Some(n);
7980                ind.right = Some(n);
7981            }
7982            "indent_x" => {
7983                any = true;
7984                let n = san_indent(num(file, arg, res, row)?);
7985                ind.left = Some(n);
7986                ind.right = Some(n);
7987            }
7988            "indent_y" => {
7989                any = true;
7990                let n = san_indent(num(file, arg, res, row)?);
7991                ind.top = Some(n);
7992                ind.bottom = Some(n);
7993            }
7994            "indent_left" => {
7995                any = true;
7996                ind.left = Some(san_indent(num(file, arg, res, row)?));
7997            }
7998            "indent_right" => {
7999                any = true;
8000                ind.right = Some(san_indent(num(file, arg, res, row)?));
8001            }
8002            "indent_top" => {
8003                any = true;
8004                ind.top = Some(san_indent(num(file, arg, res, row)?));
8005            }
8006            "indent_bottom" => {
8007                any = true;
8008                ind.bottom = Some(san_indent(num(file, arg, res, row)?));
8009            }
8010            _ => {}
8011        }
8012    }
8013    Ok(any.then_some(ind))
8014}
8015
8016fn san_indent(v: f32) -> f32 {
8017    if v.is_finite() { v.max(0.0) } else { 0.0 }
8018}
8019
8020fn maybe_indent(
8021    view: AnyView,
8022    file: &str,
8023    node: &SeqNode,
8024    res: &Resolver<'_>,
8025    row: Option<&SeqRowData>,
8026) -> Result<AnyView, SeqError> {
8027    match indents_of(file, res, row, node)? {
8028        None => Ok(view),
8029        Some(ind) => Ok(AnyView::new(Margined::with_indents(view, ind))),
8030    }
8031}
8032
8033fn boxed_children<V>(
8034    file: &str,
8035    theme: &Theme,
8036    styles: &BuiltStyles,
8037    node: &SeqNode,
8038    mut v: V,
8039    res: &Resolver<'_>,
8040    row: Option<&SeqRowData>,
8041) -> Result<AnyView, SeqError>
8042where
8043    V: FlexBoxOps + View + 'static,
8044{
8045    for c in &node.children {
8046        let kid = lower_node(file, theme, styles, c, res, row)?;
8047
8048        v = match child_flex_weight(file, res, row, c)? {
8049            Some(w) => v.add_child_flex(kid, w),
8050            None => v.add_child(kid),
8051        };
8052    }
8053    Ok(AnyView::new(v))
8054}
8055
8056fn child_flex_weight(
8057    file: &str,
8058    res: &Resolver<'_>,
8059    row: Option<&SeqRowData>,
8060    node: &SeqNode,
8061) -> Result<Option<f32>, SeqError> {
8062    match find_arg(node, "flex") {
8063        None => Ok(None),
8064        Some(arg) => match &arg.value {
8065            SeqValue::Num(n) => Ok(Some(*n)),
8066            SeqValue::Ident(_)
8067            | SeqValue::Dynamic(_)
8068            | SeqValue::ItemField(_)
8069            | SeqValue::Env(_) => Ok(Some(num(file, arg, res, row)?)),
8070            _ => Err(SeqError::new(file, arg.line, 1, "'flex' to liczba")),
8071        },
8072    }
8073}
8074
8075fn single_child(
8076    file: &str,
8077    theme: &Theme,
8078    styles: &BuiltStyles,
8079    node: &SeqNode,
8080    res: &Resolver<'_>,
8081    row: Option<&SeqRowData>,
8082) -> Result<Option<AnyView>, SeqError> {
8083    match node.children.len() {
8084        0 => Ok(None),
8085        1 => Ok(Some(lower_node(
8086            file,
8087            theme,
8088            styles,
8089            &node.children[0],
8090            res,
8091            row,
8092        )?)),
8093        _ => {
8094            let mut col = Column::new();
8095            for c in &node.children {
8096                let kid = lower_node(file, theme, styles, c, res, row)?;
8097                col = match child_flex_weight(file, res, row, c)? {
8098                    Some(w) => col.child_flex(kid, w),
8099                    None => col.child(kid),
8100                };
8101            }
8102            Ok(Some(AnyView::new(col)))
8103        }
8104    }
8105}
8106
8107fn unreachable_checked<T>(
8108    file: &str,
8109    node: &SeqNode,
8110    arg: &crate::ir::SeqArg,
8111) -> Result<T, SeqError> {
8112    Err(SeqError::new(
8113        file,
8114        arg.line,
8115        1,
8116        format!(
8117            "unknown prop '{}' in {} (call check() first)",
8118            arg.name, node.kind
8119        ),
8120    ))
8121}
8122
8123fn find_arg<'n>(node: &'n SeqNode, name: &str) -> Option<&'n crate::ir::SeqArg> {
8124    node.args.iter().find(|a| a.name == name)
8125}
8126
8127enum RowVal {
8128    Sig(FieldSignal),
8129
8130    Id(i64),
8131}
8132
8133fn row_value(
8134    file: &str,
8135    line: usize,
8136    row: Option<&SeqRowData>,
8137    field: &str,
8138) -> Result<RowVal, SeqError> {
8139    match row {
8140        None => Err(SeqError::new(
8141            file,
8142            line,
8143            1,
8144            format!("item.{field} only inside List row"),
8145        )),
8146        Some(r) if field == "id" => Ok(RowVal::Id(r.id)),
8147        Some(r) => r
8148            .fields
8149            .get(field)
8150            .cloned()
8151            .map(RowVal::Sig)
8152            .ok_or_else(|| {
8153                let mut names: Vec<&str> = r.fields.keys().map(String::as_str).collect();
8154                names.sort_unstable();
8155                SeqError::new(
8156                    file,
8157                    line,
8158                    1,
8159                    format!(
8160                        "nieznane pole wiersza '{field}' (pola: {})",
8161                        names.join(", ")
8162                    ),
8163                )
8164            }),
8165    }
8166}
8167
8168#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
8169fn row_num(
8170    file: &str,
8171    line: usize,
8172    row: Option<&SeqRowData>,
8173    field: &str,
8174) -> Result<f32, SeqError> {
8175    match row_value(file, line, row, field)? {
8176        RowVal::Sig(FieldSignal::Int(sig)) => Ok(sig.get_untracked() as f32),
8177        RowVal::Sig(FieldSignal::Float(sig)) => Ok(sig.get_untracked() as f32),
8178        RowVal::Id(id) => Ok(id as f32),
8179        _ => Err(SeqError::new(
8180            file,
8181            line,
8182            1,
8183            format!("pole wiersza '{field}' to nie liczba (Int/Float/id)"),
8184        )),
8185    }
8186}
8187
8188fn opt_str(
8189    file: &str,
8190    node: &SeqNode,
8191    name: &str,
8192    res: &Resolver<'_>,
8193    row: Option<&SeqRowData>,
8194) -> Result<Option<String>, SeqError> {
8195    match find_arg(node, name) {
8196        None => Ok(None),
8197        Some(arg) => match &arg.value {
8198            SeqValue::Str(s) => Ok(Some(s.clone())),
8199            SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
8200                Resolver::Static => Err(str_err(file, arg, name)),
8201                Resolver::Host(b) => Ok(Some(
8202                    b.get_str(prop)
8203                        .map(|sig| sig.get_untracked())
8204                        .or_else(|| b.get_memo_str(prop).map(|m| m.get_untracked()))
8205                        .or_else(|| b.get_env_str(prop).map(|sig| sig.get_untracked()))
8206                        .ok_or_else(|| prop_err(file, arg.line, prop, "String"))?,
8207                )),
8208            },
8209            SeqValue::Env(env_name) => match res {
8210                Resolver::Static => Err(str_err(file, arg, name)),
8211                Resolver::Host(b) => Ok(Some(
8212                    b.get_env_str(env_name)
8213                        .map(|sig| sig.get_untracked())
8214                        .ok_or_else(|| {
8215                            SeqError::new(
8216                                file,
8217                                arg.line,
8218                                1,
8219                                format!(
8220                                    "unknown env.{env_name} or wrong type (here you need: String)"
8221                                ),
8222                            )
8223                        })?,
8224                )),
8225            },
8226            SeqValue::ItemField(field) => match res {
8227                Resolver::Static => Err(str_err(file, arg, name)),
8228                Resolver::Host(_) => match row_value(file, arg.line, row, field)? {
8229                    RowVal::Sig(FieldSignal::Str(sig)) => Ok(Some(sig.get_untracked())),
8230                    RowVal::Id(id) => Ok(Some(id.to_string())),
8231                    _ => Err(SeqError::new(
8232                        file,
8233                        arg.line,
8234                        1,
8235                        format!("pole wiersza '{field}' to nie tekst (String/id)"),
8236                    )),
8237                },
8238            },
8239            _ => Err(str_err(file, arg, name)),
8240        },
8241    }
8242}
8243
8244fn str_err(file: &str, arg: &crate::ir::SeqArg, name: &str) -> SeqError {
8245    SeqError::new(
8246        file,
8247        arg.line,
8248        1,
8249        format!("'{name}' is a string in quotation marks"),
8250    )
8251}
8252
8253fn prop_err(file: &str, line: usize, prop: &str, want: &str) -> SeqError {
8254    SeqError::new(
8255        file,
8256        line,
8257        1,
8258        format!("unknown prop '{prop}' or wrong type (here you need: {want})"),
8259    )
8260}
8261
8262#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
8263fn prop_num_env(file: &str, line: usize, b: &HostBindings, prop: &str) -> Result<f32, SeqError> {
8264    if let Some(sig) = b.get_float(prop) {
8265        Ok(sig.get_untracked() as f32)
8266    } else if let Some(sig) = b.get_int(prop) {
8267        Ok(sig.get_untracked() as f32)
8268    } else if let Some(m) = b.get_memo_float(prop) {
8269        Ok(m.get_untracked() as f32)
8270    } else if let Some(m) = b.get_memo_int(prop) {
8271        Ok(m.get_untracked() as f32)
8272    } else {
8273        Err(prop_err(file, line, prop, "Float/Int"))
8274    }
8275}
8276
8277#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
8278fn env_num(file: &str, line: usize, b: &HostBindings, name: &str) -> Result<f32, SeqError> {
8279    if let Some(sig) = b.get_env_float(name) {
8280        Ok(sig.get_untracked() as f32)
8281    } else if let Some(sig) = b.get_env_int(name) {
8282        Ok(sig.get_untracked() as f32)
8283    } else {
8284        Err(SeqError::new(
8285            file,
8286            line,
8287            1,
8288            format!("unknown env.{name} or wrong type (here you need: Float/Int)"),
8289        ))
8290    }
8291}
8292
8293fn prop_flag(file: &str, line: usize, b: &HostBindings, prop: &str) -> Result<bool, SeqError> {
8294    if let Some(sig) = b.get_bool(prop) {
8295        Ok(sig.get_untracked())
8296    } else if let Some(m) = b.get_memo_bool(prop) {
8297        Ok(m.get_untracked())
8298    } else {
8299        Err(prop_err(file, line, prop, "Bool"))
8300    }
8301}
8302
8303fn env_flag(file: &str, line: usize, b: &HostBindings, name: &str) -> Result<bool, SeqError> {
8304    b.get_env_bool(name)
8305        .map(|sig| sig.get_untracked())
8306        .ok_or_else(|| {
8307            SeqError::new(
8308                file,
8309                line,
8310                1,
8311                format!("unknown env.{name} or wrong type (here you need: Bool)"),
8312            )
8313        })
8314}
8315
8316fn num(
8317    file: &str,
8318    arg: &crate::ir::SeqArg,
8319    res: &Resolver<'_>,
8320    row: Option<&SeqRowData>,
8321) -> Result<f32, SeqError> {
8322    match &arg.value {
8323        SeqValue::Num(n) => {
8324            if n.is_finite() {
8325                Ok(*n)
8326            } else {
8327                Err(SeqError::new(
8328                    file,
8329                    arg.line,
8330                    1,
8331                    format!("'{}' must be finished", arg.name),
8332                ))
8333            }
8334        }
8335        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
8336            Resolver::Static => Err(num_err(file, arg)),
8337            Resolver::Host(b) => prop_num_env(file, arg.line, b, prop),
8338        },
8339        SeqValue::Env(name) => match res {
8340            Resolver::Static => Err(num_err(file, arg)),
8341            Resolver::Host(b) => env_num(file, arg.line, b, name),
8342        },
8343        SeqValue::ItemField(field) => match res {
8344            Resolver::Static => Err(num_err(file, arg)),
8345            Resolver::Host(_) => row_num(file, arg.line, row, field),
8346        },
8347        _ => Err(num_err(file, arg)),
8348    }
8349}
8350
8351fn num_err(file: &str, arg: &crate::ir::SeqArg) -> SeqError {
8352    SeqError::new(file, arg.line, 1, format!("'{}' to liczba", arg.name))
8353}
8354
8355fn flag(
8356    file: &str,
8357    arg: &crate::ir::SeqArg,
8358    res: &Resolver<'_>,
8359    row: Option<&SeqRowData>,
8360) -> Result<bool, SeqError> {
8361    match &arg.value {
8362        SeqValue::Bool(b) => Ok(*b),
8363        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
8364            Resolver::Static => Err(flag_err(file, arg)),
8365            Resolver::Host(b) => prop_flag(file, arg.line, b, prop),
8366        },
8367        SeqValue::Env(name) => match res {
8368            Resolver::Static => Err(flag_err(file, arg)),
8369            Resolver::Host(b) => env_flag(file, arg.line, b, name),
8370        },
8371        SeqValue::ItemField(field) => match res {
8372            Resolver::Static => Err(flag_err(file, arg)),
8373            Resolver::Host(_) => match row_value(file, arg.line, row, field)? {
8374                RowVal::Sig(FieldSignal::Bool(sig)) => Ok(sig.get_untracked()),
8375                _ => Err(SeqError::new(
8376                    file,
8377                    arg.line,
8378                    1,
8379                    format!("pole wiersza '{field}' to nie Bool"),
8380                )),
8381            },
8382        },
8383        _ => Err(flag_err(file, arg)),
8384    }
8385}
8386
8387fn flag_err(file: &str, arg: &crate::ir::SeqArg) -> SeqError {
8388    SeqError::new(file, arg.line, 1, format!("'{}' to true/false", arg.name))
8389}
8390
8391fn ident(file: &str, arg: &crate::ir::SeqArg) -> Result<String, SeqError> {
8392    match &arg.value {
8393        SeqValue::Ident(s) => Ok(s.clone()),
8394        _ => Err(SeqError::new(
8395            file,
8396            arg.line,
8397            1,
8398            format!("'{}' is the name (without quotes)", arg.name),
8399        )),
8400    }
8401}
8402
8403fn len(
8404    file: &str,
8405    arg: &crate::ir::SeqArg,
8406    res: &Resolver<'_>,
8407    row: Option<&SeqRowData>,
8408) -> Result<Length, SeqError> {
8409    match &arg.value {
8410        SeqValue::Length(l) => match *l {
8411            LengthVal::Fill(w) => Ok(Length::Fill(w)),
8412            LengthVal::Px(v) => Ok(Length::Px(v)),
8413            LengthVal::Fit => Ok(Length::Fit),
8414            LengthVal::Percent(p) => Ok(Length::Percent(p)),
8415        },
8416        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
8417            Resolver::Static => Err(len_err(file, arg)),
8418            Resolver::Host(b) => Ok(Length::Px(prop_num_env(file, arg.line, b, prop)?)),
8419        },
8420        SeqValue::Env(name) => match res {
8421            Resolver::Static => Err(len_err(file, arg)),
8422            Resolver::Host(b) => Ok(Length::Px(env_num(file, arg.line, b, name)?)),
8423        },
8424        SeqValue::ItemField(field) => match res {
8425            Resolver::Static => Err(len_err(file, arg)),
8426            Resolver::Host(_) => Ok(Length::Px(row_num(file, arg.line, row, field)?)),
8427        },
8428        _ => Err(len_err(file, arg)),
8429    }
8430}
8431
8432fn len_err(file: &str, arg: &crate::ir::SeqArg) -> SeqError {
8433    SeqError::new(
8434        file,
8435        arg.line,
8436        1,
8437        format!("'{}' to fill()/px()/fit/percent()", arg.name),
8438    )
8439}
8440
8441enum PaddingVal {
8442    All(f32),
8443    Xy(f32, f32),
8444}
8445
8446enum MarginVal {
8447    All(f32),
8448    Xy(f32, f32),
8449}
8450
8451fn margin_val(
8452    file: &str,
8453    arg: &crate::ir::SeqArg,
8454    res: &Resolver<'_>,
8455    row: Option<&SeqRowData>,
8456) -> Result<MarginVal, SeqError> {
8457    match &arg.value {
8458        SeqValue::Num(n) => Ok(MarginVal::All(*n)),
8459        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
8460            Resolver::Static => Err(margin_err(file, arg)),
8461            Resolver::Host(b) => Ok(MarginVal::All(prop_num_env(file, arg.line, b, prop)?)),
8462        },
8463        SeqValue::Env(name) => match res {
8464            Resolver::Static => Err(margin_err(file, arg)),
8465            Resolver::Host(b) => Ok(MarginVal::All(env_num(file, arg.line, b, name)?)),
8466        },
8467        SeqValue::ItemField(field) => match res {
8468            Resolver::Static => Err(margin_err(file, arg)),
8469            Resolver::Host(_) => Ok(MarginVal::All(row_num(file, arg.line, row, field)?)),
8470        },
8471        SeqValue::Array(items) => {
8472            if items.len() == 2 {
8473                match (&items[0], &items[1]) {
8474                    (SeqValue::Num(x), SeqValue::Num(y)) => Ok(MarginVal::Xy(*x, *y)),
8475                    _ => Err(SeqError::new(
8476                        file,
8477                        arg.line,
8478                        1,
8479                        "'margin' to liczba albo [x, y]",
8480                    )),
8481                }
8482            } else {
8483                Err(SeqError::new(
8484                    file,
8485                    arg.line,
8486                    1,
8487                    "'margin' to liczba albo [x, y]",
8488                ))
8489            }
8490        }
8491        _ => Err(SeqError::new(
8492            file,
8493            arg.line,
8494            1,
8495            "'margin' to liczba albo [x, y]",
8496        )),
8497    }
8498}
8499
8500fn margin_err(file: &str, arg: &crate::ir::SeqArg) -> SeqError {
8501    SeqError::new(file, arg.line, 1, "'margin' to liczba albo [x, y]")
8502}
8503
8504fn overflow_val(file: &str, arg: &crate::ir::SeqArg) -> Result<Overflow, SeqError> {
8505    match ident(file, arg)?.as_str() {
8506        "visible" => Ok(Overflow::Visible),
8507        "hidden" => Ok(Overflow::Hidden),
8508        "scroll" => Ok(Overflow::Scroll),
8509        "auto" => Ok(Overflow::Auto),
8510        other => Err(SeqError::new(
8511            file,
8512            arg.line,
8513            1,
8514            format!("overflow to visible/hidden/scroll/auto, nie '{other}'"),
8515        )),
8516    }
8517}
8518
8519fn padding_val(
8520    file: &str,
8521    arg: &crate::ir::SeqArg,
8522    res: &Resolver<'_>,
8523    row: Option<&SeqRowData>,
8524) -> Result<PaddingVal, SeqError> {
8525    match &arg.value {
8526        SeqValue::Num(n) => Ok(PaddingVal::All(*n)),
8527        SeqValue::Ident(prop) | SeqValue::Dynamic(prop) => match res {
8528            Resolver::Static => Err(padding_err(file, arg)),
8529            Resolver::Host(b) => Ok(PaddingVal::All(prop_num_env(file, arg.line, b, prop)?)),
8530        },
8531        SeqValue::Env(name) => match res {
8532            Resolver::Static => Err(padding_err(file, arg)),
8533            Resolver::Host(b) => Ok(PaddingVal::All(env_num(file, arg.line, b, name)?)),
8534        },
8535        SeqValue::ItemField(field) => match res {
8536            Resolver::Static => Err(padding_err(file, arg)),
8537            Resolver::Host(_) => Ok(PaddingVal::All(row_num(file, arg.line, row, field)?)),
8538        },
8539        SeqValue::Array(items) => {
8540            if items.len() == 2 {
8541                match (&items[0], &items[1]) {
8542                    (SeqValue::Num(x), SeqValue::Num(y)) => Ok(PaddingVal::Xy(*x, *y)),
8543                    _ => Err(SeqError::new(
8544                        file,
8545                        arg.line,
8546                        1,
8547                        "'padding' to liczba albo [x, y]",
8548                    )),
8549                }
8550            } else {
8551                Err(SeqError::new(
8552                    file,
8553                    arg.line,
8554                    1,
8555                    "'padding' to liczba albo [x, y]",
8556                ))
8557            }
8558        }
8559        _ => Err(SeqError::new(
8560            file,
8561            arg.line,
8562            1,
8563            "'padding' to liczba albo [x, y]",
8564        )),
8565    }
8566}
8567
8568fn padding_err(file: &str, arg: &crate::ir::SeqArg) -> SeqError {
8569    SeqError::new(file, arg.line, 1, "'padding' to liczba albo [x, y]")
8570}
8571
8572fn apply_padding<V>(
8573    mut v: V,
8574    file: &str,
8575    arg: &crate::ir::SeqArg,
8576    res: &Resolver<'_>,
8577    row: Option<&SeqRowData>,
8578) -> Result<V, SeqError>
8579where
8580    V: Padded,
8581{
8582    match padding_val(file, arg, res, row)? {
8583        PaddingVal::All(n) => v = v.set_pad(n),
8584        PaddingVal::Xy(x, y) => v = v.set_pad_xy(x, y),
8585    }
8586    Ok(v)
8587}
8588
8589trait Padded {
8590    fn set_pad(self, n: f32) -> Self;
8591    fn set_pad_xy(self, x: f32, y: f32) -> Self;
8592}
8593
8594impl Padded for Container {
8595    fn set_pad(self, n: f32) -> Self {
8596        self.padding(n)
8597    }
8598    fn set_pad_xy(self, x: f32, y: f32) -> Self {
8599        self.padding_xy(x, y)
8600    }
8601}
8602
8603impl Padded for Panel {
8604    fn set_pad(self, n: f32) -> Self {
8605        self.padding(n)
8606    }
8607    fn set_pad_xy(self, x: f32, y: f32) -> Self {
8608        self.padding_xy(x, y)
8609    }
8610}
8611
8612fn apply_container_margin(
8613    v: Container,
8614    file: &str,
8615    arg: &crate::ir::SeqArg,
8616    res: &Resolver<'_>,
8617    row: Option<&SeqRowData>,
8618) -> Result<Container, SeqError> {
8619    match margin_val(file, arg, res, row)? {
8620        MarginVal::All(n) => Ok(v.margin(n)),
8621        MarginVal::Xy(x, y) => Ok(v.margin_xy(x, y)),
8622    }
8623}
8624
8625fn apply_panel_margin(
8626    v: Panel,
8627    file: &str,
8628    arg: &crate::ir::SeqArg,
8629    res: &Resolver<'_>,
8630    row: Option<&SeqRowData>,
8631) -> Result<Panel, SeqError> {
8632    match margin_val(file, arg, res, row)? {
8633        MarginVal::All(n) => Ok(v.margin(n)),
8634        MarginVal::Xy(x, y) => Ok(v.margin_xy(x, y)),
8635    }
8636}
8637
8638fn color(file: &str, theme: &Theme, arg: &crate::ir::SeqArg) -> Result<Color, SeqError> {
8639    match &arg.value {
8640        SeqValue::Color(hex) => hex_color(file, arg.line, hex),
8641        SeqValue::Theme(field) => theme_color(file, arg.line, theme, field),
8642        _ => Err(SeqError::new(
8643            file,
8644            arg.line,
8645            1,
8646            format!("'{}' to #RRGGBB albo theme.pole", arg.name),
8647        )),
8648    }
8649}
8650
8651fn hex_color(file: &str, line: usize, hex: &str) -> Result<Color, SeqError> {
8652    match u32::from_str_radix(hex, 16) {
8653        Ok(v) if hex.len() == 6 => Ok(Color::hex(v)),
8654        Ok(v) => Ok(Color::hexa(v)),
8655        Err(_) => Err(SeqError::new(file, line, 1, "wrong color hex")),
8656    }
8657}
8658
8659fn theme_color(file: &str, line: usize, theme: &Theme, field: &str) -> Result<Color, SeqError> {
8660    match field {
8661        "bg" | "background" => Ok(theme.background),
8662        "surface" => Ok(theme.surface),
8663        "border" => Ok(theme.border),
8664        "ink" | "text" => Ok(theme.text),
8665        "muted" | "text_secondary" => Ok(theme.text_secondary),
8666        "accent" => Ok(theme.accent),
8667        "disabled" => Ok(theme.disabled),
8668        _ => Err(SeqError::new(
8669            file,
8670            line,
8671            1,
8672            format!("nieznane pole theme '{field}' (bg/surface/border/ink/muted/accent/disabled)"),
8673        )),
8674    }
8675}
8676
8677fn border_pair(
8678    file: &str,
8679    theme: &Theme,
8680    arg: &crate::ir::SeqArg,
8681) -> Result<(f32, Color), SeqError> {
8682    match &arg.value {
8683        SeqValue::Array(items) if items.len() == 2 => {
8684            let w = match &items[0] {
8685                SeqValue::Num(n) => *n,
8686                _ => {
8687                    return Err(SeqError::new(
8688                        file,
8689                        arg.line,
8690                        1,
8691                        "'border' is [width, color]",
8692                    ));
8693                }
8694            };
8695            let c = match &items[1] {
8696                SeqValue::Color(hex) => hex_color(file, arg.line, hex)?,
8697                SeqValue::Theme(f) => theme_color(file, arg.line, theme, f)?,
8698                _ => {
8699                    return Err(SeqError::new(
8700                        file,
8701                        arg.line,
8702                        1,
8703                        "'border' is [width, color]",
8704                    ));
8705                }
8706            };
8707            Ok((w, c))
8708        }
8709        _ => Err(SeqError::new(
8710            file,
8711            arg.line,
8712            1,
8713            "'border' is [width, color]",
8714        )),
8715    }
8716}
8717
8718fn align_val(file: &str, arg: &crate::ir::SeqArg) -> Result<Align, SeqError> {
8719    match ident(file, arg)?.as_str() {
8720        "center" => Ok(Align::Center),
8721        "start" => Ok(Align::Start),
8722        "end" => Ok(Align::End),
8723        "stretch" => Ok(Align::Stretch),
8724        other => Err(SeqError::new(
8725            file,
8726            arg.line,
8727            1,
8728            format!("align to center/start/end/stretch, nie '{other}'"),
8729        )),
8730    }
8731}
8732
8733fn justify_val(file: &str, arg: &crate::ir::SeqArg) -> Result<Justify, SeqError> {
8734    match ident(file, arg)?.as_str() {
8735        "start" => Ok(Justify::Start),
8736        "center" => Ok(Justify::Center),
8737        "end" => Ok(Justify::End),
8738        "between" => Ok(Justify::SpaceBetween),
8739        other => Err(SeqError::new(
8740            file,
8741            arg.line,
8742            1,
8743            format!("justify to start/center/end/between, nie '{other}'"),
8744        )),
8745    }
8746}
8747
8748fn button_style_of(
8749    file: &str,
8750    styles: &BuiltStyles,
8751    node: &SeqNode,
8752) -> Result<Option<ButtonStyle>, SeqError> {
8753    match find_arg(node, "style") {
8754        None => Ok(None),
8755        Some(arg) => match &arg.value {
8756            SeqValue::Ident(name) => {
8757                if let Some(preset) = button_preset(&Some(name.clone())) {
8758                    return Ok(Some(preset));
8759                }
8760                styles.button_style(name).map(Some).ok_or_else(|| {
8761                    SeqError::new(file, arg.line, 1, format!("nieznany styl '{name}'"))
8762                })
8763            }
8764            _ => Err(SeqError::new(file, arg.line, 1, "style: to nazwa stylu")),
8765        },
8766    }
8767}