makepad/widgets/tests/named_slots_a.rs
Admin 53e0e85b92 draw and widgets: Slug text everywhere, no blank glyphs, slimmer widgets
All outline text draws with Slug curves on every platform (SDF/MSDF stays
selectable per size); a frame never presents a blank glyph and retained draw
lists keep theirs. One coverage model for vector feathering, widget families
split out of the core crate, fewer steady-state allocations, video keeps its
aspect (Contain/Cover), render and SSAO pass unification.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

271 lines
9.5 KiB
Rust

//! Named-child slots on widgets: a template's named children are slots an instance reaches with
//! `slot +: {..}` (merge, add children), `slot := V` (replace in place) and the dotted forms
//! (`body.slot +: {..}`, `split.a.left := V`); a declared property is not a child.
use makepad_widgets::makepad_script::script;
use makepad_widgets::*;
/// `id(text)[children]` for each child, comma separated, ids as written in the document.
fn tree(w: &WidgetRef) -> String {
let mut kids = Vec::new();
w.children(&mut |id, c| kids.push((id, c)));
kids.iter()
.map(|(id, c)| {
let mut s = format!("{id}");
let text = c.text();
if !text.is_empty() {
s.push_str(&format!("({text})"));
}
let inner = tree(c);
if !inner.is_empty() {
s.push_str(&format!("[{inner}]"));
}
s
})
.collect::<Vec<_>>()
.join(",")
}
/// Builds a document into a widget; the tree and the errors of the evaluation and the build.
fn build(cx: &mut Cx, doc: ScriptValue) -> (String, Vec<String>) {
let mut errs = cx.with_vm(|vm| vm.take_errors());
let w = cx.with_vm(|vm| WidgetRef::script_from_value(vm, doc));
errs.extend(cx.with_vm(|vm| vm.take_errors()));
(tree(&w), errs)
}
fn ok(cx: &mut Cx, doc: ScriptValue) -> String {
let (t, errs) = build(cx, doc);
assert!(errs.is_empty(), "{errs:?}");
t
}
fn new_cx() -> Cx {
let mut cx = Cx::new(Box::new(|_, _| {}));
cx.with_vm(|vm| makepad_widgets::script_mod(vm));
cx.with_vm(|vm| vm.capture_errors());
cx
}
#[test]
fn slots_on_a_template_with_named_children() {
let mut cx = new_cx();
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
let Shell = View{
title := Label{ text: "T" }
slot := View{ a := Label{ text: "A" } }
}
Shell{ slot +: { b := Label{ text: "B" } } }
}));
assert_eq!(ok(&mut cx, d), "title(T),slot[a(A),b(B)]", "merge +: adds a child to the slot");
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
let Shell = View{
title := Label{ text: "T" }
slot := View{ a := Label{ text: "A" } }
}
Shell{ slot := View{ c := Label{ text: "C" } } }
}));
assert_eq!(ok(&mut cx, d), "title(T),slot[c(C)]", ":= replaces the slot in place");
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
let Shell = View{
title := Label{ text: "T" }
slot := View{ a := Label{ text: "A" } }
}
Shell{ slot.a.text: "A2" title.text: "T2" }
}));
assert_eq!(ok(&mut cx, d), "title(T2),slot[a(A2)]", "path properties");
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
let Shell = View{
title := Label{ text: "T" }
slot := View{ a := Label{ text: "A" } }
}
let Page = Shell{ slot +: { b := Label{ text: "B" } } }
View{ p1 := Page{} p2 := Shell{ slot +: { b := Label{ text: "B2" } } } }
}));
assert_eq!(
ok(&mut cx, d),
"p1[title(T),slot[a(A),b(B)]],p2[title(T),slot[a(A),b(B2)]]",
"two instances of one shell are independent"
);
}
#[test]
fn dotted_slots_through_a_nested_body() {
let mut cx = new_cx();
let shell = |cx: &mut Cx, doc: ScriptValue| ok(cx, doc);
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
let Shell = View{
title := Label{ text: "T" }
body := View{ slot := View{ a := Label{ text: "A" } } }
}
Shell{ body.slot +: { b := Label{ text: "B" } } }
}));
assert_eq!(shell(&mut cx, d), "title(T),body[slot[a(A),b(B)]]", "dotted +:");
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
let Shell = View{
title := Label{ text: "T" }
body := View{ slot := View{ a := Label{ text: "A" } } }
}
Shell{ body +: { slot := View{ c := Label{ text: "C" } } } }
}));
assert_eq!(shell(&mut cx, d), "title(T),body[slot[c(C)]]", "nested block replace");
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
let Shell = View{
title := Label{ text: "T" }
body := View{ slot := View{ a := Label{ text: "A" } } }
}
Shell{ body.slot := View{ c := Label{ text: "C" } } }
}));
assert_eq!(shell(&mut cx, d), "title(T),body[slot[c(C)]]", "dotted :=");
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
let Shell = View{
title := Label{ text: "T" }
body := View{ slot := View{ a := Label{ text: "A" } } }
}
Shell{ body.extra := Label{ text: "E" } }
}));
assert_eq!(shell(&mut cx, d), "title(T),body[slot[a(A)],extra(E)]", "dotted := appends a new child");
}
#[test]
fn slots_through_a_typed_field_of_a_registered_shell() {
let mut cx = new_cx();
cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.RegShell = View{
title := Label{ text: "T" }
split := Splitter{
a: View{ left := View{ a1 := Label{ text: "L" } } }
b: View{ right := View{ b1 := Label{ text: "R" } } }
}
}
}));
let base = reg(&mut cx, 0);
let merged = reg(&mut cx, 1);
let dotted = reg(&mut cx, 2);
assert_eq!(merged, dotted, "split.a.left +: is the nested-block form");
assert!(merged.contains("x(X)"), "{merged}");
assert_ne!(base, merged, "{base}");
let replaced = reg(&mut cx, 3);
let replaced_dotted = reg(&mut cx, 4);
assert_eq!(replaced, replaced_dotted, "split.a.left := is the nested-block form");
assert!(replaced.contains("y(Y)") && !replaced.contains("a1"), "{replaced}");
let prop = reg(&mut cx, 5);
assert!(prop.contains("a1(L2)"), "{prop}");
// two instances of the registered shell do not see each other's override
let two = reg(&mut cx, 6);
assert!(two.contains("one[") && two.contains("two["), "{two}");
assert_eq!(two.matches("x(X)").count(), 1, "{two}");
}
fn reg(cx: &mut Cx, which: usize) -> String {
let d = reg_doc(cx, which);
ok(cx, d)
}
fn reg_doc(cx: &mut Cx, which: usize) -> ScriptValue {
cx.with_vm(|vm| match which {
0 => vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.RegShell{}
}),
1 => vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.RegShell{ split +: { a +: { left +: { x := Label{ text: "X" } } } } }
}),
2 => vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.RegShell{ split.a.left +: { x := Label{ text: "X" } } }
}),
3 => vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.RegShell{ split +: { a +: { left := View{ y := Label{ text: "Y" } } } } }
}),
4 => vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.RegShell{ split.a.left := View{ y := Label{ text: "Y" } } }
}),
5 => vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.RegShell{ split.a.left.a1.text: "L2" }
}),
_ => vm.eval(script! {
use mod.prelude.widgets.*
View{
one := mod.widgets.RegShell{ split.a.left +: { x := Label{ text: "X" } } }
two := mod.widgets.RegShell{}
}
}),
})
}
#[test]
fn a_name_that_is_not_a_direct_child_is_an_error() {
let mut cx = new_cx();
cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.ErrShell = View{
title := Label{ text: "T" }
split := Splitter{
a: View{ left := View{ a1 := Label{ text: "L" } } }
b: View{ right := View{ b1 := Label{ text: "R" } } }
}
}
}));
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
mod.widgets.ErrShell{ left +: { x := Label{ text: "X" } } }
}));
let (_, errs) = build(&mut cx, d);
assert!(
errs.iter().any(|e| e.contains("no property or named child `left` on View (named children: title, split); `left` is at split.a.left")),
"{errs:?}"
);
}
#[test]
fn a_declared_property_is_not_a_child() {
let mut cx = new_cx();
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
Splitter{ a := View{} }
}));
let (_, errs) = build(&mut cx, d);
assert!(
errs.iter().any(|e| e.contains("`a` is a property of Splitter, not a child: write `a: ...` or `a +: {...}`")),
"{errs:?}"
);
// the property forms work
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
Splitter{ a: View{ k := Label{ text: "K" } } }
}));
let (_, errs) = build(&mut cx, d);
assert!(errs.is_empty(), "{errs:?}");
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
Splitter{ a +: { k := Label{ text: "K" } } }
}));
let (_, errs) = build(&mut cx, d);
assert!(errs.is_empty(), "{errs:?}"); // a plain value under a property's name stays a named entry hosts read as metadata
// (Stage faces: `bind := "node.port"` on a Slider, which has its own `bind`)
let d = cx.with_vm(|vm| vm.eval(script! {
use mod.prelude.widgets.*
Slider{ bind := "deck.gain" }
}));
let (_, errs) = build(&mut cx, d);
assert!(errs.is_empty(), "{errs:?}");
}