Building the UI
In Nifty you describe what the screen should look like. You don’t create widgets, keep track of them, or update them. Nifty does that.
Windows
Every app has at least one window. Windows go at the top level of the file:
window("Notes", width: 400, height: 300) {
label("Hello")
}
The title is required. width and height are optional; the default size is 480 × 320.
An app can have more than one window. Each is its own window(…) { … } block.
Give a window a name, between window and its title, and code can open and close it
by that name. With open: false it waits for code to open it, rather than opening when
the app starts:
window main("Downloader") {
button("Settings") { open_window(settings) }
}
window settings("Settings", open: false) {
button("Done") { close_window(settings) }
}
Opening a window that’s open brings it to the front. A window shows the app’s state as it is whenever it opens, and the user can close any window themselves. The app quits when its last window closes, whether the user or the code closes it.
Elements
Inside a window you put elements: built-in pieces of UI like label, button and
column. They all look like a call:
label("Hello", style: .title)
- Unnamed arguments come first (
"Hello"), then named ones (style: .title). - Parentheses are always needed, even when empty:
spacer(),column() { … }.
Many elements take a block in { } right after the closing parenthesis, on the
same line. What the block means depends on the element:
| Element | Its block holds |
|---|---|
window, column, row, scroll | the children to show inside it |
button | the action: code that runs when it’s clicked |
text_field | an optional action: code that runs when Return is pressed in it |
drop_target | an optional action: code that runs after something is dropped |
column() { // children
button("Save") { save() } // action
}
The elements are window, column, row, scroll, label, button, text_field,
checkbox, switch, slider, picker, nav_item, image, progress, spacer,
divider, context_menu, drop_target, menu and menu_item. The
UI elements reference lists every one with all its settings.
Menus and shortcuts
Beside its windows, an app can have menus, each a top-level menu holding menu_items,
dividers and submenus. An item runs its block when the user chooses it, or presses its
shortcut:
menu("File") {
menu_item("New Download", shortcut: "n") { new_download() }
divider()
menu("Sort By") {
menu_item("Name") { sort = "name" }
menu_item("Size") { sort = "size" }
}
}
A shortcut always uses Cmd on macOS and Ctrl on Windows, so "n" is Cmd+N on a Mac and
Ctrl+N on a PC; add shift+ or alt+ before the key for more. See
menu for where the menus go on each platform.
A stack can have a menu of its own, shown when the user right-clicks it: put a
context_menu among its children, holding the same items, dividers and submenus. Its items
take no shortcut, and run with the names around them, so in a list each row’s menu acts on
its own item:
for item in downloads {
row() {
context_menu() {
menu_item("Download Again") { start(item.url) }
}
label(item.url)
}
}
Dropping files and links
A stack can take files and links the user drags onto it from the Finder, File Explorer
or a browser: put a drop_target among its children, bound to a [string]. A drop writes
a path for each file and an address for each link into it, then runs the block:
state dropped: [string] = []
column() {
drop_target($dropped) {
for link in dropped {
download(link, to: "~/Downloads", progress: $percent, status: $status)
}
}
label("Drop links here")
}
Showing things conditionally
Use if inside the UI to show elements only sometimes:
if todos.is_empty() {
label("Nothing to do!")
} else {
label("{remaining()} left")
}
Showing a list
Use for to show elements once per item:
for todo in todos {
checkbox(checked: $todo.done, title: todo.title)
}
for index, todo in todos also gives each row its position, and for i in 0..count
shows elements a number of times; see
Positions and counting.
Only UI goes in the UI
A window or view body contains elements, views, if and for. It can’t contain
statements like let or count = 0. Those go in functions or button actions:
Error: only elements, `if` and `for` can appear here; put logic in a function or a button's action
Views: your own elements
When a part of the UI gets big, or you use it more than once, give it a name with
view:
view Header(left: int) {
row(align: .center) {
label("Todos", style: .title)
spacer()
label("{left} left", style: .caption)
}
}
Then use it like any element, giving its parameters by name:
window("Todos") {
column() {
Header(left: remaining())
}
}
By convention view names start with a capital letter (Header), while elements,
functions and values use snake_case (text_field, add_todo).
A view shows what it’s given. When remaining() changes, the Header is rebuilt with
the new value.
Literal vs. live values
Some settings must be written directly in the code, because they shape the window rather than show data:
- layout numbers like
spacing: 12andwidth: 320, - options like
style: .titleandalign: .center, - a window’s title, and a text field’s placeholder.
Text that’s shown, like a label’s text, a button’s title or a checkbox’s title, can be any expression, and stays up to date.
label("{count}") // ✓ live text
window(title_of_app()) { } // ✗ the window title must be written out: window("Todos")