Last Update: 2023-07-06
Creating Screens
API Version: 619045
In Don't Starve Together, screens are full-page UI elements that take over the entire game window. Screens are used for menus, popups, and other full-screen interfaces. This document covers how to create and manage screens in your mods.
Screen Basics
Screens are the top-level UI elements in DST. They inherit from the Widget
class but have special properties for handling input and managing the UI stack.
local Screen = require "widgets/screen"
local Widget = require "widgets/widget"
local Text = require "widgets/text"
-- Create a simple screen
MyScreen = Class(Screen, function(self)
Screen._ctor(self, "MyScreen")
-- Create a root widget for all content
self.root = self:AddChild(Widget("root"))
self.root:SetVAnchor(ANCHOR_MIDDLE)
self.root:SetHAnchor(ANCHOR_MIDDLE)
self.root:SetScaleMode(SCALEMODE_PROPORTIONAL)
-- Add content to the root
self.title = self.root:AddChild(Text(TITLEFONT, 50, "My Screen"))
-- Set default focus
self.default_focus = self.title
end)
Screen Management
Screens are managed by the TheFrontEnd
object, which maintains a stack of screens:
-- Push a new screen onto the stack
TheFrontEnd:PushScreen(MyScreen())
-- Pop the top screen
TheFrontEnd:PopScreen()
-- Replace all screens with a new one
TheFrontEnd:SetScreen(MyScreen())
-- Get the currently active screen
local current_screen = TheFrontEnd:GetActiveScreen()
Screen Lifecycle
Screens have several lifecycle methods that you can override:
function MyScreen:OnBecomeActive()
Screen.OnBecomeActive(self)
-- Called when this screen becomes the active screen
-- Good place to initialize dynamic content
end
function MyScreen:OnBecomeInactive()
Screen.OnBecomeInactive(self)
-- Called when this screen is no longer the active screen
-- Good place to save state or clean up
end
function MyScreen:OnDestroy()
-- Called when the screen is being destroyed
-- Clean up any resources here
self._base.OnDestroy(self)
end
function MyScreen:OnUpdate(dt)
-- Called every frame when the screen is active
-- dt is the time since the last frame in seconds
end
Input Handling
Screens can handle input events from the keyboard, mouse, and controller:
function MyScreen:OnControl(control, down)
if Screen.OnControl(self, control, down) then return true end
if not down and control == CONTROL_CANCEL then
TheFrontEnd:PopScreen()
return true
end
return false
end
function MyScreen:OnRawKey(key, down)
if Screen.OnRawKey(self, key, down) then return true end
if key == KEY_ESCAPE and not down then
TheFrontEnd:PopScreen()
return true
end
return false
end
function MyScreen:OnMouseButton(button, down, x, y)
if Screen.OnMouseButton(self, button, down, x, y) then return true end
if button == MOUSEBUTTON_LEFT and down then
print("Clicked at", x, y)
return true
end
return false
end
Common Screen Types
Popup Screen
A popup screen is a modal dialog that appears on top of the current screen:
local PopupDialogScreen = require "screens/popupdialog"
-- Create a simple popup
local popup = PopupDialogScreen(
"Confirm Action", -- Title
"Are you sure you want to proceed?", -- Body text
{
{
text = "Yes",
cb = function()
print("User confirmed")
TheFrontEnd:PopScreen()
end
},
{
text = "No",
cb = function()
print("User cancelled")
TheFrontEnd:PopScreen()
end
}
}
)
-- Show the popup
TheFrontEnd:PushScreen(popup)
Menu Screen
A menu screen displays a list of options:
local MenuScreen = Class(Screen, function(self, title, options)
Screen._ctor(self, "MenuScreen")
self.root = self:AddChild(Widget("root"))
self.root:SetVAnchor(ANCHOR_MIDDLE)
self.root:SetHAnchor(ANCHOR_MIDDLE)
self.root:SetScaleMode(SCALEMODE_PROPORTIONAL)
-- Add background
self.bg = self.root:AddChild(Image("images/ui.xml", "bg_plain.tex"))
self.bg:SetSize(500, 400)
-- Add title
self.title = self.root:AddChild(Text(TITLEFONT, 40, title))
self.title:SetPosition(0, 150)
-- Create menu
local Menu = require "widgets/menu"
self.menu = self.root:AddChild(Menu(nil, 0, true))
self.menu:SetPosition(0, 0)
-- Add menu items
for _, option in ipairs(options) do
self.menu:AddItem(option.text, option.cb)
end
-- Set default focus
self.default_focus = self.menu
end)
-- Usage example
local options = {
{
text = "Start Game",
cb = function() print("Starting game") end
},
{
text = "Options",
cb = function() print("Opening options") end
},
{
text = "Quit",
cb = function() print("Quitting") end
}
}
local menu_screen = MenuScreen("Main Menu", options)
TheFrontEnd:PushScreen(menu_screen)
Form Screen
A screen with form elements for user input:
local FormScreen = Class(Screen, function(self, title, on_submit)
Screen._ctor(self, "FormScreen")
self.root = self:AddChild(Widget("root"))
self.root:SetVAnchor(ANCHOR_MIDDLE)
self.root:SetHAnchor(ANCHOR_MIDDLE)
self.root:SetScaleMode(SCALEMODE_PROPORTIONAL)
-- Add background
self.bg = self.root:AddChild(Image("images/ui.xml", "bg_plain.tex"))
self.bg:SetSize(500, 400)
-- Add title
self.title = self.root:AddChild(Text(TITLEFONT, 40, title))
self.title:SetPosition(0, 150)
-- Create form elements
local TextEdit = require "widgets/textedit"
self.name_label = self.root:AddChild(Text(NEWFONT, 25, "Name:"))
self.name_label:SetPosition(-100, 50)
self.name_edit = self.root:AddChild(TextEdit(NEWFONT, 25, "", {1,1,1,1}))
self.name_edit:SetPosition(50, 50)
self.name_edit:SetRegionSize(200, 40)
local Spinner = require "widgets/spinner"
self.age_label = self.root:AddChild(Text(NEWFONT, 25, "Age:"))
self.age_label:SetPosition(-100, 0)
local ages = {}
for i = 1, 100 do
table.insert(ages, tostring(i))
end
self.age_spinner = self.root:AddChild(Spinner(ages, 100, 40, {font=NEWFONT, size=25}))
self.age_spinner:SetPosition(50, 0)
-- Add submit button
local ImageButton = require "widgets/imagebutton"
self.submit_button = self.root:AddChild(ImageButton("images/ui.xml", "button.tex", "button_focus.tex"))
self.submit_button:SetPosition(0, -100)
self.submit_button:SetText("Submit")
self.submit_button:SetOnClick(function()
local data = {
name = self.name_edit:GetString(),
age = self.age_spinner:GetSelected()
}
on_submit(data)
TheFrontEnd:PopScreen()
end)
-- Set default focus
self.default_focus = self.name_edit
end)
-- Usage example
local form_screen = FormScreen("User Information", function(data)
print("Name:", data.name)
print("Age:", data.age)
end)
TheFrontEnd:PushScreen(form_screen)
Advanced Screen Techniques
Screen Transitions
You can create smooth transitions between screens:
function MyScreen:TransitionIn()
self.root:SetScale(0.1)
self.root:MoveTo(Vector3(0, 0, 0), Vector3(0, 0, 0), 0.3, function()
-- Transition complete
end)
self.root:ScaleTo(0.1, 1, 0.3)
end
function MyScreen:TransitionOut(cb)
self.root:ScaleTo(1, 0.1, 0.3)
self.root:MoveTo(Vector3(0, 0, 0), Vector3(0, -500, 0), 0.3, function()
if cb then cb() end
end)
end
Screen with Tabs
Create a screen with multiple tabs:
local TabScreen = Class(Screen, function(self, title, tabs)
Screen._ctor(self, "TabScreen")
self.root = self:AddChild(Widget("root"))
self.root:SetVAnchor(ANCHOR_MIDDLE)
self.root:SetHAnchor(ANCHOR_MIDDLE)
self.root:SetScaleMode(SCALEMODE_PROPORTIONAL)
-- Add background
self.bg = self.root:AddChild(Image("images/ui.xml", "bg_plain.tex"))
self.bg:SetSize(600, 400)
-- Add title
self.title = self.root:AddChild(Text(TITLEFONT, 40, title))
self.title:SetPosition(0, 150)
-- Create tab buttons
local TabGroup = require "widgets/tabgroup"
self.tab_group = self.root:AddChild(TabGroup())
self.tab_group:SetPosition(0, 100)
-- Create content area
self.content = self.root:AddChild(Widget("content"))
self.content:SetPosition(0, -50)
-- Add tabs
self.tabs = {}
for i, tab in ipairs(tabs) do
local tab_widget = self.content:AddChild(Widget("tab_" .. i))
tab.build_fn(tab_widget)
tab_widget:Hide()
self.tabs[i] = tab_widget
self.tab_group:AddTab(tab.title, function()
self:ShowTab(i)
end)
end
-- Show first tab by default
self:ShowTab(1)
-- Add close button
local ImageButton = require "widgets/imagebutton"
self.close_button = self.root:AddChild(ImageButton("images/ui.xml", "button.tex", "button_focus.tex"))
self.close_button:SetPosition(0, -150)
self.close_button:SetText("Close")
self.close_button:SetOnClick(function()
TheFrontEnd:PopScreen()
end)
-- Set default focus
self.default_focus = self.tab_group
end)
function TabScreen:ShowTab(index)
for i, tab in ipairs(self.tabs) do
if i == index then
tab:Show()
else
tab:Hide()
end
end
end
-- Usage example
local tabs = {
{
title = "General",
build_fn = function(parent)
local text = parent:AddChild(Text(NEWFONT, 25, "General settings go here"))
text:SetPosition(0, 0)
end
},
{
title = "Audio",
build_fn = function(parent)
local text = parent:AddChild(Text(NEWFONT, 25, "Audio settings go here"))
text:SetPosition(0, 0)
end
},
{
title = "Graphics",
build_fn = function(parent)
local text = parent:AddChild(Text(NEWFONT, 25, "Graphics settings go here"))
text:SetPosition(0, 0)
end
}
}
local tab_screen = TabScreen("Settings", tabs)
TheFrontEnd:PushScreen(tab_screen)
Scrollable Content Screen
Create a screen with scrollable content:
local ScrollableScreen = Class(Screen, function(self, title, content_items)
Screen._ctor(self, "ScrollableScreen")
self.root = self:AddChild(Widget("root"))
self.root:SetVAnchor(ANCHOR_MIDDLE)
self.root:SetHAnchor(ANCHOR_MIDDLE)
self.root:SetScaleMode(SCALEMODE_PROPORTIONAL)
-- Add background
self.bg = self.root:AddChild(Image("images/ui.xml", "bg_plain.tex"))
self.bg:SetSize(600, 400)
-- Add title
self.title = self.root:AddChild(Text(TITLEFONT, 40, title))
self.title:SetPosition(0, 150)
-- Create scrollable list
local ScrollableList = require "widgets/scrollablelist"
local function BuildItem(item, index)
local widget = Widget("list-item")
local text = widget:AddChild(Text(NEWFONT, 20, item.text))
widget.OnGainFocus = function()
text:SetColour(1, 1, 0, 1) -- Yellow on focus
end
widget.OnLoseFocus = function()
text:SetColour(1, 1, 1, 1) -- White when not focused
end
widget.OnSelect = function()
if item.cb then
item.cb()
end
end
return widget
end
self.scroll_list = self.root:AddChild(ScrollableList(
content_items, -- Items array
400, -- Item width
40, -- Item height
8, -- Number of visible items
1, -- Items per row
false -- Not horizontal
))
self.scroll_list:SetPosition(0, 0)
self.scroll_list:SetUpdateFn(BuildItem)
-- Add close button
local ImageButton = require "widgets/imagebutton"
self.close_button = self.root:AddChild(ImageButton("images/ui.xml", "button.tex", "button_focus.tex"))
self.close_button:SetPosition(0, -150)
self.close_button:SetText("Close")
self.close_button:SetOnClick(function()
TheFrontEnd:PopScreen()
end)
-- Set default focus
self.default_focus = self.scroll_list
end)
-- Usage example
local items = {}
for i = 1, 20 do
table.insert(items, {
text = "Item " .. i,
cb = function() print("Selected item", i) end
})
end
local scroll_screen = ScrollableScreen("Scrollable Content", items)
TheFrontEnd:PushScreen(scroll_screen)
Best Practices for Screen Development
- Root Widget: Always create a root widget for your screen content to manage layout properly
- Default Focus: Set
self.default_focus
to ensure controller navigation works correctly - Cleanup: Override
OnDestroy
to clean up any resources when the screen is closed - Input Handling: Always call the parent method first in input handlers and return true if handled
- Transitions: Add smooth transitions for a polished user experience
- Error Handling: Wrap callbacks in pcall to prevent crashes from user input
- Accessibility: Ensure all interactive elements can be navigated with a controller
- Consistency: Follow DST's UI style for a consistent user experience
- Performance: Minimize the number of widgets and avoid creating them frequently
- Testing: Test your screens on different resolutions and with both mouse and controller input
Example: Complete Custom Screen
Here's a complete example of a custom screen for a mod settings menu:
local Screen = require "widgets/screen"
local Widget = require "widgets/widget"
local Text = require "widgets/text"
local Image = require "widgets/image"
local ImageButton = require "widgets/imagebutton"
local Spinner = require "widgets/spinner"
local TextEdit = require "widgets/textedit"
local TEMPLATES = require "widgets/templates"
-- Create a settings screen for a mod
ModSettingsScreen = Class(Screen, function(self, mod_name, settings, on_save)
Screen._ctor(self, "ModSettingsScreen")
self.mod_name = mod_name
self.settings = settings
self.on_save = on_save
-- Create root widget
self.root = self:AddChild(Widget("root"))
self.root:SetVAnchor(ANCHOR_MIDDLE)
self.root:SetHAnchor(ANCHOR_MIDDLE)
self.root:SetScaleMode(SCALEMODE_PROPORTIONAL)
-- Add dark background
self.black = self.root:AddChild(TEMPLATES.BackgroundTint(0.7))
-- Create panel
self.panel = self.root:AddChild(TEMPLATES.RectangleWindow(500, 450))
-- Add title
self.title = self.panel:AddChild(Text(TITLEFONT, 40, mod_name .. " Settings"))
self.title:SetPosition(0, 180)
-- Create settings container
self.settings_root = self.panel:AddChild(Widget("settings_root"))
self.settings_root:SetPosition(0, 50)
-- Create settings controls based on settings types
local y_offset = 100
self.controls = {}
for id, setting in pairs(settings) do
local label = self.settings_root:AddChild(Text(NEWFONT, 25, setting.label))
label:SetPosition(-150, y_offset)
label:SetHAlign(ANCHOR_RIGHT)
if setting.type == "spinner" then
local spinner = self.settings_root:AddChild(Spinner(
setting.options,
200,
40,
{font=NEWFONT, size=25},
function(selected)
self.settings[id].value = selected
end
))
spinner:SetPosition(50, y_offset)
spinner:SetSelected(setting.value)
self.controls[id] = spinner
elseif setting.type == "checkbox" then
local checkbox = self.settings_root:AddChild(TEMPLATES.Checkbox(
"",
setting.value,
function(checked)
self.settings[id].value = checked
end
))
checkbox:SetPosition(0, y_offset)
self.controls[id] = checkbox
elseif setting.type == "text" then
local text_edit = self.settings_root:AddChild(TextEdit(
NEWFONT,
25,
setting.value,
{1,1,1,1}
))
text_edit:SetPosition(50, y_offset)
text_edit:SetRegionSize(200, 40)
text_edit:SetOnTextInputted(function(text)
self.settings[id].value = text
end)
self.controls[id] = text_edit
end
y_offset = y_offset - 50
end
-- Add save button
self.save_button = self.panel:AddChild(TEMPLATES.StandardButton(
function()
self:Save()
end,
"Save"
))
self.save_button:SetPosition(-80, -150)
-- Add cancel button
self.cancel_button = self.panel:AddChild(TEMPLATES.StandardButton(
function()
TheFrontEnd:PopScreen()
end,
"Cancel"
))
self.cancel_button:SetPosition(80, -150)
-- Set default focus
self.default_focus = self.settings_root
end)
function ModSettingsScreen:Save()
local result = {}
-- Extract values from settings
for id, setting in pairs(self.settings) do
result[id] = setting.value
end
-- Call save callback
if self.on_save then
self.on_save(result)
end
TheFrontEnd:PopScreen()
end
function ModSettingsScreen:OnControl(control, down)
if Screen.OnControl(self, control, down) then return true end
if not down and control == CONTROL_CANCEL then
TheFrontEnd:PopScreen()
return true
end
return false
end
-- Usage example
local settings = {
difficulty = {
label = "Difficulty",
type = "spinner",
options = {"Easy", "Normal", "Hard"},
value = "Normal"
},
spawn_monsters = {
label = "Spawn Monsters",
type = "checkbox",
value = true
},
player_name = {
label = "Player Name",
type = "text",
value = "Player"
}
}
local settings_screen = ModSettingsScreen("My Mod", settings, function(result)
print("Settings saved:")
for k, v in pairs(result) do
print(k, "=", v)
end
end)
TheFrontEnd:PushScreen(settings_screen)