1use serde_json::{json, Map, Value};
8
9use super::{OutEvent, Surface, SurfaceCommand};
10use crate::data::{absolute_path, is_binding, run_action, set_pointer, ResolveContext};
11use crate::layout_tree::fields::{clamp_number_value, dates, number_prop, number_step_precision, parse_time};
12use crate::layout_tree::{FieldErrors, LNode, FORM_FIELDS};
13use crate::types::UiNode;
14
15fn js(v: &Value) -> String {
16 crate::json::to_js_string(v)
17}
18
19pub(crate) fn check_fails(v: Option<&Value>) -> bool {
22 matches!(v, None | Some(Value::Null) | Some(Value::Bool(false))) || v.and_then(Value::as_str) == Some("")
23}
24
25fn suffix_index(id: &str) -> Option<usize> {
27 id.rsplit('.').next().and_then(|s| s.parse().ok())
28}
29
30impl Surface {
31 pub fn scroll(&mut self, id: &str, offset: f32) -> bool {
39 let x = self.scroll_offset(id).0;
40 self.scroll_to(id, x, offset)
41 }
42
43 fn scroll_id(&self, id: &str) -> String {
46 self.slot_of.get(id).map(|&s| self.scroll_key(s)).filter(|k| k != id && self.lists.iter().any(|l| l.id == *k)).unwrap_or_else(|| id.to_string())
47 }
48
49 pub fn scroll_to(&mut self, id: &str, x: f32, y: f32) -> bool {
52 let key = self.scroll_id(id);
53 let id = key.as_str();
54 let (mut x, mut y) = (x.max(0.0), y.max(0.0));
55 if let Some((max_x, max_y)) = self.scroll_max(id) {
56 x = x.min(max_x);
57 y = y.min(max_y);
58 }
59 let current = self.local.scroll.get(id).copied().unwrap_or((0.0, 0.0));
60 if (current.0 - x).abs() < 0.5 && (current.1 - y).abs() < 0.5 {
61 return false;
62 }
63 self.local.scroll.insert(id.to_string(), (x, y));
64 let lists: Vec<String> = self
68 .lists
69 .iter()
70 .filter(|l| l.windowed)
71 .filter(|l| l.id == id || matches!(self.local.list_views.get(&l.id).map(|v| &v.source), Some(crate::layout_tree::ListViewSource::Ancestor { id: a, .. }) if a == id))
72 .map(|l| l.id.clone())
73 .collect();
74 self.subtree_pending.extend(lists);
75 if self.slot_of.get(id).is_some_and(|&s| self.nodes[s as usize].part.as_deref() == Some("list") && matches!(self.nodes[s as usize].owner_component.as_deref(), Some("SearchList" | "Menu"))) {
78 self.needs_build = true;
79 if !self.pending.iter().any(|e| matches!(e, OutEvent::Relayout)) {
80 self.pending.push(OutEvent::Relayout);
81 }
82 }
83 if (self.local.sticky_any || self.lists.iter().any(|l| l.sticky)) && !self.pending.iter().any(|e| matches!(e, OutEvent::Relayout)) {
84 self.pending.push(OutEvent::Relayout);
86 }
87 true
88 }
89
90 pub fn set_surface_scroll(&mut self, x: f32, y: f32) -> bool {
96 if self.local.surface_scroll == Some((x, y)) {
97 return false;
98 }
99 self.local.surface_scroll = Some((x, y));
100 let lists: Vec<String> = self.lists.iter().filter(|l| l.windowed && matches!(self.local.list_views.get(&l.id).map(|v| &v.source), Some(crate::layout_tree::ListViewSource::Host { .. }))).map(|l| l.id.clone()).collect();
101 let any = !lists.is_empty() || self.local.sticky_any;
102 self.subtree_pending.extend(lists);
103 any
104 }
105
106 fn scroll_max(&self, id: &str) -> Option<(f32, f32)> {
111 let slot = self.lists.iter().find(|l| l.id == id).map(|l| l.node).or_else(|| self.slot_of.get(id).copied())?;
112 let s = self.scrolls.iter().find(|s| s.index == slot)?;
113 let f = self.last_frames.get(slot as usize)?;
114 let max = |content: f32, size: f32, on: bool| if on && content > size + 0.5 { content - size } else { f32::INFINITY };
115 Some((max(s.content_width, f.w, s.scroll_x), max(s.content_height, f.h, s.scroll_y)))
116 }
117
118 pub(super) fn scroll_key(&self, slot: u32) -> String {
122 self.lists.iter().find(|l| l.node == slot).map(|l| l.id.clone()).unwrap_or_else(|| self.nodes[slot as usize].id.clone())
123 }
124
125 pub fn scroll_offset(&self, id: &str) -> (f32, f32) {
127 self.local.scroll.get(&self.scroll_id(id)).copied().unwrap_or((0.0, 0.0))
128 }
129
130 pub fn set_open(&mut self, id: &str, open: bool) -> Vec<OutEvent> {
137 self.ensure_reduced();
138 self.local.open.insert(id.to_string(), open);
139 if !open {
140 self.local.submenu.remove(id);
141 self.local.query.remove(id);
142 self.local.range_anchor.remove(id);
143 }
144 self.needs_build = true;
145 let mut out = vec![];
146 let written = self.write_through(id, "open", Value::Bool(open));
147 if !written.is_empty() {
151 self.local.open.remove(id);
152 }
153 out.extend(written);
154 out.extend(self.fire(id, "change", Some(json!({"open": open}))));
155 out.push(OutEvent::Relayout);
156 out
157 }
158
159 fn is_open_now(&self, id: &str) -> bool {
160 self.local.open.get(id).copied().unwrap_or_else(|| self.node_by_id(id).and_then(|o| o.props.get("open").and_then(Value::as_bool)).unwrap_or(false))
161 }
162
163 pub fn dismiss_toast(&mut self, id: &str) -> Vec<OutEvent> {
166 self.ensure_reduced();
167 let mut out = self.fire(id, "dismiss", None);
168 out.extend(self.set_open(id, false));
169 out
170 }
171
172 pub(super) fn node_by_id(&self, id: &str) -> Option<&LNode> {
177 self.slot_of.get(id).map(|s| &self.nodes[*s as usize])
178 }
179
180 fn source_node(&self, id: &str) -> Option<UiNode> {
185 let source = self.node_by_id(id).and_then(|n| n.source.as_ref());
186 let (source_id, tree) = match source {
187 Some(s) => (s.id.as_str(), match &s.template {
188 Some(t) => self.template_cache.get(t)?.as_ref()?,
189 None => self.root.as_ref()?,
190 }),
191 None => (id, self.root.as_ref()?),
192 };
193 let mut found = None;
194 tree.walk(&mut |n| {
195 if found.is_none() && n.id == source_id {
196 found = Some(n.clone());
197 }
198 });
199 found.map(|mut n| {
200 n.id = id.to_string();
201 n
202 })
203 }
204
205 fn instance_suffix(&self, id: &str) -> String {
208 self.node_by_id(id).and_then(|n| n.source.as_ref()).and_then(|s| id.strip_prefix(s.id.as_str())).unwrap_or("").to_string()
209 }
210
211 fn scope_of(&self, id: &str) -> String {
212 self.node_by_id(id).map(|n| n.scope.clone()).unwrap_or_default()
213 }
214
215 fn resolve_ctx<'s>(&'s self, scope: &'s str) -> ResolveContext<'s> {
218 let ctx = ResolveContext::new(&self.data, scope).with_strings(&self.strings).with_formatter(&*self.formatter);
219 let ctx = match self.clock {
220 Some(now) => ctx.with_now(now),
221 None => ctx,
222 };
223 match crate::layout_tree::row_scope_root(scope).and_then(|root| self.seed.row_scopes.get_key_value(root)) {
224 Some((root, row)) => ctx.with_overlay(root, row),
225 None => ctx,
226 }
227 }
228
229 fn binding_path(&self, id: &str, prop: &str) -> Option<String> {
233 let node = self.source_node(id)?;
234 let binding = node.props.get(prop).filter(|v| is_binding(v))?;
235 Some(absolute_path(binding["path"].as_str().unwrap_or(""), &self.scope_of(id))).filter(|p| !p.starts_with("/$row/"))
236 }
237
238 fn write_through(&mut self, id: &str, prop: &str, value: Value) -> Vec<OutEvent> {
240 let Some(path) = self.binding_path(id, prop) else { return vec![] };
241 if set_pointer(&mut self.data, &path, Some(value.clone())).is_err() {
243 return vec![];
244 }
245 self.data_version += 1;
246 self.needs_build = true;
247 vec![OutEvent::DataChanged { path, value }]
248 }
249
250 fn write_field(&mut self, id: &str, prop: &str, value: Value) -> Vec<OutEvent> {
253 let out = self.write_through(id, prop, value.clone());
254 if out.is_empty() {
255 self.local.field_values.insert(id.to_string(), value);
256 self.needs_build = true;
257 } else {
258 self.local.field_values.remove(id);
259 }
260 out
261 }
262
263 pub(super) fn fire(&mut self, id: &str, event: &str, payload: Option<Value>) -> Vec<OutEvent> {
272 let Some(node) = self.source_node(id) else { return vec![] };
273 let Some(action) = node.on.as_ref().and_then(|o| o.get(event)).cloned() else { return vec![] };
274 let mut out = vec![];
275 let scope = self.scope_of(id);
276 let pressed = if event == "rowPress" { payload.as_ref().and_then(|p| p.get("row")).cloned() } else { None };
279 let outcome = {
280 let ctx = self.resolve_ctx(&scope);
281 let ctx = match &pressed {
282 Some(row) => ctx.with_item(row),
283 None => ctx,
284 };
285 run_action(&action, &ctx)
286 };
287 if let Some((path, value)) = outcome.written {
288 self.data = outcome.data;
289 self.data_version += 1;
290 self.needs_build = true;
291 out.push(OutEvent::DataChanged { path, value });
292 }
293 if let Some(ev) = outcome.event {
294 let mut context = match ev.context {
295 Some(Value::Object(m)) => m,
296 _ => Map::new(),
297 };
298 if let Some(Value::Object(p)) = &payload {
299 for (k, v) in p {
300 context.insert(k.clone(), v.clone());
301 }
302 }
303 out.push(OutEvent::Action { name: ev.name, component_id: id.to_string(), event: event.to_string(), context: Value::Object(context), payload });
304 }
305 if let Some(call) = outcome.call {
306 if call.call == "openUrl" {
307 if let Some(url) = call.args.get("url").and_then(Value::as_str) {
308 out.push(OutEvent::OpenUrl { url: url.to_string() });
309 }
310 } else if !call.call.is_empty() && (call.call == "bind" || !crate::host::is_builtin_function(&call.call)) {
311 out.push(OutEvent::FunctionCall { component_id: id.to_string(), name: call.call, args: Value::Object(call.args) });
315 }
316 }
317 out
318 }
319
320 pub fn event(&mut self, index: u32, event: &str, payload: Option<Value>) -> Vec<OutEvent> {
329 self.ensure_reduced();
330 if !self.live.get(index as usize).copied().unwrap_or(false) {
331 return vec![];
332 }
333 let n = self.nodes[index as usize].clone();
334 if !matches!(event, "dismiss" | "blur" | "reset") && self.is_inert(&n) {
338 return vec![];
339 }
340 if event == "contextmenu" {
341 return self.context_menu(&n, payload);
342 }
343 if event == "press" && n.component == "Button" && n.part.is_none() && n.props.get("hold").and_then(Value::as_bool) == Some(true) {
345 return vec![];
346 }
347 if event == "press" {
348 if let Some(target) = n.trigger_for.clone() {
349 let mut out = self.fire(&n.id, "press", None);
350 let open = !self.is_open_now(&target);
351 out.extend(self.set_open(&target, open));
352 return out;
353 }
354 }
355 let owner_id = n.owner.clone().unwrap_or_else(|| n.id.clone());
356 let owner_component = n.owner_component.clone().unwrap_or_else(|| n.component.clone());
357 let part = n.part.clone();
358 let mut out = vec![];
359 if event == "press" && n.part.is_none() && matches!(n.component.as_str(), "Box" | "Button") {
362 if let Some(url) = n.props.get("href").and_then(Value::as_str).filter(|url| !url.is_empty()) {
363 out.push(OutEvent::OpenUrl { url: url.to_string() });
364 }
365 }
366 match (owner_component.as_str(), part.as_deref(), event) {
367 ("Tabs", Some("tab"), "press") => {
368 let value = n.props.get("value").map(js).unwrap_or_default();
369 self.local.tabs.insert(owner_id.clone(), value.clone());
370 self.subtree_pending.insert(owner_id.clone());
371 out.extend(self.write_through(&owner_id, "value", Value::String(value.clone())));
372 out.extend(self.fire(&owner_id, "change", Some(json!({"value": value}))));
373 out.push(OutEvent::Relayout);
374 }
375 ("Accordion", Some("trigger"), "press") => {
376 let Some(owner) = self.node_by_id(&owner_id).cloned() else { return out };
377 let value = n.props.get("value").map(js).unwrap_or_default();
378 let multiple = owner.props.get("type").and_then(Value::as_str) == Some("multiple");
379 let mut open = self.local.accordion.get(&owner_id).cloned().unwrap_or_else(|| owner.props.get("value").map(js).map(|v| v.split(',').filter(|s| !s.is_empty()).map(str::to_string).collect()).unwrap_or_default());
380 if open.contains(&value) {
381 open.retain(|v| *v != value);
382 } else if multiple {
383 open.push(value.clone());
384 } else {
385 open = vec![value.clone()];
386 }
387 let joined = open.join(",");
388 self.local.accordion.insert(owner_id.clone(), open);
389 self.subtree_pending.insert(owner_id.clone());
390 out.extend(self.write_through(&owner_id, "value", Value::String(joined.clone())));
391 out.extend(self.fire(&owner_id, "change", Some(json!({"value": joined}))));
392 out.push(OutEvent::Relayout);
393 }
394 ("Carousel", _, "change") | ("Carousel", Some("previous" | "next" | "thumbnail"), "press") => {
395 let page = if event == "press" { n.props.get("target").and_then(Value::as_i64) } else { payload.as_ref().and_then(|p| p.get("page")).and_then(Value::as_i64) }.unwrap_or(0);
397 self.local.carousel.insert(owner_id.clone(), page);
398 self.subtree_pending.insert(owner_id.clone());
399 out.extend(self.write_through(&owner_id, "page", json!(page)));
400 out.extend(self.fire(&owner_id, "change", Some(json!({"page": page}))));
401 out.push(OutEvent::Relayout);
402 }
403 ("Menu", Some("item"), "press") => out.extend(self.menu_select(&n, &owner_id)),
404 ("Toast", Some("action"), "press") => {
405 out.extend(self.fire(&owner_id, "action", None));
406 out.extend(self.dismiss_toast(&owner_id));
407 }
408 ("Toast", Some("close"), "press") | ("Toast", _, "dismiss") => out.extend(self.dismiss_toast(&owner_id)),
409 ("Dialog" | "Drawer" | "Popover" | "Tooltip" | "Menu", part, "press") if part.is_some() || n.id == owner_id => {
410 if part == Some("close") {
411 out.extend(self.set_open(&owner_id, false));
412 } else if part.is_none() || part == Some("trigger") {
413 let open = !self.is_open_now(&owner_id);
414 out.extend(self.set_open(&owner_id, open));
415 }
416 }
417 (_, _, "dismiss") => {
418 let dismissible = self.layers.iter().find(|l| l.owner == owner_id).map(|l| l.dismissible).unwrap_or(true);
420 if dismissible {
421 out.extend(self.set_open(&owner_id, false));
422 }
423 }
424 ("Dialog" | "Drawer" | "Popover" | "Tooltip" | "Menu" | "Select" | "DatePicker" | "DateRangePicker" | "TimePicker", _, "change") if payload.as_ref().and_then(|p| p.get("open")).is_some() => {
425 let open = payload.as_ref().and_then(|p| p.get("open")).and_then(Value::as_bool).unwrap_or(false);
426 out.extend(self.set_open(&owner_id, open));
427 }
428 ("Select", Some("item"), "press") => out.extend(self.select_option(&n, &owner_id)),
429 ("Tabs", Some("close"), "press") => {
431 let value = n.props.get("value").cloned().unwrap_or(Value::Null);
432 out.extend(self.fire(&owner_id, "close", Some(json!({"value": value}))));
433 }
434 ("Tabs", Some("new"), "press") => out.extend(self.fire(&owner_id, "new", None)),
435 ("Tabs", Some("more"), "press") => out.extend(self.tab_more(&owner_id)),
437 ("Tabs", Some("tab"), "drag") if self.is_tab_strip(&owner_id) => out.extend(self.tab_drag(&n.id, &owner_id, payload.as_ref())),
438 ("Tabs", Some("tab"), "key") if self.is_tab_strip(&owner_id) => out.extend(self.tab_key(&n.id, &owner_id, payload.as_ref())),
439 ("SearchList", Some("search"), "change") => {
441 let query = payload.as_ref().and_then(|p| p.get("value")).map(js).unwrap_or_default();
442 out.extend(self.search_query(&owner_id, &owner_id, &format!("{owner_id}.list"), query.clone()));
443 out.extend(self.write_through(&owner_id, "query", Value::String(query.clone())));
444 out.extend(self.fire(&owner_id, "search", Some(json!({"query": query}))));
445 }
446 ("SearchList", Some("search"), "key") => {
447 let key = payload.as_ref().and_then(|p| p.get("key")).map(js).unwrap_or_default();
448 out.extend(self.search_key(&owner_id, &owner_id, &format!("{owner_id}.list"), &key));
449 }
450 ("SearchList", Some("item"), "press") => out.extend(self.search_pick(&n, &owner_id)),
451 ("Menu", Some("search"), "change") => {
452 let query = payload.as_ref().and_then(|p| p.get("value")).map(js).unwrap_or_default();
453 let list = format!("{owner_id}.list{}", n.id.get(owner_id.len() + ".search".len()..).unwrap_or_default());
454 out.extend(self.search_query(&n.id, &owner_id, &list, query));
455 }
456 ("Menu", Some("search"), "key") => {
457 let key = payload.as_ref().and_then(|p| p.get("key")).map(js).unwrap_or_default();
458 if key == "ArrowLeft" {
459 self.local.submenu.remove(&owner_id);
461 self.needs_build = true;
462 out.push(OutEvent::Relayout);
463 } else {
464 let list = format!("{owner_id}.list{}", n.id.get(owner_id.len() + ".search".len()..).unwrap_or_default());
465 out.extend(self.search_key(&n.id, &owner_id, &list, &key));
466 }
467 }
468 ("Select", Some("search"), "change") => {
469 let query = payload.as_ref().and_then(|p| p.get("value")).map(js).unwrap_or_default();
470 self.local.query.insert(owner_id.clone(), query.clone());
471 self.needs_build = true;
472 out.extend(self.fire(&owner_id, "search", Some(json!({"query": query}))));
473 out.push(OutEvent::Relayout);
474 }
475 ("DatePicker" | "DateRangePicker", Some("previous" | "next"), "press") => {
476 let delta = if part.as_deref() == Some("next") { 1 } else { -1 };
477 let current = self.calendar_month(&owner_id);
478 let (y, m) = dates::shift_month(current.0, current.1, delta);
479 self.local.month.insert(owner_id.clone(), (y, m));
480 self.needs_build = true;
481 if owner_component == "DatePicker" {
483 out.extend(self.fire(&owner_id, "month", Some(json!({"month": format!("{y:04}-{m:02}")}))));
484 }
485 out.push(OutEvent::Relayout);
486 }
487 ("DatePicker", Some("day"), "press") => {
488 let date = n.props.get("date").map(js).unwrap_or_default();
489 out.extend(self.write_field(&owner_id, "value", Value::String(date.clone())));
490 out.extend(self.fire(&owner_id, "change", Some(json!({"value": date}))));
491 if self.node_by_id(&owner_id).is_some_and(|o| o.props.get("inline").and_then(Value::as_bool) == Some(true)) {
493 self.needs_build = true;
494 out.push(OutEvent::Relayout);
495 } else {
496 out.extend(self.set_open(&owner_id, false));
497 }
498 }
499 ("DateRangePicker", Some("day"), "press") => out.extend(self.range_pick(&n, &owner_id)),
500 ("TimePicker", Some("item"), "press") => {
501 let value = n.props.get("value").map(js).unwrap_or_default();
502 out.extend(self.write_field(&owner_id, "value", Value::String(value.clone())));
503 out.extend(self.fire(&owner_id, "change", Some(json!({"value": value}))));
504 out.extend(self.set_open(&owner_id, false));
505 }
506 ("NumberField", Some("decrement" | "increment"), "press") => {
507 let up = part.as_deref() == Some("increment");
508 out.extend(self.number_step(&owner_id, up));
509 }
510 ("NumberField", _, "submit") => {
511 out.extend(self.fire(&owner_id, "submit", payload.clone()));
512 if let Some(form) = n.form.clone() {
513 out.extend(self.submit_form(&form));
514 }
515 }
516 ("NumberField", _, "change" | "commit") => {
517 let raw = payload.as_ref().and_then(|p| p.get("value")).cloned().unwrap_or(Value::Null);
521 let parsed = match &raw {
522 Value::Number(_) => raw.as_f64(),
523 Value::String(s) => match crate::format::parse_number(self.formatter.as_ref(), s) {
524 None if !s.trim().is_empty() => return out,
525 v => v,
526 },
527 _ => None,
528 };
529 let value = parsed.map(|v| if event == "commit" { self.clamp_number(&owner_id, v) } else { v });
530 let v = value.map(crate::json::number).unwrap_or(Value::Null);
531 self.local.errors.remove(&owner_id);
532 out.extend(self.write_field(&owner_id, "value", v.clone()));
533 out.extend(self.fire(&owner_id, "change", Some(json!({"value": v}))));
534 out.push(OutEvent::Relayout);
535 }
536 ("ChipInput", Some("input"), "change") => {
537 let text = payload.as_ref().and_then(|p| p.get("value")).map(js).unwrap_or_default();
538 if let Some(entry) = text.strip_suffix(',') {
539 out.extend(self.chip_add(&owner_id, entry.trim()));
540 } else {
541 self.local.query.insert(owner_id.clone(), text);
542 self.needs_build = true;
543 out.push(OutEvent::Relayout);
544 }
545 }
546 ("ChipInput", Some("input"), "submit" | "commit") => {
547 let text = payload.as_ref().and_then(|p| p.get("value")).map(js).unwrap_or_default();
548 out.extend(self.chip_add(&owner_id, text.trim()));
549 }
550 ("ChipInput", Some("suggestion"), "press") => {
551 let value = n.props.get("value").map(js).unwrap_or_default();
552 out.extend(self.chip_add(&owner_id, &value));
553 }
554 ("ChipInput", Some("remove"), "press") => {
555 let value = n.props.get("value").map(js).unwrap_or_default();
556 out.extend(self.chip_remove(&owner_id, &value));
557 }
558 ("FileUpload", Some("dropzone" | "browse"), "press") => {
559 let owner = self.node_by_id(&owner_id).cloned();
560 let accept = owner.as_ref().and_then(|o| o.props.get("accept")).and_then(Value::as_str).map(str::to_string);
561 let multiple = owner.as_ref().and_then(|o| o.props.get("multiple")).and_then(Value::as_bool).unwrap_or(false);
562 out.push(OutEvent::PickFiles { component_id: owner_id.clone(), accept, multiple });
563 }
564 ("FileUpload", _, "upload") => out.extend(self.files_upload(&owner_id, payload)),
565 ("FileUpload", Some("remove"), "press") => {
566 let i = suffix_index(&n.id).unwrap_or(0);
567 out.extend(self.file_remove(&owner_id, i));
568 }
569 ("Table", Some("headerCell"), "press") => out.extend(self.table_sort(&n, &owner_id)),
570 ("Table", Some("row"), "press") => out.extend(self.table_row(&n, &owner_id)),
571 ("Table", Some("checkbox"), "press") => out.extend(self.table_check(&n, &owner_id)),
572 ("CodeBlock", Some("copy"), "press") => {
573 let owner = self.node_by_id(&owner_id);
575 let raw = owner.and_then(|o| o.props.get("code")).map(js).unwrap_or_default();
576 let language = owner.and_then(|o| o.props.get("language")).and_then(Value::as_str).unwrap_or("plain").to_string();
577 let code = crate::code::code_text(&raw, &language);
578 self.local.copied.insert(owner_id.clone(), true);
579 self.subtree_pending.insert(owner_id.clone());
580 out.push(OutEvent::Copy { text: code });
581 out.push(OutEvent::Announce { text: self.strings.get("copied").cloned().unwrap_or_default(), live: "polite".into() });
582 out.push(OutEvent::Relayout);
583 }
584 ("CodeBlock", Some("copy"), "reset") => {
585 self.local.copied.remove(&owner_id);
586 self.subtree_pending.insert(owner_id.clone());
587 out.push(OutEvent::Relayout);
588 }
589 ("Resizable", Some("handle"), "drag" | "key") => out.extend(self.resize_event(&n, &owner_id, event, payload.as_ref())),
590 ("Checkbox" | "Switch", _, "press" | "change") => {
591 let current = self.current_prop(&owner_id, "checked").and_then(|v| v.as_bool()).unwrap_or(false);
592 let checked = payload.as_ref().and_then(|p| p.get("checked")).and_then(Value::as_bool).unwrap_or(!current);
593 self.local.errors.remove(&owner_id);
594 out.extend(self.write_field(&owner_id, "checked", Value::Bool(checked)));
595 out.extend(self.fire(&owner_id, "change", Some(json!({"checked": checked}))));
596 out.push(OutEvent::Relayout);
597 }
598 ("Radio", Some("item" | "dot" | "label"), "press") => {
599 let suffix = n.id.rsplit('.').next().unwrap_or_default();
602 let row_id = format!("{owner_id}.item.{suffix}");
603 let value = self.node_by_id(&row_id).and_then(|row| row.props.get("value").cloned()).unwrap_or(Value::Null);
604 self.local.errors.remove(&owner_id);
605 out.extend(self.write_field(&owner_id, "value", value.clone()));
606 out.extend(self.fire(&owner_id, "change", Some(json!({"value": value}))));
607 out.push(OutEvent::Relayout);
608 }
609 ("ChoiceTiles", Some("tile" | "picture" | "label" | "description"), "press") => {
610 let suffix = n.id.rsplit('.').next().unwrap_or_default();
614 let tile = self.node_by_id(&format!("{owner_id}.tile.{suffix}")).cloned();
615 let Some(tile) = tile.filter(|t| t.props.get("disabled").and_then(Value::as_bool) != Some(true)) else { return out };
616 let value = tile.props.get("value").map(js).unwrap_or_default();
617 let mut props = self.node_by_id(&owner_id).map(|o| o.props.clone()).unwrap_or_default();
618 if let Some(current) = self.current_prop(&owner_id, "value") {
619 props.insert("value".into(), current);
620 }
621 let next = Value::String(crate::layout_tree::fields::choice_toggle(&props, &value));
622 self.local.errors.remove(&owner_id);
623 out.extend(self.write_field(&owner_id, "value", next.clone()));
624 out.extend(self.fire(&owner_id, "change", Some(json!({"value": next}))));
625 out.push(OutEvent::Relayout);
626 }
627 ("Toggle", _, "press") => {
628 let current = self.local.pressed_toggles.get(&owner_id).copied().unwrap_or_else(|| n.props.get("pressed").and_then(Value::as_bool).unwrap_or(false));
629 self.local.pressed_toggles.insert(owner_id.clone(), !current);
630 self.subtree_pending.insert(owner_id.clone());
631 out.extend(self.write_through(&owner_id, "pressed", Value::Bool(!current)));
632 out.extend(self.fire(&owner_id, "change", Some(json!({"pressed": !current}))));
633 out.push(OutEvent::Relayout);
634 }
635 ("Link", _, "press") => {
636 if let Some(url) = n.props.get("href").and_then(Value::as_str) {
637 out.push(OutEvent::OpenUrl { url: url.to_string() });
638 }
639 out.extend(self.fire(&owner_id, "press", None));
640 }
641 ("Button", None, "press") if n.props.get("submit").and_then(Value::as_bool) == Some(true) => {
642 out.extend(self.fire(&owner_id, "press", None));
643 if let Some(form) = n.form.clone() {
644 out.extend(self.submit_form(&form));
645 }
646 }
647 ("Input" | "Textarea" | "Select" | "DatePicker" | "Slider" | "Composer" | "Segmented" | "ChoiceTiles" | "TimePicker" | "DateRangePicker", _, "change" | "commit" | "submit") => {
648 let value = payload.as_ref().and_then(|p| p.get("value")).cloned().unwrap_or(Value::Null);
649 let name = n.props.get("name").or_else(|| self.node_by_id(&owner_id).and_then(|o| o.props.get("name"))).and_then(Value::as_str).unwrap_or(&owner_id).to_string();
650 let path = self.binding_path(&owner_id, "value");
651 if event != "submit" || self.current_prop(&owner_id, "value").as_ref() != Some(&value) {
654 out.extend(self.write_field(&owner_id, "value", value.clone()));
655 if event == "change" && (self.validate_on(&owner_id) == "change" || self.local.errors.contains_key(&owner_id)) {
658 self.validate_field(&owner_id);
659 }
660 }
661 out.push(OutEvent::Input { component_id: owner_id.clone(), name, path, value: value.clone(), commit: event != "change" });
662 out.extend(self.fire(&owner_id, event, Some(json!({"value": value}))));
663 if event == "submit" && owner_component != "Textarea" {
665 if let Some(form) = n.form.clone() {
666 out.extend(self.submit_form(&form));
667 }
668 }
669 }
670 ("ZoomableImage", None, "zoom") => {
673 if let Some(scale) = payload.as_ref().and_then(|p| p.get("scale")).filter(|s| s.is_number()).cloned() {
674 out.extend(self.write_through(&owner_id, "scale", scale));
675 }
676 out.extend(self.fire(&owner_id, "zoom", payload));
677 }
678 ("Form", _, "submit") => out.extend(self.submit_form(&owner_id)),
679 ("Input" | "Textarea" | "NumberField" | "Select" | "DatePicker" | "TimePicker" | "ChipInput", _, "blur") => {
680 if self.validate_on(&owner_id) == "blur" {
681 self.validate_field(&owner_id);
682 out.push(OutEvent::Relayout);
683 }
684 out.extend(self.fire(&owner_id, "blur", None));
685 }
686 _ => {
687 out.extend(self.fire(&owner_id, event, payload));
688 }
689 }
690 out
691 }
692
693 pub fn hold(&mut self, index: u32, input: &crate::hold::HoldInput) -> Vec<OutEvent> {
703 self.ensure_reduced();
704 if !self.live.get(index as usize).copied().unwrap_or(false) {
705 return vec![];
706 }
707 let n = self.nodes[index as usize].clone();
708 if n.component != "Button" || n.part.is_some() || n.props.get("hold").and_then(Value::as_bool) != Some(true) {
709 return vec![];
710 }
711 if self.is_inert(&n) {
712 if self.local.holds.remove(&n.id).is_some_and(|t| t.active()) {
714 self.subtree_pending.insert(n.id.clone());
715 return vec![OutEvent::Relayout];
716 }
717 return vec![];
718 }
719 let frame = self.last_frame(index).unwrap_or_default();
720 let tracker = self.local.holds.entry(n.id.clone()).or_default();
721 let Some(fired) = tracker.input(input, frame.w, frame.h) else { return vec![] };
722 if !tracker.active() {
723 self.local.holds.remove(&n.id);
724 }
725 self.subtree_pending.insert(n.id.clone());
726 let mut out = self.fire(&n.id, fired.name(), None);
727 out.push(OutEvent::Relayout);
728 out
729 }
730
731 pub fn is_held(&self, id: &str) -> bool {
733 self.local.holds.get(id).is_some_and(crate::hold::HoldTracker::active)
734 }
735
736 fn is_inert(&self, n: &LNode) -> bool {
737 let flagged = |p: &crate::types::Props| p.get("disabled").and_then(Value::as_bool) == Some(true) || p.get("loading").and_then(Value::as_bool) == Some(true);
738 if flagged(&n.props) || self.states.get(&n.id).is_some_and(|s| s.iter().any(|x| x == "disabled")) {
739 return true;
740 }
741 n.owner.as_ref().and_then(|o| self.node_by_id(o)).is_some_and(|o| flagged(&o.props))
742 }
743
744 fn context_menu(&mut self, n: &LNode, payload: Option<Value>) -> Vec<OutEvent> {
745 let mut cur = Some(n.index);
747 while let Some(c) = cur {
748 let node = &self.nodes[c as usize];
749 if crate::layout_tree::is_context_menu(&node.component, &node.props) {
750 let id = node.id.clone();
751 let x = payload.as_ref().and_then(|p| p.get("x")).and_then(Value::as_f64);
752 let y = payload.as_ref().and_then(|p| p.get("y")).and_then(Value::as_f64);
753 match (x, y) {
754 (Some(x), Some(y)) => {
755 self.local.context_point.insert(id.clone(), (x as f32, y as f32));
756 }
757 _ => {
758 self.local.context_point.remove(&id);
759 }
760 }
761 return self.set_open(&id, true);
762 }
763 cur = node.parent;
764 }
765 vec![]
766 }
767
768 fn menu_select(&mut self, n: &LNode, owner_id: &str) -> Vec<OutEvent> {
771 let kind = n.props.get("kind").and_then(Value::as_str).unwrap_or("item").to_string();
772 let path: Vec<usize> = n.id.strip_prefix(&format!("{owner_id}.item.")).map(|s| s.split('.').filter_map(|p| p.parse().ok()).collect()).unwrap_or_default();
773 let mut out = vec![];
774 if kind == "submenu" {
775 let i = path.first().copied().unwrap_or(0);
776 if self.local.submenu.get(owner_id) == Some(&i) {
777 self.local.submenu.remove(owner_id);
778 } else {
779 self.local.submenu.insert(owner_id.to_string(), i);
780 }
781 self.needs_build = true;
782 out.push(OutEvent::Relayout);
783 return out;
784 }
785 let value = n.props.get("value").cloned().unwrap_or(Value::Null);
786 if kind == "checkbox" {
787 let checked = !n.props.get("checked").and_then(Value::as_bool).unwrap_or(false);
788 if let Some(source) = self.source_node(owner_id) {
790 let mut item = source.props.get("items");
791 for (depth, i) in path.iter().enumerate() {
792 let at = item.and_then(|a| a.get(*i));
793 item = if depth + 1 < path.len() { at.and_then(|x| x.get("items")) } else { at };
794 }
795 if let Some(binding) = item.and_then(|i| i.get("checked")).filter(|b| is_binding(b)) {
796 let p = absolute_path(binding["path"].as_str().unwrap_or(""), &self.scope_of(owner_id));
797 if set_pointer(&mut self.data, &p, Some(Value::Bool(checked))).is_ok() {
798 self.data_version += 1;
799 self.needs_build = true;
800 out.push(OutEvent::DataChanged { path: p, value: Value::Bool(checked) });
801 }
802 }
803 }
804 out.extend(self.fire(owner_id, "select", Some(json!({"value": value, "checked": checked}))));
805 } else {
808 out.extend(self.fire(owner_id, "select", Some(json!({"value": value}))));
809 out.extend(self.set_open(owner_id, false));
810 }
811 out
812 }
813
814 fn search_query(&mut self, key: &str, owner_id: &str, list_id: &str, query: String) -> Vec<OutEvent> {
818 let _ = owner_id;
819 self.local.query.insert(key.to_string(), query);
820 self.local.active.remove(key);
821 self.local.scroll.remove(list_id);
822 self.needs_build = true;
823 vec![OutEvent::Relayout]
824 }
825
826 fn search_rows(&self, list_id: &str) -> Vec<u32> {
829 let Some(&list) = self.slot_of.get(list_id) else { return vec![] };
830 self.nodes[list as usize].children.iter().copied().filter(|&c| self.live.get(c as usize).copied().unwrap_or(false) && self.nodes[c as usize].part.as_deref() == Some("item")).collect()
831 }
832
833 fn search_key(&mut self, key: &str, owner_id: &str, list_id: &str, pressed: &str) -> Vec<OutEvent> {
837 let rows = self.search_rows(list_id);
838 if rows.is_empty() {
839 return vec![];
840 }
841 let active = self.local.active.get(key).copied().unwrap_or(0).min(rows.len() - 1);
842 match pressed {
843 "ArrowDown" | "ArrowUp" => {
844 let next = if pressed == "ArrowDown" { (active + 1).min(rows.len() - 1) } else { active.saturating_sub(1) };
845 if next == active {
846 return vec![];
847 }
848 self.local.active.insert(key.to_string(), next);
849 let slot = rows[next];
851 if let (Some(&list), Some(row)) = (self.slot_of.get(list_id), self.last_frames.get(slot as usize).copied()) {
852 if let Some(view) = self.last_frames.get(list as usize).copied() {
853 let offset = self.scroll_offset(list_id).1;
854 let reveal = crate::layout_tree::search::reveal_offset(next, row.h, view.h, offset);
855 if (reveal - offset).abs() > 0.5 {
856 self.local.scroll.insert(list_id.to_string(), (0.0, reveal));
857 }
858 }
859 }
860 self.needs_build = true;
861 vec![OutEvent::Relayout]
862 }
863 "Enter" | "Tab" => {
864 let n = self.nodes[rows[active] as usize].clone();
865 if n.props.get("disabled").and_then(Value::as_bool) == Some(true) {
866 return vec![];
867 }
868 if self.nodes[rows[active] as usize].owner_component.as_deref() == Some("Menu") {
869 self.menu_select(&n, owner_id)
870 } else {
871 self.search_pick(&n, owner_id)
872 }
873 }
874 _ => vec![],
875 }
876 }
877
878 fn search_pick(&mut self, n: &LNode, owner_id: &str) -> Vec<OutEvent> {
881 let value = n.props.get("value").cloned().unwrap_or(Value::Null);
882 let mut out = self.write_field(owner_id, "value", value.clone());
883 out.extend(self.fire(owner_id, "select", Some(json!({"value": value}))));
884 out.push(OutEvent::Relayout);
885 out
886 }
887
888 fn select_option(&mut self, n: &LNode, owner_id: &str) -> Vec<OutEvent> {
889 let v = n.props.get("value").map(js).unwrap_or_default();
890 let owner = self.node_by_id(owner_id).cloned();
891 let multiple = owner.as_ref().and_then(|o| o.props.get("multiple")).and_then(Value::as_bool).unwrap_or(false);
892 let value = if multiple {
893 let current = self.current_prop(owner_id, "value").filter(|v| !v.is_null()).map(|v| js(&v)).unwrap_or_default();
894 let mut set: Vec<String> = current.split(',').filter(|s| !s.is_empty()).map(str::to_string).collect();
895 if set.contains(&v) {
896 set.retain(|x| *x != v);
897 } else {
898 set.push(v.clone());
899 }
900 set.join(",")
901 } else {
902 if self.current_prop(owner_id, "value").filter(|v| !v.is_null()).map(|v| js(&v)).unwrap_or_default() == v {
905 return self.set_open(owner_id, false);
906 }
907 v
908 };
909 self.local.errors.remove(owner_id);
910 let mut out = self.write_field(owner_id, "value", Value::String(value.clone()));
911 out.extend(self.fire(owner_id, "change", Some(json!({"value": value}))));
912 if multiple {
913 out.push(OutEvent::Relayout);
914 } else {
915 out.extend(self.set_open(owner_id, false));
916 }
917 out
918 }
919
920 fn calendar_month(&self, id: &str) -> (i32, u32) {
921 if let Some(m) = self.local.month.get(id) {
922 return *m;
923 }
924 let owner = self.node_by_id(id);
925 let start = owner.and_then(|o| o.props.get("value").or_else(|| o.props.get("start"))).and_then(Value::as_str).and_then(dates::parse);
926 let prop = |k: &str| owner.and_then(|o| o.props.get(k)).and_then(Value::as_str).and_then(dates::parse).map(|(y, m, _)| (y, m));
928 start
929 .map(|(y, m, _)| (y, m))
930 .or_else(|| prop("today"))
931 .or_else(|| self.settings.today.as_deref().and_then(dates::parse).map(|(y, m, _)| (y, m)))
932 .or_else(|| prop("min"))
933 .unwrap_or((2026, 1))
934 }
935
936 fn range_pick(&mut self, n: &LNode, owner_id: &str) -> Vec<OutEvent> {
939 let date = n.props.get("date").map(js).unwrap_or_default();
940 let mut out = vec![];
941 match self.local.range_anchor.remove(owner_id) {
942 None => {
943 self.local.range_anchor.insert(owner_id.to_string(), date);
944 self.needs_build = true;
945 out.push(OutEvent::Relayout);
946 }
947 Some(first) => {
948 let (start, end) = if first <= date { (first, date) } else { (date, first) };
949 let a = self.write_through(owner_id, "start", Value::String(start.clone()));
950 let b = self.write_through(owner_id, "end", Value::String(end.clone()));
951 if a.is_empty() && b.is_empty() {
952 self.local.field_values.insert(owner_id.to_string(), json!({"start": start, "end": end}));
953 }
954 out.extend(a);
955 out.extend(b);
956 out.extend(self.fire(owner_id, "change", Some(json!({"start": start, "end": end}))));
957 out.extend(self.set_open(owner_id, false));
958 }
959 }
960 out
961 }
962
963 fn clamp_number(&self, id: &str, v: f64) -> f64 {
966 let Some(owner) = self.node_by_id(id) else { return v };
967 let (_, precision) = number_step_precision(&owner.props);
968 clamp_number_value(v, number_prop(&owner.props, "min"), number_prop(&owner.props, "max"), precision)
969 }
970
971 fn number_step(&mut self, id: &str, up: bool) -> Vec<OutEvent> {
974 let owner = self.node_by_id(id).cloned();
975 let (step, _) = owner.as_ref().map(|o| number_step_precision(&o.props)).unwrap_or((1.0, 0));
976 let min = owner.as_ref().and_then(|o| number_prop(&o.props, "min"));
977 let value = self.current_prop(id, "value").filter(|v| !v.is_null() && v.as_str() != Some("")).map(|v| match v {
978 Value::Number(n) => n.as_f64().unwrap_or(0.0),
979 other => crate::json::to_number(&other),
980 });
981 let current = value.unwrap_or_else(|| min.filter(|m| *m > 0.0).unwrap_or(0.0));
982 let next = self.clamp_number(id, if up { current + step } else { current - step });
983 let v = crate::json::number(next);
984 self.local.errors.remove(id);
985 let mut out = self.write_field(id, "value", v.clone());
986 out.extend(self.fire(id, "change", Some(json!({"value": v}))));
987 out.push(OutEvent::Relayout);
988 out
989 }
990
991 fn current_prop(&self, id: &str, prop: &str) -> Option<Value> {
995 if let Some(path) = self.binding_path(id, prop) {
996 let scope = self.scope_of(id);
997 return self.resolve_ctx(&scope).read(&path).cloned();
998 }
999 let local = match prop {
1000 "values" => self.local.chips.get(id).map(|v| Value::Array(v.iter().cloned().map(Value::String).collect())),
1001 "files" => self.local.files.get(id).map(|f| Value::Array(f.clone())),
1002 "selected" => self.local.selected.get(id).map(|v| Value::Array(v.iter().cloned().map(Value::String).collect())),
1003 "sort" => self.local.sort.get(id).map(|(k, d)| json!({"key": k, "direction": d})),
1004 "checked" | "value" => self.local.field_values.get(id).cloned(),
1005 _ => None,
1006 };
1007 local.or_else(|| self.node_by_id(id).and_then(|o| o.props.get(prop)).cloned())
1008 }
1009
1010 fn chip_values(&self, id: &str) -> Vec<String> {
1011 self.current_prop(id, "values").and_then(|v| v.as_array().map(|a| a.iter().map(js).collect())).unwrap_or_default()
1012 }
1013
1014 fn chips_write(&mut self, id: &str, values: Vec<String>) -> Vec<OutEvent> {
1015 let value = Value::Array(values.iter().cloned().map(Value::String).collect());
1016 let out = self.write_through(id, "values", value);
1017 if out.is_empty() {
1018 self.local.chips.insert(id.to_string(), values);
1019 } else {
1020 self.local.chips.remove(id);
1021 }
1022 self.needs_build = true;
1023 out
1024 }
1025
1026 fn chip_add(&mut self, id: &str, value: &str) -> Vec<OutEvent> {
1027 self.local.query.remove(id);
1028 self.needs_build = true;
1029 let mut values = self.chip_values(id);
1030 let max = self.node_by_id(id).and_then(|o| o.props.get("max")).and_then(Value::as_f64);
1031 if value.is_empty() || values.iter().any(|v| v == value) || max.is_some_and(|m| values.len() as f64 >= m) {
1032 return vec![OutEvent::Relayout];
1033 }
1034 values.push(value.to_string());
1035 let mut out = self.chips_write(id, values.clone());
1036 out.extend(self.fire(id, "add", Some(json!({"value": value}))));
1037 out.extend(self.fire(id, "change", Some(json!({"values": values}))));
1038 out.push(OutEvent::Relayout);
1039 out
1040 }
1041
1042 fn chip_remove(&mut self, id: &str, value: &str) -> Vec<OutEvent> {
1043 let mut values = self.chip_values(id);
1044 values.retain(|v| v != value);
1045 let mut out = self.chips_write(id, values.clone());
1046 out.extend(self.fire(id, "remove", Some(json!({"value": value}))));
1047 out.extend(self.fire(id, "change", Some(json!({"values": values}))));
1048 out.push(OutEvent::Relayout);
1049 out
1050 }
1051
1052 fn files_of(&self, id: &str) -> Vec<Value> {
1053 self.current_prop(id, "files").and_then(|v| v.as_array().cloned()).unwrap_or_default()
1054 }
1055
1056 fn files_write(&mut self, id: &str, files: Vec<Value>) -> Vec<OutEvent> {
1057 let out = self.write_through(id, "files", Value::Array(files.clone()));
1058 if out.is_empty() {
1059 self.local.files.insert(id.to_string(), files);
1060 } else {
1061 self.local.files.remove(id);
1062 }
1063 self.needs_build = true;
1064 out
1065 }
1066
1067 fn files_upload(&mut self, id: &str, payload: Option<Value>) -> Vec<OutEvent> {
1071 let incoming: Vec<Value> = payload.as_ref().and_then(|p| p.get("files")).and_then(Value::as_array).cloned().unwrap_or_default();
1072 let owner = self.node_by_id(id).cloned();
1073 let max = owner.as_ref().and_then(|o| o.props.get("maxSize")).and_then(Value::as_f64);
1074 let multiple = owner.as_ref().and_then(|o| o.props.get("multiple")).and_then(Value::as_bool).unwrap_or(false);
1075 let (ok, refused): (Vec<Value>, Vec<Value>) = incoming.into_iter().partition(|f| !max.is_some_and(|m| f.get("size").and_then(Value::as_f64).is_some_and(|s| s > m)));
1076 if !refused.is_empty() {
1077 let name = owner.as_ref().and_then(|o| o.props.get("name")).map(js).unwrap_or_default();
1078 let template = self.strings.get("fileTooLargeNamed").cloned().unwrap_or_default();
1079 let messages = refused
1080 .iter()
1081 .map(|f| {
1082 let mut params = serde_json::Map::new();
1083 params.insert("name".into(), Value::String(f.get("name").map(js).unwrap_or_default()));
1084 crate::strings::format_string(&template, ¶ms)
1085 })
1086 .collect();
1087 self.local.errors.insert(id.to_string(), FieldErrors { name, messages });
1088 } else {
1089 self.local.errors.remove(id);
1090 }
1091 let accepted: Vec<Value> = if multiple { ok } else { ok.into_iter().take(1).collect() };
1092 if accepted.is_empty() {
1093 self.needs_build = true;
1095 return vec![OutEvent::Relayout];
1096 }
1097 let mut files: Vec<Value> = if multiple { self.files_of(id) } else { Vec::new() };
1099 files.retain(|f| !accepted.iter().any(|a| a.get("name") == f.get("name")));
1100 files.extend(accepted.iter().cloned());
1101 let mut out = self.files_write(id, files.clone());
1104 out.extend(self.fire(id, "upload", Some(json!({"files": files, "added": accepted}))));
1105 out.push(OutEvent::Relayout);
1106 out
1107 }
1108
1109 fn file_remove(&mut self, id: &str, i: usize) -> Vec<OutEvent> {
1110 let mut files = self.files_of(id);
1111 if i >= files.len() {
1112 return vec![];
1113 }
1114 let removed = files.remove(i);
1115 let mut out = self.files_write(id, files.clone());
1116 out.extend(self.fire(id, "remove", Some(json!({"name": removed.get("name").cloned().unwrap_or(Value::Null), "files": files}))));
1117 out.push(OutEvent::Relayout);
1118 out
1119 }
1120
1121 fn table_sort(&mut self, n: &LNode, id: &str) -> Vec<OutEvent> {
1122 if n.props.get("sortable").and_then(Value::as_bool) != Some(true) {
1123 return vec![];
1124 }
1125 let key = n.props.get("key").map(js).unwrap_or_default();
1126 let current = self.current_prop(id, "sort");
1127 let same = current.as_ref().and_then(|s| s.get("key")).map(js).as_deref() == Some(key.as_str());
1128 let dir = if same && current.as_ref().and_then(|s| s.get("direction")).and_then(Value::as_str) == Some("asc") { "desc" } else { "asc" };
1129 let sort = json!({"key": key, "direction": dir});
1130 let mut out = self.write_through(id, "sort", sort.clone());
1131 if out.is_empty() {
1132 self.local.sort.insert(id.to_string(), (key, dir.to_string()));
1133 }
1134 self.needs_build = true;
1135 out.extend(self.fire(id, "sort", Some(json!({"sort": sort}))));
1136 out.push(OutEvent::Relayout);
1137 out
1138 }
1139
1140 fn table_selected(&self, id: &str) -> Vec<String> {
1141 self.current_prop(id, "selected").and_then(|v| v.as_array().map(|a| a.iter().map(js).collect())).unwrap_or_default()
1142 }
1143
1144 fn table_select(&mut self, id: &str, selected: Vec<String>) -> Vec<OutEvent> {
1145 let value = Value::Array(selected.iter().cloned().map(Value::String).collect());
1146 let mut out = self.write_through(id, "selected", value.clone());
1147 if out.is_empty() {
1148 self.local.selected.insert(id.to_string(), selected);
1149 }
1150 self.needs_build = true;
1151 out.extend(self.fire(id, "select", Some(json!({"selected": value}))));
1152 out.push(OutEvent::Relayout);
1153 out
1154 }
1155
1156 fn table_row(&mut self, n: &LNode, id: &str) -> Vec<OutEvent> {
1157 let key = n.props.get("key").map(js).unwrap_or_default();
1158 let row = self.node_by_id(id).and_then(|o| o.props.get("rows")).and_then(|r| r.get(n.props.get("index").and_then(Value::as_u64).unwrap_or(0) as usize)).cloned().unwrap_or(Value::Null);
1159 let mut out = self.fire(id, "rowPress", Some(json!({"key": key, "row": row})));
1160 let selectable = self.node_by_id(id).and_then(|o| o.props.get("selectable")).and_then(Value::as_str).unwrap_or("none").to_string();
1161 if selectable == "single" {
1162 let current = self.table_selected(id);
1163 let next = if current == [key.clone()] { vec![] } else { vec![key] };
1164 out.extend(self.table_select(id, next));
1165 }
1166 out
1167 }
1168
1169 fn table_check(&mut self, n: &LNode, id: &str) -> Vec<OutEvent> {
1170 let mut selected = self.table_selected(id);
1171 if n.props.get("header").and_then(Value::as_bool) == Some(true) {
1172 let all: Vec<String> = self.nodes.iter().enumerate().filter(|(i, r)| self.live[*i] && r.owner.as_deref() == Some(id) && r.part.as_deref() == Some("row")).filter_map(|(_, r)| r.props.get("key").map(js)).collect();
1173 let rows = self.node_by_id(id).and_then(|o| o.props.get("rows")).and_then(Value::as_array).map(Vec::len).unwrap_or(0);
1174 let checked = n.props.get("checked").and_then(Value::as_bool).unwrap_or(false);
1175 selected = if checked {
1176 vec![]
1177 } else if all.len() == rows {
1178 all
1179 } else {
1180 let key = self.node_by_id(id).and_then(|o| o.props.get("rowKey")).map(js).unwrap_or_else(|| "id".into());
1181 self.node_by_id(id).and_then(|o| o.props.get("rows")).and_then(Value::as_array).map(|rs| rs.iter().enumerate().map(|(i, r)| r.get(&key).map(js).unwrap_or_else(|| i.to_string())).collect()).unwrap_or_default()
1182 };
1183 } else {
1184 let key = n.props.get("key").map(js).unwrap_or_default();
1185 if selected.contains(&key) {
1186 selected.retain(|k| *k != key);
1187 } else {
1188 selected.push(key);
1189 }
1190 }
1191 self.table_select(id, selected)
1192 }
1193
1194 fn resizable_state(&self, owner_id: &str) -> Option<ResizableState> {
1200 let slot = *self.slot_of.get(owner_id)?;
1201 let owner = &self.nodes[slot as usize];
1202 let sizes: Vec<f64> = owner.props.get("sizes").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect())?;
1203 let limits = crate::resizable::PanelLimits::list(owner.props.get("panels"));
1204 let orientation = if owner.props.get("direction").and_then(Value::as_str) == Some("vertical") { crate::resizable::Orientation::Vertical } else { crate::resizable::Orientation::Horizontal };
1205 let rtl = self.node_state.get(slot as usize).is_some_and(|s| s.direction == taffy::style::Direction::Rtl);
1206 Some((sizes, limits, orientation, rtl, slot))
1207 }
1208
1209 fn resize_event(&mut self, n: &LNode, owner_id: &str, event: &str, payload: Option<&Value>) -> Vec<OutEvent> {
1217 let Some((sizes, limits, orientation, rtl, slot)) = self.resizable_state(owner_id) else { return vec![] };
1218 let handle = n.props.get("handle").and_then(Value::as_u64).unwrap_or(0) as usize;
1219 let hairline = self.effective.as_ref().and_then(|t| t.tokens.control.get("hairline").copied()).unwrap_or(1.0);
1220 let next = if event == "key" {
1221 let key = payload.and_then(|p| p.get("key")).and_then(Value::as_str).unwrap_or("");
1222 if !crate::resizable::RESIZE_KEYS.contains(&key) {
1223 return vec![];
1224 }
1225 let next = crate::resizable::keyboard_resize(&sizes, handle, key, orientation, rtl, &limits);
1226 if next == sizes {
1227 return vec![];
1228 }
1229 next
1230 } else {
1231 let phase = payload.and_then(|p| p.get("phase")).and_then(Value::as_str).unwrap_or("move");
1232 if phase == "start" {
1233 self.local.drags.insert(n.id.clone(), sizes);
1234 return vec![];
1235 }
1236 let start = self.local.drags.get(&n.id).cloned().unwrap_or(sizes.clone());
1237 let px = payload.and_then(|p| p.get("delta")).and_then(Value::as_f64).unwrap_or(0.0);
1238 let frame = self.last_frames.get(slot as usize).copied().unwrap_or_default();
1239 let container = if orientation == crate::resizable::Orientation::Vertical { frame.h } else { frame.w } as f64;
1240 let delta = crate::resizable::drag_delta(px, container, start.len(), orientation, rtl, hairline);
1241 let next = crate::resizable::resize_panels(&start, handle, delta, &limits);
1242 if phase != "end" {
1243 if next != sizes {
1244 self.local.sizes.insert(owner_id.to_string(), next);
1245 self.subtree_pending.insert(owner_id.to_string());
1246 return vec![OutEvent::Relayout];
1247 }
1248 return vec![];
1249 }
1250 self.local.drags.remove(&n.id);
1251 if next == start {
1252 if sizes != start {
1254 if self.binding_path(owner_id, "sizes").is_some() {
1255 self.local.sizes.remove(owner_id);
1256 } else {
1257 self.local.sizes.insert(owner_id.to_string(), start);
1258 }
1259 self.subtree_pending.insert(owner_id.to_string());
1260 return vec![OutEvent::Relayout];
1261 }
1262 return vec![];
1263 }
1264 next
1265 };
1266 let value = json!(next);
1267 let mut out = self.write_through(owner_id, "sizes", value.clone());
1268 if out.is_empty() {
1269 self.local.sizes.insert(owner_id.to_string(), next);
1270 } else {
1271 self.local.sizes.remove(owner_id);
1272 }
1273 self.needs_build = true;
1274 out.extend(self.fire(owner_id, "change", Some(json!({"sizes": value}))));
1275 out.push(OutEvent::Relayout);
1276 out
1277 }
1278
1279 pub fn failing_checks(&self, id: &str) -> Vec<String> {
1290 let Some(node) = self.source_node(id) else { return vec![] };
1291 let scope = self.scope_of(id);
1292 let ctx = self.resolve_ctx(&scope);
1293 node.props
1294 .get("checks")
1295 .and_then(Value::as_array)
1296 .map(|checks| {
1297 checks
1298 .iter()
1299 .filter(|c| {
1300 let Some(cond) = c.get("condition") else { return false };
1301 check_fails(crate::data::resolve_value(cond, &ctx).as_ref())
1302 })
1303 .map(|c| c.get("message").and_then(Value::as_str).map(|message| crate::strings::resolve_string(message, &self.strings).to_string()).unwrap_or_else(|| self.strings.get("invalidValue").cloned().unwrap_or_else(|| "Invalid value".into())))
1304 .collect()
1305 })
1306 .unwrap_or_default()
1307 }
1308
1309 fn validate_on(&self, id: &str) -> String {
1311 let node = self.node_by_id(id);
1312 node.and_then(|n| n.props.get("validateOn"))
1313 .and_then(Value::as_str)
1314 .map(str::to_string)
1315 .or_else(|| node.and_then(|n| self.view.component(&n.component)).and_then(|d| d.props.get("validateOn")).and_then(|p| p.default.as_ref()).and_then(Value::as_str).map(str::to_string))
1316 .unwrap_or_else(|| "blur".into())
1317 }
1318
1319 fn validate_field(&mut self, id: &str) {
1321 let failed = self.failing_checks(id);
1322 let name = self.node_by_id(id).and_then(|n| n.props.get("name")).map(js).unwrap_or_default();
1323 let before = self.local.errors.get(id).map(|e| e.messages.clone());
1324 if failed.is_empty() {
1325 self.local.errors.remove(id);
1326 } else {
1327 self.local.errors.insert(id.to_string(), FieldErrors { name, messages: failed.clone() });
1328 }
1329 if before.unwrap_or_default() != failed {
1330 self.needs_build = true;
1331 }
1332 }
1333
1334 fn field_value(&self, id: &str, component: &str) -> Value {
1336 if let Some(v) = self.local.field_values.get(id) {
1337 return v.clone();
1338 }
1339 let props = self.node_by_id(id).map(|n| n.props.clone()).unwrap_or_default();
1340 match component {
1341 "Checkbox" | "Switch" => props.get("checked").cloned().unwrap_or(Value::Bool(false)),
1342 "ChipInput" => props.get("values").cloned().unwrap_or(json!([])),
1343 "FileUpload" => props.get("files").cloned().unwrap_or(json!([])),
1344 "DateRangePicker" => json!({"start": props.get("start").cloned().unwrap_or(Value::Null), "end": props.get("end").cloned().unwrap_or(Value::Null)}),
1345 "TimePicker" => props.get("value").filter(|v| v.as_str().and_then(parse_time).is_some()).cloned().unwrap_or(Value::Null),
1346 _ => props.get("value").cloned().unwrap_or(Value::Null),
1347 }
1348 }
1349
1350 pub fn submit_form(&mut self, form_id: &str) -> Vec<OutEvent> {
1355 self.ensure_reduced();
1356 let Some(form) = self.source_node(form_id) else { return vec![] };
1357 if self.node_by_id(form_id).and_then(|n| n.props.get("busy")).and_then(Value::as_bool) == Some(true) {
1358 return vec![];
1359 }
1360 let mut fields: Vec<(String, String, String)> = Vec::new();
1361 fn walk(n: &UiNode, out: &mut Vec<(String, String, String)>, top: bool) {
1362 if !top && n.component == "Form" {
1363 return;
1364 }
1365 if FORM_FIELDS.contains(&n.component.as_str()) {
1366 if let Some(name) = n.props.get("name").and_then(Value::as_str) {
1367 out.push((n.id.clone(), n.component.clone(), name.to_string()));
1368 }
1369 }
1370 for s in n.slots.iter().flat_map(|s| s.values()) {
1371 walk(s, out, false);
1372 }
1373 for c in &n.children {
1374 walk(c, out, false);
1375 }
1376 }
1377 walk(&form, &mut fields, true);
1378 let suffix = self.instance_suffix(form_id);
1381 if !suffix.is_empty() {
1382 for f in &mut fields {
1383 f.0.push_str(&suffix);
1384 }
1385 }
1386 let mut errors = Vec::new();
1387 let mut first_invalid: Option<String> = None;
1388 for (id, _, name) in &fields {
1389 let failed = self.failing_checks(id);
1390 if failed.is_empty() {
1391 self.local.errors.remove(id);
1392 continue;
1393 }
1394 first_invalid.get_or_insert_with(|| id.clone());
1395 for m in &failed {
1396 errors.push(json!({"name": name, "message": m}));
1397 }
1398 self.local.errors.insert(id.clone(), FieldErrors { name: name.clone(), messages: failed });
1399 }
1400 self.needs_build = true;
1401 let mut out = vec![];
1402 if let Some(first) = first_invalid {
1403 out.extend(self.fire(form_id, "invalid", Some(json!({"errors": errors}))));
1404 let index = self.slot_of.get(&first).copied().unwrap_or(0);
1405 out.push(OutEvent::Focus { id: first, index });
1406 out.push(OutEvent::Announce { text: self.strings.get("invalidFields").cloned().unwrap_or_default(), live: "assertive".into() });
1407 out.push(OutEvent::Relayout);
1408 return out;
1409 }
1410 let mut values = Map::new();
1411 for (id, component, name) in &fields {
1412 values.insert(name.clone(), self.field_value(id, component));
1413 }
1414 out.extend(self.fire(form_id, "submit", Some(json!({"values": values}))));
1415 if let Some(overlay) = self.enclosing_modal(form_id) {
1420 out.extend(self.set_open(&overlay, false));
1421 }
1422 out.push(OutEvent::Relayout);
1423 out
1424 }
1425
1426 fn enclosing_modal(&self, id: &str) -> Option<String> {
1434 const OVERLAYS: [&str; 6] = ["Dialog", "Drawer", "Popover", "Tooltip", "Menu", "Toast"];
1435 let mut cur = self.node_by_id(id).map(|n| n.index);
1436 let mut guard = 0;
1437 let nearest = loop {
1438 let Some(i) = cur else { return None };
1439 guard += 1;
1440 if guard > 100_000 {
1441 return None;
1442 }
1443 let n = &self.nodes[i as usize];
1444 if n.part.is_none() && OVERLAYS.contains(&n.component.as_str()) && n.id != id {
1445 break n.id.clone();
1446 }
1447 if n.owner_component.as_deref().is_some_and(|c| OVERLAYS.contains(&c)) {
1448 break n.owner.clone()?;
1449 }
1450 cur = n.parent.or_else(|| n.owner.as_deref().and_then(|o| self.node_by_id(o)).map(|o| o.index));
1451 };
1452 let overlay = self.node_by_id(&nearest)?;
1453 let modal = matches!(overlay.component.as_str(), "Dialog" | "Drawer");
1454 let dismissible = overlay.props.get("dismissible").and_then(Value::as_bool) != Some(false);
1455 (modal && dismissible).then_some(nearest)
1456 }
1457
1458 pub fn command(&mut self, command: &SurfaceCommand) -> Vec<OutEvent> {
1464 self.ensure_reduced();
1465 match command {
1466 SurfaceCommand::Announce { text, live } => vec![OutEvent::Announce { text: text.clone(), live: live.clone().unwrap_or_else(|| "polite".into()) }],
1467 SurfaceCommand::Focus { id } => {
1468 let Some(&slot) = self.slot_of.get(id) else { return vec![] };
1469 let mut stack = vec![slot];
1471 while let Some(s) = stack.pop() {
1472 let n = &self.nodes[s as usize];
1473 if n.hidden {
1474 continue;
1475 }
1476 if n.pressable || matches!(n.part.as_deref(), Some("field" | "input" | "trigger" | "search")) {
1477 return vec![OutEvent::Focus { id: n.id.clone(), index: s }];
1478 }
1479 stack.extend(n.children.iter().rev());
1480 }
1481 vec![OutEvent::Focus { id: id.clone(), index: slot }]
1482 }
1483 SurfaceCommand::ScrollIntoView { id } => self.scroll_into_view(id),
1484 SurfaceCommand::ScrollToIndex { id, index, align } => self.scroll_to_index(id, *index as usize, align.as_deref().and_then(crate::list::ScrollAlign::parse).unwrap_or_default()),
1485 }
1486 }
1487
1488 pub fn scroll_to_index(&mut self, id: &str, index: usize, align: crate::list::ScrollAlign) -> Vec<OutEvent> {
1494 self.ensure_reduced();
1495 use crate::layout_tree::ListViewSource;
1496 let Some(spec) = self.lists.iter().find(|l| l.id == id).cloned() else { return vec![] };
1497 let pos = match &spec.data_index {
1498 Some(map) => map.iter().position(|d| *d == Some(index)),
1499 None => Some(spec.static_count + index),
1500 };
1501 let Some(pos) = pos.filter(|p| *p < spec.keys.len()) else { return vec![] };
1502 let Some(window) = self.local.lists.get_mut(id) else { return vec![] };
1505 window.gap = spec.gap;
1506 let offsets = window.offsets_cached(&spec.keys);
1507 let count = spec.keys.len();
1508 let extent = |r: usize| (offsets[r + 1] - offsets[r] - if r + 1 < count { spec.gap } else { 0.0 }) as f64;
1509 let h = spec.horizontal;
1510 let axis = |p: (f32, f32)| if h { p.0 } else { p.1 };
1511 let own = self.local.scroll.get(id).copied().map(axis).unwrap_or(0.0);
1512 let view = self.local.list_views.get(id).cloned();
1513 let (source, viewport) = match view {
1514 Some(v) => (v.source, v.viewport),
1515 None => (ListViewSource::Own, if h { self.viewport.0 } else if self.viewport.1 > 0.0 { self.viewport.1 } else { self.max_height.unwrap_or(2000.0) }),
1516 };
1517 let scroll = match &source {
1518 ListViewSource::Own => own,
1519 ListViewSource::Ancestor { id: a, rel } => self.local.scroll.get(a).copied().map(axis).unwrap_or(0.0) - rel,
1520 ListViewSource::Host { rel } => self.local.surface_scroll.map(|p| axis(p) - rel).unwrap_or(own),
1521 };
1522 let inset = if spec.sticky {
1524 let before = spec.headers.partition_point(|&hp| hp < pos);
1525 before.checked_sub(1).map(|i| extent(spec.headers[i])).unwrap_or(0.0)
1526 } else {
1527 0.0
1528 };
1529 let target = crate::list::scroll_offset_at(offsets[pos] as f64, extent(pos), offsets[count] as f64, viewport as f64, scroll.max(0.0) as f64, align, inset) as f32;
1530 let with_axis = |p: (f32, f32), v: f32| if h { (v, p.1) } else { (p.0, v) };
1531 let mut out = vec![];
1532 match source {
1533 ListViewSource::Own => {
1534 let (x, y) = with_axis(self.local.scroll.get(id).copied().unwrap_or_default(), target);
1535 self.local.scroll.insert(id.to_string(), (x, y));
1536 }
1537 ListViewSource::Ancestor { id: a, rel } => {
1538 let (x, y) = with_axis(self.local.scroll.get(&a).copied().unwrap_or_default(), target + rel);
1539 self.local.scroll.insert(a.clone(), (x, y));
1540 }
1541 ListViewSource::Host { rel } => match self.local.surface_scroll {
1542 Some(p) => {
1543 let (x, y) = with_axis(p, target + rel);
1544 self.local.surface_scroll = Some((x, y));
1545 out.push(OutEvent::ScrollSurface { x, y });
1546 }
1547 None => {
1548 let (x, y) = with_axis(self.local.scroll.get(id).copied().unwrap_or_default(), target);
1549 self.local.scroll.insert(id.to_string(), (x, y));
1550 }
1551 },
1552 }
1553 if spec.windowed {
1554 self.subtree_pending.insert(id.to_string());
1555 }
1556 out.push(OutEvent::Relayout);
1557 out
1558 }
1559
1560 pub fn scroll_into_view(&mut self, id: &str) -> Vec<OutEvent> {
1563 self.ensure_reduced();
1564 let mut out = vec![];
1565 if let Some(&slot) = self.slot_of.get(id) {
1566 let target = self.last_frames.get(slot as usize).copied().unwrap_or_default();
1567 let mut cur = self.nodes[slot as usize].parent;
1568 while let Some(p) = cur {
1569 if let Some(s) = self.scrolls.iter().find(|s| s.index == p).copied() {
1570 let frame = self.last_frames[p as usize];
1571 let pid = self.scroll_key(p);
1572 let (mut ox, mut oy) = (s.offset_x, s.offset_y);
1573 let top = target.y - frame.y;
1574 let left = target.x - frame.x;
1575 if s.scroll_y {
1576 if top < oy {
1577 oy = top;
1578 } else if top + target.h > oy + frame.h {
1579 oy = top + target.h - frame.h;
1580 }
1581 }
1582 if s.scroll_x {
1583 if left < ox {
1584 ox = left;
1585 } else if left + target.w > ox + frame.w {
1586 ox = left + target.w - frame.w;
1587 }
1588 }
1589 if self.scroll_to(&pid, ox, oy) {
1590 out.push(OutEvent::Relayout);
1591 }
1592 }
1593 cur = self.nodes[p as usize].parent;
1594 }
1595 return out;
1596 }
1597 for l in self.lists.clone() {
1599 if let Some(pos) = l.keys.iter().position(|k| k == id || id.ends_with(&format!(".{k}"))) {
1600 let y = l.offsets.get(pos).copied().unwrap_or(0.0);
1601 if self.scroll_to(&l.id, 0.0, y) {
1602 out.push(OutEvent::Relayout);
1603 }
1604 }
1605 }
1606 out
1607 }
1608}
1609
1610type ResizableState = (Vec<f64>, Vec<crate::resizable::PanelLimits>, crate::resizable::Orientation, bool, u32);