Skip to content

Building UI

The page event fires every time a screen opens. page.top is the top part (the whole screen on single-screen devices) and page.bottom the bottom screen on dual-screen handhelds (nil otherwise).

nari.on("page", function(name, page)
if name ~= "NowPlaying" then
return
end
local button = NariButton.new()
button.text = "Hi!"
button.custom_minimum_size = Vector2(240, 96)
button.pressed:connect(function()
local dialog = Dialog.menu("Hello from a plugin", "It works")
dialog:add_option("Close", function() end)
dialog:popup()
end)
page.top:add_child(button)
end)
  • Find nodes inside a screen with Godot’s own methods: page.top:find_child("Header", true, false), page.top:get_node("%List"), get_children()…
  • Use the theme’s colors with ThemeEngine:get_color(ThemeEngine.Colors.AccentBg), and repaint when the theme changes with ThemeEngine:follow(node, handler).
  • Nodes the plugin creates are freed when it’s turned off, so a button you added goes away with it.
Class Notes
NariButton The app’s button. text, icon, font_size, bg_color / fg_color / gradient_color (a ThemeEngine.Colors value), pressed signal.
NariLabel A label that follows the theme. text, font_size, weight, marquee.
Snackbar Snackbar.post(title, message, action) shows a message at the bottom for a few seconds. message and action are optional; action runs when it’s tapped.
Dialog Dialog.menu(title, subtitle), then :add_option(text, action), :add_toggle(text, on, toggled), :add_button(text, action, style, closes) and :popup(). style is Dialog.Style.NORMAL, PRIMARY or DANGER. Dialog.confirm(message, accept_text, on_accept, danger) shows right away.
GlyphIcon Shows the controller or keyboard glyph for an InputGlyphs.Glyph.
Icon A themed SVG icon.
ScrollList, SettingItem The same pieces Nari’s screens use. See ui/components and ui/shared in Nari’s code.
WebView A web page over Nari. See Web views.
nari.home_entry({
title = "Random picks",
subtitle = "A new mix every time you open it",
icon = "res://assets/icons/shuffle.svg",
color = "Purple",
action = function() nari.log("opened!") end,
})

Entries show up before Settings, on single and dual screen Home. color is a theme color family: Blue, Purple, Warning, Green, Red, Yellow, Pink, Orange, Teal, Cyan, Indigo, Lime, Success, Error, Accent or Gray (the default). icon is a path in the plugin folder, a res:// path or a texture.

The Tracks screen can show any list of songs:

Main:change_page(Main.Pages.Tracks, Dictionary({
tracks = songs,
views = Dictionary({ single = "stair", top = "circle", bottom = "stair" }),
}))
  • tracks is a list of songs, the same as MediaPlayer:get_tracks() gives, shown in that order.
  • views is optional and picks the layout: single for one screen (circle, ipod, vertical, stair, grid, compact), top and bottom for dual screens. Without it, the user’s Tracks layout is used.
  • Picking a song plays the list from it. The hints add Shuffle and Play all.
nari.now_playing_tab({
id = "hello",
title = "Hello",
icon = "res://assets/icons/notifications.svg",
build = function(area)
local label = NariLabel.new()
label.text = "Hello World!"
area:add_child(label)
end,
})

The tab goes into the Now Playing tab bar after the built-in ones. build runs every time Now Playing opens, with the tab’s page (a MarginContainer) to fill. It shows and hides like the Queue and Effects pages.

Discover, the first entry of Home, is a page of sections built from what the user plays and likes. Add your own:

nari.discover_section({
id = "trending",
title = "Trending now",
kind = "tracks",
order = 15,
items = function(done)
nari.http({ url = "https://example.com/trending" }, function(response)
done(response and nari.json_decode(response.body) or {})
end)
end,
})
Key
title Shown above the section.
kind quick_picks (a grid of songs), tracks (song cards), track_list (up to 5 song rows), albums, artists or playlists.
items A function that returns the list, or calls done(list) later, with the same keys as the library lists.
order Where the section goes. Built-in ones use 0 (Quick picks) to 60, lower comes first. 50 by default.
id Optional name for the section.

Sections without items are hidden. Songs play the section from the one picked, and albums, artists and playlists open their screens.

nari.item_action({
title = "Share link",
kinds = { "track", "album" },
visible = function(kind, item) return item.id ~= nil end,
action = function(kind, item) nari.open_url("https://example.com/" .. kind .. "/" .. item.id) end,
})

The option shows up in the menu of every item of those kinds (track, album, artist, playlist; all of them when left out), after Nari’s own options. visible is optional and hides it for some items.

nari.webview(spec) opens a web page as a full Nari screen and returns a handle, or nil when the device can’t show web pages. Going back closes it. It’s the easy way to sign in to a service:

local web = nari.webview({
title = "Sign in",
url = "https://example.com/login",
incognito = true,
on_load = function(web, url)
if url:find("/home", 1, true) then
local cookies = web:cookies("https://example.com")
web:close()
end
end,
})
Spec field
title, url Header title and first page.
service The name in “Enter your … account” on the top screen of dual-screen devices. The plugin’s name by default.
screen 0 follows the screen the page is on, 1 always uses the main window, 2 the second screen.
incognito Nothing is kept: cookies and storage are gone when it closes.
desktop_mode true by default: pages load their desktop version, also on phones.
user_agent Replaces the browser’s user agent.
scripts JavaScript run at the start of every page, before the page’s own scripts.
transparent, devtools, autoplay, zoom Page background, the inspector, media autoplay and zoom.
on_start, on_load, on_title, on_message, on_close A page started or finished loading, its title changed, it called window.ipc.postMessage(text), or it closed.

The handle has web:load(url), web:eval(script, done), web:cookies(url) (a Cookie header with every cookie for that URL, HttpOnly ones too), web:close() and web.view, the WebView node.

WebView is a Control that draws a native browser where it’s placed, so nothing Nari draws can cover it. On Linux it needs X11 and WebKitGTK 4.1 (the Flatpak has it), and on Windows WebView2 (part of Windows 10 and 11).