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