diff --git a/Cargo.toml b/Cargo.toml index 554a351..f7c5e69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ makepad-widgets = { git = "https://github.com/OctoSense-org/makepad.git", rev = "a5a3cf5b0dbd1b56c25defe6d98479f25843e770" } makepad-script = { git = "https://github.com/OctoSense-org/makepad.git", rev = "a5a3cf5b0dbd1b56c25defe6d98479f25843e770" } # The VM-independent Octoscript node model, from the Octoscript repo itself. -octoscript-ui-l0 = { git = "https://github.com/OctoSense-org/Octoscript.git", rev = "68f6a9df55692b5d8ef8873a12721e279a3f40d6" } +octoscript-ui-l0 = { git = "https://github.com/OctoSense-org/Octoscript.git", rev = "76032ed4b0a9cce346cbc9e4f9f30fe00d76e65b" } [profile.release] opt-level = 3 diff --git a/crates/octoscript-makepad/src/lib.rs b/crates/octoscript-makepad/src/lib.rs index e83b543..87cb737 100644 --- a/crates/octoscript-makepad/src/lib.rs +++ b/crates/octoscript-makepad/src/lib.rs @@ -61,7 +61,7 @@ pub fn to_makepad_ui(root: &UiNode) -> String { let mut root = root.clone(); resolve_ink_planes(&mut root, None, None); let mut out = String::new(); - emit(&root, &mut out, 0, false); + emit(&root, &mut out, 0, false, None); out } @@ -69,13 +69,30 @@ pub fn to_makepad_ui(root: &UiNode) -> String { /// Material's semantic Card/Chip lowering must not overwrite these dimensions, /// colors or child labels, and L0 sizes are already Makepad points. pub fn to_makepad_l0_ui(root: &UiNode) -> String { + to_makepad_l0_ui_inner(root, None) +} + +/// Route checked L0 event targets to a host-owned channel. The host must +/// verify the currently mounted Card identity before dispatching the target. +pub fn to_makepad_l0_ui_with_events(root: &UiNode, event_channel: &str) -> String { + to_makepad_l0_ui_inner(root, Some(event_channel)) +} + +fn to_makepad_l0_ui_inner(root: &UiNode, event_channel: Option<&str>) -> String { let mut root = root.clone(); resolve_ink_planes(&mut root, None, None); let mut out = String::new(); - emit(&root, &mut out, 0, true); + emit(&root, &mut out, 0, true, event_channel); out } +fn click_call(target: &str, event_channel: Option<&str>) -> String { + match event_channel.filter(|_| target.starts_with("l0:")) { + Some(channel) => format!("agent.notify({channel:?}, {{target: {target:?}}})"), + None => format!("NAV(t: {target:?})"), + } +} + pub mod design; /// Relative luminance of an 0xAARRGGBB colour, sRGB, alpha ignored. @@ -229,7 +246,7 @@ fn needs_click_overlay(node: &UiNode) -> bool { node.attrs.tapto.is_some() && is_container(node.kind) } -fn emit(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { +fn emit(node: &UiNode, out: &mut String, depth: usize, resolved: bool, event_channel: Option<&str>) { let mut primitive; let node = if resolved && matches!(node.kind, NodeKind::Card | NodeKind::Chip) { primitive = node.clone(); @@ -243,18 +260,18 @@ fn emit(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { // and left every lowered component inert. This terminates because a lowered // node is primitive, and a resolved text role clears the `variant`. if let Some(lowered) = (!resolved).then(|| material::lower(node, &theme())).flatten() { - emit(&lowered, out, depth, resolved); + emit(&lowered, out, depth, resolved, event_channel); return; } if !resolved && needs_vertical_pad_wrapper(node) { - emit_vertical_pad(node, out, depth, resolved); + emit_vertical_pad(node, out, depth, resolved, event_channel); return; } if needs_click_overlay(node) { - emit_click_overlay(node, out, depth, resolved); + emit_click_overlay(node, out, depth, resolved, event_channel); return; } - emit_widget(node, out, depth, resolved); + emit_widget(node, out, depth, resolved, event_channel); } /// Vertical-only padding (`pady` with no `padx`) on a node that has no fixed @@ -295,7 +312,7 @@ fn needs_vertical_pad_wrapper(node: &UiNode) -> bool { || text_default } -fn emit_vertical_pad(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { +fn emit_vertical_pad(node: &UiNode, out: &mut String, depth: usize, resolved: bool, event_channel: Option<&str>) { let ind = " ".repeat(depth); let inner_ind = " ".repeat(depth + 1); // The widget already carries makepad's own `theme.mspace_1` inset (~4dp), so @@ -349,19 +366,19 @@ fn emit_vertical_pad(node: &UiNode, out: &mut String, depth: usize, resolved: bo bare.attrs.pad = None; bare.attrs.marginy = Some(0.0); bare.attrs.margin = None; - emit(&bare, out, depth + 1, resolved); + emit(&bare, out, depth + 1, resolved, event_channel); let _ = writeln!(out, "{inner_ind}View {{ height: {py} }}"); let _ = writeln!(out, "{ind}}}"); } /// `Overlay{ , Button{…on_click} }` — see [`needs_click_overlay`]. /// -/// The handler calls `NAV`, a global the host registers, rather than reaching -/// through `ui.nav_signal`. `ui` is injected by `Splash::eval_body`, so a body +/// The handler calls `NAV` for ordinary routes. L0 Card event targets can use +/// a host-owned notification channel instead. `ui` is injected by `Splash::eval_body`, so a body /// mounted anywhere else — notably on the app's main VM, which is what gets this /// crate's fonts and a widget kit's theming into reach — had every tap silently /// do nothing. A global works on either VM. -fn emit_click_overlay(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { +fn emit_click_overlay(node: &UiNode, out: &mut String, depth: usize, resolved: bool, event_channel: Option<&str>) { let ind = " ".repeat(depth); let inner_ind = " ".repeat(depth + 1); let a = &node.attrs; @@ -426,7 +443,7 @@ fn emit_click_overlay(node: &UiNode, out: &mut String, depth: usize, resolved: b // The content, with `tapto` stripped so it does not re-enter this path. let mut content = node.clone(); content.attrs.tapto = None; - emit_widget(&content, out, depth + 1, resolved); + emit_widget(&content, out, depth + 1, resolved, event_channel); // The hit target: an empty ButtonFlatter filling the wrapper. // @@ -445,15 +462,13 @@ fn emit_click_overlay(node: &UiNode, out: &mut String, depth: usize, resolved: b let _ = writeln!(out, "{inner_ind}OctoscriptTap {{"); let _ = writeln!(out, "{inner_ind} width: Fill"); let _ = writeln!(out, "{inner_ind} height: Fill"); - let _ = writeln!( - out, - "{inner_ind} on_click: || {{ NAV(t: {target:?}) }}" - ); + let call = click_call(target, event_channel); + let _ = writeln!(out, "{inner_ind} on_click: || {{ {call} }}"); let _ = writeln!(out, "{inner_ind}}}"); let _ = writeln!(out, "{ind}}}"); } -fn emit_widget(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { +fn emit_widget(node: &UiNode, out: &mut String, depth: usize, resolved: bool, event_channel: Option<&str>) { let ind = " ".repeat(depth); let name = widget_for(node); // An `id` makes the widget addressable in the mounted tree: `name := Widget{…}`. @@ -465,14 +480,14 @@ fn emit_widget(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { let _ = writeln!(out, "{ind}{name} {{"); } } - emit_attrs(node, out, depth + 1, resolved); + emit_attrs(node, out, depth + 1, resolved, event_channel); // Only containers carry children — decided by the node's *kind*, and now // actually so. This read `widget_name(node.kind) == "View"`, which is a // question about the mapped name wearing the comment of a question about the // kind. See `is_container`. if is_container(node.kind) { for c in &node.children { - emit(c, out, depth + 1, resolved); + emit(c, out, depth + 1, resolved, event_channel); } } let _ = writeln!(out, "{ind}}}"); @@ -586,7 +601,7 @@ fn control_roles(kind: NodeKind, a: &Attrs) -> Vec { /// makepad evidently resolves those on a different rounding path. const DP_SCALE: f32 = 157.7 / 157.0; -fn emit_attrs(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { +fn emit_attrs(node: &UiNode, out: &mut String, depth: usize, resolved: bool, event_channel: Option<&str>) { let ind = " ".repeat(depth); let a = &node.attrs; if resolved && node.kind == NodeKind::Column { @@ -778,8 +793,9 @@ fn emit_attrs(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { if let Some(ph) = a.placeholder.as_ref() { let _ = writeln!(out, "{ind}empty_text: {ph:?}"); } - // `tapto` wires an on_click that writes the route into the `nav_signal` - // widget; the host app reads that text and re-mounts the target screen. + // `tapto` wires an on_click to the registered navigation callback, or to + // the Card host's event channel for L0 event targets; text fields report + // their value through the navigation callback. if matches!(node.kind, NodeKind::Input | NodeKind::Textarea) { for (property, route) in [("on_change", &a.changeto), ("on_return", &a.tapto)] { if let Some(target) = route.as_ref().filter(|t| !t.is_empty()) { @@ -787,7 +803,8 @@ fn emit_attrs(node: &UiNode, out: &mut String, depth: usize, resolved: bool) { } } } else if let Some(target) = a.tapto.as_ref().filter(|t| !t.is_empty()) { - let _ = writeln!(out, "{ind}on_click: || {{ NAV(t: {target:?}) }}"); + let call = click_call(target, event_channel); + let _ = writeln!(out, "{ind}on_click: || {{ {call} }}"); } if let Some(s) = a.size { // The DSL states type sizes in sp, as Material does; makepad's font_size diff --git a/crates/octoscript-makepad/tests/l0_packs.rs b/crates/octoscript-makepad/tests/l0_packs.rs index 76d555c..cd6d03a 100644 --- a/crates/octoscript-makepad/tests/l0_packs.rs +++ b/crates/octoscript-makepad/tests/l0_packs.rs @@ -63,3 +63,23 @@ fn theme_modes_answer_the_same_registered_component_names() { assert!(dark["compounds"].as_object().unwrap().len() > 50); } } + +#[test] +fn l0_event_target_routes_to_a_host_owned_channel() { + let source = "state selected { shape: enum[off, on], initial: .off }\n\ + event flip { selected: cycle(.off, .on) }\n\ + view root Row(on_tap: flip) { TextBody(text: selected) }"; + let prepared = octoscript_makepad::l0::prepare(source, &serde_json::json!({}), &directory()).unwrap(); + let ui = octoscript_makepad::to_makepad_l0_ui_with_events(&prepared.tree, "card-runtime-42"); + assert!(ui.contains("OctoscriptTap {")); + assert!(ui.contains("agent.notify(\"card-runtime-42\"")); + assert!(ui.contains("l0:{")); + assert!(!ui.contains("NAV(")); + let mut state = octoscript_ui_l0::InstanceStore::default(); + assert!(octoscript_ui_l0::dispatch_with_data(source, &mut state, "root", "flip", None, &serde_json::json!({}))); + let changed = octoscript_makepad::l0::prepare_with_state(source, &serde_json::json!({}), &state, &directory()).unwrap(); + fn has_text(node: &octoscript_render::UiNode, text: &str) -> bool { + node.attrs.text.as_deref() == Some(text) || node.children.iter().any(|child| has_text(child, text)) + } + assert!(has_text(&changed.tree, "on")); +} diff --git a/runtime.json b/runtime.json index 4247196..69cd21c 100644 --- a/runtime.json +++ b/runtime.json @@ -7,7 +7,7 @@ }, "octoscript": { "url": "https://github.com/OctoSense-org/Octoscript.git", - "revision": "68f6a9df55692b5d8ef8873a12721e279a3f40d6" + "revision": "76032ed4b0a9cce346cbc9e4f9f30fe00d76e65b" } } }