Panorama Layout DSL
dota-panorama-layout-dsl allows you to write Dota 2 Panorama UI layouts in Kotlin using a type-safe DSL. These files are typically named with the .dota.xml.kts extension and are compiled to standard Panorama XML by ktox-dota.
Benefits
- Type Safety: IDE autocompletion for panel types, attributes.
- Templating: Use Kotlin functions (Templating) and loops to generate repetitive UI elements.
- Refactoring: Safely rename IDs or classes across your project.
- Code Reuse: Store common UI elements in functions and reuse them across multiple layouts, even via libraries.
- Unified Logic: Keep your UI structure and its associated logic in the same language.
Basic Usage
Add the dependency to your Panorama module's build.gradle.kts:
Panorama Classes
@PanoramaView turns a panel subclass into a reusable Panorama class with typed child selectors.
@PanoramaView
class GoodDayHud : Panel(id = "GoodDayHud", classes = "CoolHudClass") {
lateinit var header: Panel
private set
@NoCacheSelector
lateinit var statusLabel: Label
private set
init {
layout {
Panel(id = "Header", classes = "CoolHeaderClass") {
bind(::header)
}
Label(id = "StatusLabel") bind ::statusLabel
}
}
override fun onLoad() {
println("ready")
}
}
What @PanoramaView does
- Generates a JavaScript constructor that creates a panel with
$.CreatePanel(...)and immediately bootstraps it. - Generates
ClassName.bootstrap(panel), which usesktoxGraftClass(...)and then runspanel.__panoramaInit__(). - Maps selector properties from the DSL tree onto the matching child panels on the live panel instance.
- Resolves selector panel types from the declared Kotlin property type (
Label,Panel,List<Image>,Map<String, Panel>, and so on). - Re-queries
@NoCacheSelectorproperties on every access instead of caching the first panel reference.
Collection and template-backed selectors
Selector properties are not limited to single panels. @PanoramaView also supports collection-shaped selectors, so you can describe repeated UI with normal Kotlin templating and still get typed accessors on the bootstrapped panel.
@PanoramaView
class ScoreboardHud : Panel(id = "ScoreboardHud") {
lateinit var playerRows: List<Panel>
private set
lateinit var itemSlots: Map<String, Panel>
private set
init {
layout {
Panel(id = "Rows") {
(1..10).map { Panel(classes = "PlayerRow") } bind ::playerRows
}
Panel(id = "Items") {
itemSlots = mapOf(
"slot0" to Panel(classes = "ItemSlot"),
"slot1" to Panel(classes = "ItemSlot"),
)
}
}
}
}
List<T>properties can be populated fromlistOf(...)or templated expressions such as(1..10).map { ... }.Map<String, T>properties can be populated frommapOf(...)and preserve the keys you declared in Kotlin.- The generated selectors are still type-driven:
List<Image>queriesImagepanels,List<Label>queriesLabelpanels, andMap<String, Panel>queriesPanelinstances regardless of how the right-hand side was written. - For repeated children, order comes from the order of the generated sibling panels in the DSL tree.
Using a Panorama class in layout XML DSL
You can place the class directly inside a .dota.xml.kts file:
By default, @PanoramaView uses snippet = true, so the XML generator emits one <snippet name="GoodDayHud">...</snippet> definition and replaces each usage site with <include snippet="GoodDayHud"/>. If you want the panel tree inlined instead, set @PanoramaView(snippet = false).
The XML generator also ensures the root panel always boots the class by writing an onload attribute. If the panel already has an onload string or an overridden onLoad() function, they are chained after ClassName.bootstrap(this).
Example Layout
Create a file named src/main/layout/my_layout.dota.xml.kts:
root {
styles {
include("s2r://panorama/styles/dotastyles.css")
include("file://{resources}/styles/custom_game/my_styles.css")
}
scripts {
include("file://{resources}/scripts/custom_game/my_script.js")
}
Panel(id = "MainContainer", classes = "${Constants.containerClass} MyCustomClass") {
Label(id = "Title", text = "Welcome to My Custom Game!")
// Use Kotlin loops to generate elements
for (i in 1..5) {
Panel(classes = "ListItem") {
Label(text = "Item #$i")
}
}
Button(id = "StartButton", onactivate = "OnStartClicked()") {
Label(text = "Start Game")
}
Button("BeepButton", onactivate = "Beep()") {
Label(text = "Start Game")
}
}
}
Snippets
The DSL also supports Panorama snippets:
root {
snippets {
snippet("PlayerRow") {
Panel(classes = "PlayerRow") {
Image(id = "HeroIcon")
Label(id = "PlayerName")
}
}
}
// Main layout
Panel(id = "PlayerList") {
// Snippets are instantiated via Javascript at runtime as usual
}
}
Templating
Since the DSL is just Kotlin, you can create reusable UI components (templates) simply by defining functions. This is a powerful alternative to Panorama snippets when you want to share UI logic and structure across multiple layout files or within the same file without the overhead of snippet instantiation.
Creating a Reusable Component
Define a function as an extension of BasePanoramaScope:
fun BasePanoramaScope.PlayerCard(name: String, hero: String, level: Int) {
Panel(classes = "PlayerCard") {
DOTAHeroImage(heroname = hero, heroimagestyle = "icon")
Panel(classes = "PlayerInfo") {
Label(classes = "PlayerName", text = name)
Label(classes = "PlayerLevel", text = "Level $level")
}
}
}
Using the Component
You can then use this function anywhere within a DSL block:
root {
Panel(id = "PlayerList") {
PlayerCard(name = "Player 1", hero = "npc_dota_hero_axe", level = 10)
PlayerCard(name = "Player 2", hero = "npc_dota_hero_crystal_maiden", level = 5)
}
}
Compilation
When using the ktox-dota Gradle plugin, any .dota.xml.kts file in your source directories will be automatically compiled to a .xml file in the generated resources directory, which is then picked up by the Dota 2 engine.
For concrete examples, see the in-repo dota-panorama-example module and the external ktox-dota-example project.