Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 41 additions & 24 deletions crates/octoscript-makepad/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,38 @@ 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
}

/// Translate a tree whose presentation was already resolved by the L0 kit.
/// 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.
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{ <content>, 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;
Expand Down Expand Up @@ -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.
//
Expand All @@ -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{…}`.
Expand All @@ -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}}}");
Expand Down Expand Up @@ -586,7 +601,7 @@ fn control_roles(kind: NodeKind, a: &Attrs) -> Vec<String> {
/// 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 {
Expand Down Expand Up @@ -778,16 +793,18 @@ 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()) {
let _ = writeln!(out, "{ind}{property}: fn(text) {{ NAV(t: {target:?}, v: text) }}");
}
}
} 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
Expand Down
20 changes: 20 additions & 0 deletions crates/octoscript-makepad/tests/l0_packs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
}
2 changes: 1 addition & 1 deletion runtime.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
},
"octoscript": {
"url": "https://github.com/OctoSense-org/Octoscript.git",
"revision": "68f6a9df55692b5d8ef8873a12721e279a3f40d6"
"revision": "76032ed4b0a9cce346cbc9e4f9f30fe00d76e65b"
}
}
}