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