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