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