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