Your First App
Goal: At the end of this chapter you have a running Forge app with a window, a label and a button – and you know what every line does.
What you need
- A Mac with Apple Silicon, or a Linux PC (x86-64).
git.- A text editor. Any editor works. SML and SMS are plain text.
Get Forge4D:
git clone https://codeberg.org/CrowdWare/Forge4D.git
cd Forge4D
That is the whole installation. The first time you start an app, run.sh downloads the ready-made Forge Runner for your system into the folder .runtime/. You do not need to compile anything.
The example
Create a new folder for your project, for example ~/forge/first-app, and put two files into it.
app.sml – what is there:
Window {
id: mainWindow
title: "My First Forge App"
size: 480, 320
VBoxContainer {
anchors: left | top | right | bottom
padding: 32, 32, 32, 32
spacing: 16
Label {
id: greeting
text: "Hello, Forge!"
fontSize: 24
}
Button {
id: btnHello
text: "Say hello"
}
}
}
app.sms – what should happen:
var clicks = 0
on mainWindow.ready() {
log.info("My first app is ready")
}
on btnHello.clicked() {
clicks = clicks + 1
greeting.text = "Hello for the ${clicks}. time!"
}
Start it from inside the Forge4D folder:
./run.sh url file://$HOME/forge/first-app/app.sml
A window opens. Press the button three times.

Congratulations. That is a native app, and your script was compiled to machine code before the window appeared.
The structure, line by line
Window {
Every SML file is a tree of nodes. A node has a name, curly braces, and inside them properties and child nodes. Window is the root of this screen.
id: mainWindow
title: "My First Forge App"
size: 480, 320
Properties are written name: value. No equals sign, no semicolon.
idgives the node a name your script can use. It is written without quotes and must be unique in the file.titleis a string, so it is in double quotes.sizeis a pair of whole numbers: width and height in pixels.
VBoxContainer {
anchors: left | top | right | bottom
padding: 32, 32, 32, 32
spacing: 16
A VBoxContainer stacks its children vertically – one below the other. You almost never position things by hand in Forge. You put them into containers and let the containers do the layout.
anchors: left | top | right | bottom– stretch the container to all four edges of the window.padding– space inside the container: left, top, right, bottom.spacing– space between the children.
Label {
id: greeting
text: "Hello, Forge!"
fontSize: 24
}
Button {
id: btnHello
text: "Say hello"
}
A Label shows text. A Button can be pressed. Both have an id, because the script wants to talk to them.
The behaviour, line by line
var clicks = 0
A variable at the top of the script. It lives as long as the screen lives and counts how often the button was pressed.
on mainWindow.ready() {
log.info("My first app is ready")
}
This is an event handler. Read it aloud: on mainWindow ready, do this. The ready event fires once, when the window has been built.
log.info writes a line into the terminal you started the app from. It is your first debugging tool.
on btnHello.clicked() {
clicks = clicks + 1
greeting.text = "Hello for the ${clicks}. time!"
}
On btnHello clicked, do this. Count up, then change the text of the label.
greeting is not a variable – it is the id of the label in app.sml. Every element with an id can be used in the script by that name: greeting.text = … changes it, greeting.text reads it back. Nothing to declare, nothing to look up.
${clicks} inside a string is replaced by the value of clicks. This is called string interpolation.
Notice what is not there: no main function, no event loop, no listener registration, no thread handling. You wrote down two intentions – when the window is ready and when the button is clicked – and the Runner connected them.
How the two files find each other
You started app.sml. The Runner looked for a script with the same name next to it – app.sms – and loaded it. That is the whole convention:
| You start | The Runner also loads |
|---|---|
app.sml |
app.sms |
main.sml |
main.sms |
The connection between them is the id. The id you give a node in SML is the name you use in SMS: btnHello in the SML file, on btnHello.clicked() in the script; greeting in the SML file, greeting.text in the script.
Pitfalls
The handler never fires, or the label does not change. Check the id. on btnhello.clicked() and id: btnHello are not the same name. Copy ids, do not retype them.
The app does not start and the terminal says SMS AOT compile failed. Your script was compiled before the window opened, and the compiler found something it could not translate. The message names the file and usually the variable. Fix it and start again.
Everything is squeezed into the top-left corner. The container has no anchors. Without them it only takes the space its children need.
text: Hello without quotes is not a string. Strings always need double quotes in SML.
size: 480 x 320 does not work. Pairs of numbers are written with a comma: size: 480, 320.
Exercise
- Add a second button,
btnReset, with the text"Reset". - Write a handler for it that sets
clicksback to0and the label back to"Hello, Forge!". - Change the window title to your own name.
The solution is in the appendix – but try it first. You already know everything you need.
What you learned
- A Forge project can be as small as two files:
app.smlandapp.sms. ./run.sh url file://…/app.smlstarts it; the Runner is downloaded on first use.- SML nodes have properties (
name: value) and children; containers do the layout. idconnects structure and behaviour.on id.event() { … }is an event handler;readyfires once,clickedon every press.- An element's
idis its name in the script:greeting.text = "…"changes it. ${…}puts a value into a string.
Next: Part II – Structure
