Panels
A panel replaces the keys with an interface you write yourself. Here is the loop it runs, the builders you draw with, and the limits it works inside.
Clink for iOSThe loop
A panel defines view(state), and optionally initial(), on_open(state) and on_action(action, state). Clink calls initial() once for the starting state, on_open each time the panel appears, then view(state) to build the interface. A control either inserts text where you are writing, sets new state, or names an action for on_action to handle, and the view is built again.
def initial():
return {"count": 0, "step": 1}
def view(state):
return vstack([
text(f"{state['count']}", size=34, weight="bold", align="center"),
segmented(["1", "5", "10"], value=str(state["step"]), action="step"),
hstack([
button("−", "bump", value=-1),
button("+", "bump", value=1, style="primary"),
]),
button("Type it", "type", icon="keyboard"),
])
def on_action(action, value, state):
if action == "bump":
state["count"] = state["count"] + value * state["step"]
elif action == "step":
state["step"] = int(value)
elif action == "type":
insert(str(state["count"]))
close()
return stateCalculators, snippet boards, pickers: your panels appear behind the keyboard's panel button.
Builders
view(state) returns nodes made by these 19 functions. Nest them freely: stacks and grids take a list of children.
| Builder | What it draws |
|---|---|
| vstack(children, spacing=6, align="leading") | Stacks its children down the panel. |
| hstack(children, spacing=6, align="center") | Stacks its children across the panel. |
| grid(children, columns=4, spacing=6) | Flows its children into even columns. |
| wrap(children, spacing=6) | Flows its children across the panel and on to the next line. |
| scroll(children, spacing=6) | A scrolling region of its own, for a list longer than the panel. |
| card(children, padding=10, color="") | Groups its children on a surface, with padding around them. |
| spacer(size=0) | Pushes whatever follows it to the far end. |
| divider() | A hairline between rows. |
| text(s, size=17, weight="regular", color="", align="leading", lines=0, mono=False) | A line of text. weight takes regular, medium, semibold, bold, heavy, light or thin; align takes leading, center or trailing; lines caps how many it wraps to; color takes a colour name or #RRGGBB. |
| icon(name, size=20, color="") | An SF Symbol, by name. |
| badge(s, color="") | A short word in a tinted capsule. |
| progress(value, total=1, label="") | A bar filled to value out of total. |
| button(label, action="", value=None, insert="", set=None, style="plain", icon="", enabled=True) | A button. insert types its text into whatever you are writing, set merges keys into state, and action names a handler for on_action. style takes plain, primary, tinted, quiet or destructive. |
| row(title, subtitle="", detail="", icon="", action="", value=None, insert="") | A tappable row: a title, a second line, an icon, and a detail on the right. |
| toggle(label, on=False, key="", action="") | A switch. key writes True or False into that state key. |
| slider(value, min=0, max=100, step=1, label="", key="", action="") | A slider between min and max. key writes the position into that state key. |
| stepper(value, min=0, max=100, step=1, label="", key="", action="") | A value with − and + beside it, between min and max. |
| segmented(options, value=None, key="", action="") | One choice out of a list. key writes the chosen option into that state key. |
| field(key, placeholder="", action="", submit="") | A text box bound to state[key]. Tapping it aims the keys at that key; submit names the handler Return runs. |
Handling a tap
A control that has to work something out names an action instead of carrying a literal set. Clink then calls on_action(action, state), or on_action(action, value, state) when you want the value the control carries. State is an ordinary dictionary, so you can change it in place and return nothing, or return a new one.
def initial():
return {"items": [], "draft": ""}
def view(state):
return vstack([
field("draft", placeholder="Add one…", submit="add"),
scroll([row(item, action="use", value=item) for item in state["items"]]),
])
def on_action(action, value, state):
if action == "add" and value:
state["items"].append(value)
state["draft"] = ""
elif action == "use":
insert(value)
close()
return stateAsking the keyboard
From on_action and on_open a panel can ask the keyboard for things a return value cannot express. Each call is recorded and applied after your function returns, so a panel never edits the field while its own view is being built.
Commands
insert()backspace()delete_word()replace()move_cursor()copy()close()haptic()toast()
def on_action(action, state):
if action == "quote":
word = context()["word"]
replace(len(word), "“" + word + "”")
haptic("light")
return stateState
State is an ordinary dictionary, and it holds whatever a script can build: numbers, strings, booleans, None, and lists and dictionaries of them. It is handed back to you on every call, so a list you appended to on one tap is still there on the next.
def initial():
return {"name": ""}
def view(state):
return vstack([
field("name", placeholder="Who?"),
button("Greet", insert="Hi " + state["name"] + "!", style="primary"),
])Remember state keeps what the panel holds between openings, and between keyboard launches. Leave it off for a calculator; turn it on for a list.
Limits
Each panel render has a 2,000,000-step limit. If a script runs too long, Clink shows an error. Keep loops finite and reduce repeated work if your panel reaches the limit.
A panel that arrives as a file or from a repository is checked before it is stored. It has to be under 48,000 bytes and 1,200 lines, it has to define def view(, every import has to name one of the sandboxed namespaces, and these fragments are refused outright:
Refused in a shared panel
__exec(eval(open(compile(
Importable in a shared panel
jsonmathrandomresystime
Placement
Give each panel its own button in the picker instead of nesting them behind one Panels button. Individual panels can override this in their editor.
Default follows that switch, Standalone always takes its own button, and Grouped always nests.
Writing one
- Open Custom Panels in Clink and tap New panel.
- Write the script. A live preview runs the real panel beside it, and errors show as you type.
- Save, switch the panel on, then open the panel button on the keyboard.
Open the panel in Plugins and select its Share tab to open the export popup and send its QR card. Choose File in the popup to export a .clinkpanel file, which is JSON. Opening one back in Clink stores it with a fresh id, so importing the same panel twice never overwrites the first.