Skip to content

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:

dependencies {
    implementation("com.isycat.ktox:dota-panorama-layout-dsl:<version>")
}

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 uses ktoxGraftClass(...) and then runs panel.__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 @NoCacheSelector properties 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 from listOf(...) or templated expressions such as (1..10).map { ... }.
  • Map<String, T> properties can be populated from mapOf(...) and preserve the keys you declared in Kotlin.
  • The generated selectors are still type-driven: List<Image> queries Image panels, List<Label> queries Label panels, and Map<String, Panel> queries Panel instances 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:

root {
    Panel(id = "HeroInfoArea") {
        GoodDayHud()
    }
}

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.