Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

ElementIts block holds
window, column, row, scrollthe children to show inside it
buttonthe action: code that runs when it’s clicked
text_fieldan optional action: code that runs when Return is pressed in it
drop_targetan 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.

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)
  }
}

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: 12 and width: 320,
  • options like style: .title and align: .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")