First Widgets
On this page you will learn about the JSX syntax. To learn about it more in depth you can read the Gnim docs.
TIP
gnim symbols is reexported from the ags module.
Entry point of applications
Every application's entry point is an app.start invocation. app is a singleton instance of Gtk.Application.
import app from "ags/gtk4/app"
app.start({
main() {
// you will instantiate Widgets here
// and setup anything else if you need
},
})Root of every shell component: Window
Desktop Shells are composed of widgets. A widget is a piece of UI that has its own logic and style. A widget can be as small as a button or an entire bar. The top level - also known as a root - widget is always a Window.
function Bar(monitor = 0) {
return (
<window visible class="Bar" monitor={monitor}>
<box>Content of the widget</box>
</window>
)
}
app.start({
main() {
Bar(0)
Bar(1) // instantiate for each monitor
},
})IMPORTANT
In Gtk4 unlike other widgets, window widgets are not visible by default. Don't forget to explicitly make it visible.
Creating and nesting widgets
Widgets are JavaScript functions which return GObject.Object (usually Gtk.Widget) instances by using JSX expressions.
function MyButton() {
return (
<button onClicked={(self) => console.log(self, "clicked")}>
<label label="Click me!" />
</button>
)
}Now that you have declared MyButton, you can nest it into another component.
function MyBar() {
return (
<window visible>
<box>
Click The button
<MyButton />
</box>
</window>
)
}Notice that widgets you defined start with a capital letter <MyButton />. Lowercase tags are builtin intrinsic widgets, while capital letter is for custom widgets.
Displaying Data
JSX lets you put markup into JavaScript. Curly braces let you “escape back” into JavaScript so that you can embed some variable from your code and display it.
function MyWidget() {
const label = "hello"
return <button>{label}</button>
}You can also pass JavaScript to markup attributes
function MyWidget() {
const label = "hello"
return <button label={label} />
}Conditional Rendering
You can use the same techniques as you use when writing regular JavaScript code. For example, you can use an if statement to conditionally include JSX:
function MyWidget() {
let content
if (condition) {
content = <True />
} else {
content = <False />
}
return <box>{content}</box>
}You can also inline a conditional ? (ternary) expression.
function MyWidget() {
return <box>{condition ? <True /> : <False />}</box>
}When you don’t need the else branch, you can also use a shorter logical && syntax:
function MyWidget() {
return <box>{condition && <True />}</box>
}TIP
falsy values are not rendered.
Rendering lists
You can use for loops or array map() function.
function MyWidget() {
const labels = ["label1", "label2", "label3"]
return (
<box>
{labels.map((label) => (
<label label={label} />
))}
</box>
)
}Widget signal handlers
You can respond to events by declaring event handler functions inside your widget:
import Gtk from "gi://Gtk"
function MyButton() {
function onClicked(self: Gtk.Button) {
console.log(self, "was clicked")
}
return <button onClicked={onClicked} />
}TIP
Using Gtk4, you can use EventControllers for more complex event handling.
<box>
<Gtk.GestureClick
propagationPhase={Gtk.PropagationPhase.CAPTURE}
button={Gdk.BUTTON_PRIMARY}
onPressed={() => print("clicked with primary button")}
/>
</box>How properties are passed
Using JSX, a custom widget will always have a single object as its parameter.
type Props = {
myprop: string
children?: JSX.Element | Array<JSX.Element>
}
function MyWidget({ myprop, children }: Props) {
//
}TIP
JSX.Element is an alias to GObject.Object
The children property is a special one which is used to pass the children given in the JSX expression.
// `children` prop of MyWidget is the box
return (
<MyWidget myprop="hello">
<box />
</MyWidget>
)// `children` prop of MyWidget is [box, box]
return (
<MyWidget myprop="hello">
<box />
<box />
</MyWidget>
)State management
State is managed using signals which are called Accessor.
- with
createStateyou can instantiate a writable reactive value - with
createBindingyou can hook into GObject properties. - with
createComputedyou can derive reactive values
import { createState, createComputed } from "ags"
function Counter() {
const [count, setCount] = createState(0)
function increment() {
setCount((v) => v + 1)
}
const label = createComputed(() => count().toString())
return (
<box>
<label label={label} />
<button onClicked={increment}>Click to increment</button>
</box>
)
}import GObject, { register, property } from "ags/gobject"
import { createBinding, createComputed } from "ags"
@register()
class CounterStore extends GObject.Object {
@property(Number) count = 0
}
function Counter() {
const counter = new CounterStore()
function increment() {
counter.count += 1
}
const count = createBinding(count, "count")
const label = createComputed(() => count().toString())
return (
<box>
<label label={label} />
<button onClicked={increment}>Click to increment</button>
</box>
)
}Notice how in the createComputed body count is called as a function to track it automatically as a dependency for the derived label property.
TIP
There is a shorthand for createComputed.
// these two lines mean and do the same thing
const label = createComputed(() => count().toString())
const label = count((c) => c.toString())Integrating external programs
Other than the aforementioned functions to manage state, AGS provides ways to integrate CLI tools you might be already familiar with: createPoll which polls a program at each given interval and createSubprocess which launches a given program and monitors its standard output.
As an example let's say you want to use the date CLI command to get a formatted date.
const date = createPoll("", 1000, `bash -c "date +%H:%M"`)
return <label label={date} />WARNING
Running subprocesses are relatively expensive, so always prefer to use a library when available.
In reality you would use GLib.DateTime or JavaScript's Date. In newer version of GJS (1.85.2 >=) you can also use the new Temporal JavaScript builtin.
const date = createPoll("", 1000, () => new Date().toString())
return <label label={date} />Avoid polling when possible.
Keep in mind that polling is generally considered bad practice. You should use events and signals whenever possible which will only do operations when necessary.
Dynamic rendering
When you want to render based on a value, you can use the <With> component.
import { With, Accessor } from "ags"
let value: Accessor<{ member: string } | null>
return (
<box>
<With value={value}>
{(value) => value && <label label={value.member} />}
</With>
</box>
)TIP
In most cases it is better to always render the component and set its visible property instead. Use <With> in cases when you need to unpack a nullable object or when you need to access nested values.
WARNING
When the value changes and the widget is re-rendered the previous one is removed from the parent component and the new one is appended. Order of widgets are not kept so make sure to wrap <With> in a container to avoid it. This is due to Gtk not having a generic API on containers to sort widgets.
Dynamic list rendering
The <For> component let's you render based on an array dynamically. Each time the array changes it is compared with its previous state. Widgets for new items are inserted while widgets associated with removed items are removed.
import { For, Accessor } from "ags"
let list: Accessor<Array<any>>
return (
<box>
<For each={list}>
{(item, index: Accessor<number>) => (
<label label={index((i) => `${i}. ${item}`)} />
)}
</For>
</box>
)WARNING
Similarly to <With>, when the list changes and a new item is added it is simply appended to the parent. Order of widgets are not kept so make sure to wrap <For> in a container to avoid this.