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