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