State & actions
A screen you can only look at is a mockup. Three things in the inspector's Connections tab turn a document into a component you can wire up: state variables, bindings, and actions.
State variables#
With nothing selected, the inspector shows the document's own panel. Its state table takes a name, a type, and a starting value.
| Type | Becomes | Initial written as |
|---|---|---|
| text | Signal<String> |
anything |
| bool | Signal<bool> |
true / false |
| int | Signal<i64> |
a whole number |
| float | Signal<f64> |
a number |
| items | Signal<Vec<String>> |
one per line |
Each becomes a public Signal<T> field on the generated type. A
Signal is guise's reactive cell: read it during render and
the component redraws when it changes.
The name becomes a Rust identifier, so Email Address becomes email_address
and type becomes type_. Whatever it turns into, no state variable and no
generated field will ever collide — the generator seeds its name table with your
variable names first, so a text input called Email next to a variable called
email gives you email_field and email, not two emails.
Bindings#
Select a component, and in Connections bind a prop to a variable instead of giving it a literal. The canvas shows the variable's starting value, so you can see what the first frame will look like.
A binding is two-way. guise has two shapes for that, and which one you get depends on the kind of component — the same split described in components:
A stateful entity — a text input, a select, a slider — binds with a call after both sides exist:
let query = Signal::new(cx, "".to_string());
let search = cx.new(|cx| {
TextInput::new(cx)
.placeholder("Name or role")
.label("Search")
});
TextInput::bind(&search, &query, cx);
A controlled builder — a checkbox, a switch, a chip, a rating — binds in the builder chain, because it has no constructor to bind after:
Switch::new("node-16")
.bind(self.only_active.binding())
.label("Active only")
Either way, typing writes to the signal and setting the signal updates the control.
Two details follow from that, and both are visible in the output:
- The setter the binding drives is not also emitted. A bound text input's
chain has no
.value(..)in it, becauseTextInput::bindsets the value. Emitting both would leave them fighting over it. - The signals are built first. In the generated constructor, every
Signal::newcomes before the fields, because a field that reads a signal reads it while it is being built — and because a binding needs both sides to exist before it can be made.
Binding a prop that is not the one guise binds two-way — a placeholder, a label — is a one-shot read of the signal at construction. That is still useful and it still compiles; it just does not write back.
Actions#
The same document panel takes actions. Each becomes a method on the generated type:
/// Clear the form.
pub fn submit(&mut self, cx: &mut Context<Self>) {
self.email.set(cx, String::new());
cx.notify();
}
Click an action to write it. That opens a Rust buffer over the body, with
line numbers, highlighting, the signature it generates shown above it, and what
the handler can reach listed below — the document's state signals, and the
entity fields codegen will give it, self.-prefixed the way the method reaches
them. The name field renames the action and follows every control wired to it;
"What it does" becomes the doc comment.
⌃Space accepts a completion and ⌃N / ⌃P walk them. What is offered is what is in
scope, ranked above Rust's own keywords — typing em in a handler means
email, not emit. Nothing is offered after a .: this completer does not
know types, and guessing a method would be worse than staying quiet.
The body lives in the .tailor file, not in the export. That is what makes
it survive: it is design data like everything else, so regenerating rewrites the
file around your code rather than over it. You never have to protect an export
from the next Run. An action with no body is marked empty in the list and
generates // TODO, which is the difference between a project and a mockup.
Tailor never runs your code while you design — it places a method where the handler belongs. Run is what runs it.
For anything bigger than a handler, add a module and call into it.
Events#
Select a component and connect one of its events to an action. Which events a
component has comes from the catalog: a button has click, a select has
change, a nav link has click, a modal has close.
How the connection is generated depends, again, on the kind of component:
- A builder takes a handler:
.on_click(cx.listener(|this, _event, _window, cx| this.add_person(cx))) - An entity emits, so the wiring goes in the constructor:
cx.subscribe(&select, |this, _entity, _event, cx| this.pick(cx)).detach(); - A builder inside one of the drawn containers goes through a weak
handle, because those regions are
'staticclosures and a borrowed context cannot outlive the method that made it:
.header(64., {
let view = cx.entity().downgrade();
move |_window, _cx| {
// …
Button::new("node-11", "Add person")
.on_click({
let view = view.clone();
move |_event, _window, cx| {
view.update(cx, |this, cx| this.add_person(cx)).ok();
}
})
}
})
The handle is weak because a live component tree must not own the view that
renders it, and it is cloned again per handler because the region closure is
Fn — a move handler inside it would move the shared handle out of the
closure that owns it.
You do not have to think about any of that. It is here because it is what you will read in the file, and a generated file you cannot read is a generated file you cannot own.
Problems#
The lint pass runs on every edit — off the main thread, debounced — and the Problems panel (⌥⌘4) shows what it found.
Errors, which mean the document will not generate or will not compile:
- a prop bound to a variable you renamed or deleted
- an event pointing at an action that is gone
- a component reference to a document that no longer exists
- two documents that generate the same Rust type name
- a document whose name collides with a guise component
- a component that would contain itself
Warnings, which mean it probably was not meant:
- a stateful component inside
Tabs,AccordionorSplitPanel— their regions are'staticclosures, so extract that part into its own component - an event wired to no action
- a button with no label, an image with no source, an empty container
- a node pushed outside its parent
Clicking a row opens the document and selects the node. Right-clicking offers Reveal and copy the message.
A clean Problems panel is not a promise that your app is right. It is a promise that the file will generate and the generated file will compile.