diff options
Diffstat (limited to 'docs/guide')
-rw-r--r-- | docs/guide/ags/binding.md | 234 | ||||
-rw-r--r-- | docs/guide/ags/faq.md | 189 | ||||
-rw-r--r-- | docs/guide/ags/first-widgets.md | 41 | ||||
-rw-r--r-- | docs/guide/ags/gobject.md | 166 | ||||
-rw-r--r-- | docs/guide/ags/utilities.md | 2 | ||||
-rw-r--r-- | docs/guide/ags/variable.md | 72 |
6 files changed, 502 insertions, 202 deletions
diff --git a/docs/guide/ags/binding.md b/docs/guide/ags/binding.md new file mode 100644 index 0000000..f1592a0 --- /dev/null +++ b/docs/guide/ags/binding.md @@ -0,0 +1,234 @@ +# Binding + +As mentioned before binding an object's state to another - +so in most cases a `Variable` or a `GObject.Object` property to a widget's property - +is done through the `bind` function which returns a `Binding` object. + +`Binding` objects simply hold information about the source and how it should be transformed +which Widget constructors can use to setup a connection between themselves and the source. + +```ts +class Binding<Value> { + private transformFn: (v: any) => unknown + private emitter: Subscribable<Value> | Connectable + private prop?: string + + as<T>(fn: (v: Value) => T): Binding<T> + get(): Value + subscribe(callback: (value: Value) => void): () => void +} +``` + +A `Binding` can be constructed from an object implementing +the `Subscribable` interface (usually a `Variable`) +or an object implementing the `Connectable` interface and one of its properties +(usually a `GObject.Object` instance). + +```ts +function bind<T>(obj: Subscribable): Binding<T> + +function bind< + Obj extends Connectable, + Prop extends keyof Obj, +>(obj: Obj, prop: Prop): Binding<Obj[Prop]> +``` + +## Subscribable and Connectable interface + +Any object implementing one of these interfaces can be used with `bind`. + +```ts +interface Subscribable<T> { + subscribe(callback: (value: T) => void): () => void + get(): T +} + +interface Connectable { + connect(signal: string, callback: (...args: any[]) => unknown): number + disconnect(id: number): void +} +``` + +## Example Custom Subscribable + +When binding the children of a box from an array, usually not all elements +of the array changes each time, so it would make sense to not destroy +the widget which represents the element. + +::: code-group + +```ts :line-numbers [varmap.ts] +import { type Subscribable } from "astal/binding" +import { Gtk } from "astal" + +export class VarMap<K, T = Gtk.Widget> implements Subscribable { + #subs = new Set<(v: Array<[K, T]>) => void>() + #map: Map<K, T> + + #notifiy() { + const value = this.get() + for (const sub of this.#subs) { + sub(value) + } + } + + #delete(key: K) { + const v = this.#map.get(key) + + if (v instanceof Gtk.Widget) { + v.destroy() + } + + this.#map.delete(key) + } + + constructor(initial?: Iterable<[K, T]>) { + this.#map = new Map(initial) + } + + set(key: K, value: T) { + this.#delete(key) + this.#map.set(key, value) + this.#notifiy() + } + + delete(key: K) { + this.#delete(key) + this.#notifiy() + } + + get() { + return [...this.#map.entries()] + } + + subscribe(callback: (v: Array<[K, T]>) => void) { + this.#subs.add(callback) + return () => this.#subs.delete(callback) + } +} +``` + +::: + +And this `VarMap<key, Widget>` can be used as an alternative to `Variable<Array<Widget>>`. + +```tsx +function MappedBox() { + const map = new VarMap([ + [1, <MyWidget id={id} />] + [2, <MyWidget id={id} />] + ]) + + const conns = [ + gobject.connect("added", (_, id) => map.set(id, MyWidget({ id }))), + gobject.connect("removed", (_, id) => map.delete(id, MyWidget({ id }))), + ] + + return <box onDestroy={() => conns.map(id => gobject.disconnect(id))}> + {bind(map).as(arr => arr.sort(([a], [b]) => a - b).map(([,w]) => w))} + </box> +} +``` + +## Example Custom Connectable + +This was formerly known as a "Service" in AGS. +Astal provides [decorator functions](./gobject#example-usage) that make it easy to subclass gobjects, however +you can read more about GObjects and subclassing on [gjs.guide](https://gjs.guide/guides/gobject/subclassing.html#gobject-subclassing). + +Objects coming from [libraries](../libraries/references#astal-libraries) +usually have a singleton gobject you can access with `.get_default()`. + +Here is an example of a Brightness library by wrapping the `brightnessctl` cli utility +and by monitoring `/sys/class/backlight` + +::: code-group + +```ts :line-numbers [brightness.ts] +import GObject, { register, property } from "astal/gobject" +import { monitorFile, readFileAsync } from "astal/file" +import { exec, execAsync } from "astal/process" + +const get = (args: string) => Number(exec(`brightnessctl ${args}`)) +const screen = exec(`bash -c "ls -w1 /sys/class/backlight | head -1"`) +const kbd = exec(`bash -c "ls -w1 /sys/class/leds | head -1"`) + +@register({ GTypeName: "Brightness" }) +export default class Brightness extends GObject.Object { + static instance: Brightness + static get_default() { + if (!this.instance) + this.instance = new Brightness() + + return this.instance + } + + #kbdMax = get(`--device ${kbd} max`) + #kbd = get(`--device ${kbd} get`) + #screenMax = get("max") + #screen = get("get") / (get("max") || 1) + + @property(Number) + get kbd() { return this.#kbd } + + set kbd(value) { + if (value < 0 || value > this.#kbdMax) + return + + execAsync(`brightnessctl -d ${kbd} s ${value} -q`).then(() => { + this.#kbd = value + this.notify("kbd") + }) + } + + @property(Number) + get screen() { return this.#screen } + + set screen(percent) { + if (percent < 0) + percent = 0 + + if (percent > 1) + percent = 1 + + execAsync(`brightnessctl set ${Math.floor(percent * 100)}% -q`).then(() => { + this.#screen = percent + this.notify("screen") + }) + } + + constructor() { + super() + + const screenPath = `/sys/class/backlight/${screen}/brightness` + const kbdPath = `/sys/class/leds/${kbd}/brightness` + + monitorFile(screenPath, async f => { + const v = await readFileAsync(f) + this.#screen = Number(v) / this.#screenMax + this.notify("screen") + }) + + monitorFile(kbdPath, async f => { + const v = await readFileAsync(f) + this.#kbd = Number(v) / this.#kbdMax + this.notify("kbd") + }) + } +} +``` + +::: + +And it can be used like any other library object. + +```tsx +function BrightnessSlider() { + const brightness = Brightness.get_default() + + return <slider + value={bind(brightness, "screen")} + onDragged={({ value }) => brightness.screen = value} + /> +} +``` diff --git a/docs/guide/ags/faq.md b/docs/guide/ags/faq.md index cb5d609..76d8e72 100644 --- a/docs/guide/ags/faq.md +++ b/docs/guide/ags/faq.md @@ -2,22 +2,22 @@ ## Monitor id does not match compositor -The monitor property that windows expect is mapped by Gdk, which is not always +The monitor id property that windows expect is mapped by Gdk, which is not always the same as the compositor. Instead use the `gdkmonitor` property which expects -a `Gdk.Monitor` object which you can get from compositor libraries. - -Example with Hyprland +a `Gdk.Monitor` object. ```tsx -import Hyprland from "gi://AstalHyprland" +import { App } from "astal" function Bar(gdkmonitor) { return <window gdkmonitor={gdkmonitor} /> } function main() { - for (const m of Hyprland.get_default().get_monitors()) { - Bar(m.gdk_monitor) + for (const monitor of App.get_monitors()) { + if (monitor.model == "your-desired-model") { + Bar(monitor) + } } } @@ -53,7 +53,7 @@ import GLib from "gi://GLib" const HOME = GLib.getenv("HOME") ``` -## Custom svg symbolic icons +## Custom SVG symbolic icons Put the svgs in a directory, named `<icon-name>-symbolic.svg` and use `App.add_icons` or `icons` parameter in `App.start` @@ -90,74 +90,6 @@ print("print this line to stdout") printerr("print this line to stderr") ``` -## Binding custom structures - -The `bind` function can take two types of objects. - -```ts -interface Subscribable<T = unknown> { - subscribe(callback: (value: T) => void): () => void - get(): T -} - -interface Connectable { - connect(signal: string, callback: (...args: any[]) => unknown): number - disconnect(id: number): void -} -``` - -`Connectable` is for mostly gobjects, while `Subscribable` is for `Variables` -and custom objects. - -For example you can compose `Variables` in using a class. - -```ts -type MyVariableValue = { - number: number - string: string -} - -class MyVariable { - number = Variable(0) - string = Variable("") - - get(): MyVariableValue { - return { - number: this.number.get(), - string: this.string.get(), - } - } - - subscribe(callback: (v: MyVariableValue) => void) { - const unsub1 = this.number.subscribe((value) => { - callback({ string: value, number: this.number.get() }) - }) - - const unsub2 = this.string.subscribe((value) => { - callback({ number: value, string: this.string.get() }) - }) - - return () => { - unsub1() - unsub2() - } - } -} -``` - -Then it can be used with `bind`. - -```tsx -function MyWidget() { - const myvar = new MyVariable() - const label = bind(myvar).as(({ string, number }) => { - return `${string} ${number}` - }) - - return <label label={label} /> -} -``` - ## Populate the global scope with frequently accessed variables It might be annoying to always import Gtk only for `Gtk.Align` enums. @@ -289,91 +221,46 @@ return <RegularWindow </RegularWindow> ``` -## Avoiding unnecessary re-rendering - -As mentioned before, any object can be bound that implements the `Subscribable` interface. - -```ts -interface Subscribable<T = unknown> { - subscribe(callback: (value: T) => void): () => void - get(): T -} -``` - -This can be used to our advantage to create a reactive `Map` object. - -```ts -import { type Subscribable } from "astal/binding" -import { Gtk } from "astal" - -export class VarMap<K, T = Gtk.Widget> implements Subscribable { - #subs = new Set<(v: Array<[K, T]>) => void>() - #map: Map<K, T> - - #notifiy() { - const value = this.get() - for (const sub of this.#subs) { - sub(value) - } - } - - #delete(key: K) { - const v = this.#map.get(key) - - if (v instanceof Gtk.Widget) { - v.destroy() - } - - this.#map.delete(key) - } +## Is there a way to limit the width/height of a widget? - constructor(initial?: Iterable<[K, T]>) { - this.#map = new Map(initial) - } +Unfortunately not. You can set a minimum size with `min-width` and `min-heigth` css attributes, +but you can not set max size. - add(key: K, value: T) { - this.#delete(key) - this.#map.set(key, value) - this.#notifiy() - } +## Custom widgets with bindable properties - delete(key: K) { - this.#delete(key) - this.#notifiy() - } +In function components you can wrap any primitive to handle both +binding and value cases as one. - get() { - return [...this.#map.entries()] +```tsx +function MyWidget(props: { prop: string | Binding<string> }) { + const prop = props.prop instanceof Binding + ? props.prop + : bind({ get: () => props.prop, subscribe: () => () => {} }) + + function setup(self: Widget.Box) { + self.hook(prop, () => { + const value = prop.get() + // handler + }) } - subscribe(callback: (v: Array<[K, T]>) => void) { - this.#subs.add(callback) - return () => this.#subs.delete(callback) - } + return <box setup={setup}> + </box> } ``` -And this `VarMap<key, Widget>` can be used as an alternative to `Variable<Array<Widget>>`. +You can pass the prop the super constructor in subclasses ```tsx -function MappedBox() { - const map = new VarMap([ - [1, <MyWidget id={id} />] - [2, <MyWidget id={id} />] - ]) - - const conns = [ - gobject.connect("added", (_, id) => map.set(id, MyWidget({ id }))), - gobject.connect("removed", (_, id) => map.delete(id, MyWidget({ id }))), - ] - - return <box onDestroy={() => conns.map(id => gobject.disconnect(id))}> - {bind(map).as(arr => arr.sort(([a], [b]) => a - b).map(([,w]) => w))} - </box> +@register() +class MyWidget extends Widget.Box { + @property(String) + set prop(v: string) { + // handler + } + + constructor(props: { prop: string | Binding<string> }) { + super(props) + } } ``` - -## Is there a way to limit the width/height of a widget? - -Unfortunately not. You can set a minimum size with `min-width` and `min-heigth` css attributes, -but you can not set max size. diff --git a/docs/guide/ags/first-widgets.md b/docs/guide/ags/first-widgets.md index c4c4436..6dba7b3 100644 --- a/docs/guide/ags/first-widgets.md +++ b/docs/guide/ags/first-widgets.md @@ -75,7 +75,7 @@ either by using JSX or using a widget constructor. ```tsx [MyButton.tsx] function MyButton(): JSX.Element { return <button onClicked="echo hello"> - Clicke Me! + <label label="Click me!" /> </button> } ``` @@ -84,10 +84,10 @@ function MyButton(): JSX.Element { import { Widget } from "astal" function MyButton(): Widget.Button { - return Widget.Button({ - onClicked: "echo hello", - label: "Click Me!", - }) + return new Widget.Button( + { onClicked: "echo hello" }, + new Widget.Label({ label: "Click me!" }), + ) } ``` @@ -96,7 +96,7 @@ function MyButton(): Widget.Button { :::info The only difference between the two is the return type. Using markup the return type is always `Gtk.Widget` (globally available as `JSX.Element`), -while using constructors the return type is the type of the widget. +while using constructors the return type is the actual type of the widget. It is rare to need the actual return type, so most if not all of the time, you can use markup. ::: @@ -261,13 +261,12 @@ return <MyWidget myprop="hello"> ## State management -The state of widgets are handled with Bindings. A `Binding` lets you -connect the state of one [GObject](https://docs.gtk.org/gobject/class.Object.html) to another, in our case it is used to -rerender part of a widget based on the state of a `GObject`. -A `GObject` can be a [Variable](./variable) or it can be from a [Library](../libraries/references). +The state of widgets are handled with Bindings. A [Binding](./binding) lets you +connect the state of an [object](./binding#subscribable-and-connectable-interface) +to a widget so it re-renders when that state changes. -We use the `bind` function to create a `Binding` object from a `Variable` or -a regular GObject and one of its properties. +Use the `bind` function to create a `Binding` object from a `Variable` or +a regular `GObject` and one of its properties. Here is an example of a Counter widget that uses a `Variable` as its state: @@ -349,23 +348,25 @@ return <box> <box> ``` -:::warning -Only bind children of the `box` or the `stack` widget. Gtk does not cleanup widgets by default, -they have to be explicitly destroyed. The box widget is a special container that -will implicitly call `.destroy()` on its removed child widgets. -You can disable this behavior by setting the `noImplicityDestroy` property. +:::tip +Binding children of widgets will implicitly call `.destroy()` on widgets +that would be left without a parent. You can opt out of this behavior +by setting `noImplicityDestroy` property on the container widget. ::: :::info -The above example destroys and recreates every widget in the list everytime +The above example destroys and recreates every widget in the list **every time** the value of the `Variable` changes. There might be cases where you would want to [handle child creation and deletion](/guide/ags/faq#avoiding-unnecessary-re-rendering) yourself, because you don't want to lose the -inner state of widgets that does not need to be recreated. +inner state of widgets that does not need to be recreated. In this case +you can create a [custom reactive structure](./binding#example-custom-subscribable) ::: When there is at least one `Binding` passed as a child, the `children` -parameter will always be a flattened `Binding<Array<JSX.Element>>` +parameter will always be a flattened `Binding<Array<JSX.Element>>`. +When there is a single `Binding` passed as a child, the `child` parameter will +be a `Binding<JSX.Element>` or a flattened `Binding<Array<JSX.Element>>`. ```tsx function MyContainer({ children }) { diff --git a/docs/guide/ags/gobject.md b/docs/guide/ags/gobject.md new file mode 100644 index 0000000..a3080ee --- /dev/null +++ b/docs/guide/ags/gobject.md @@ -0,0 +1,166 @@ +# Subclassing GObject.Object + +These were formerly known as custom services in AGS. +Astal provides decorator functions that make it easy to subclass gobjects. + +## Example Usage + +```ts +import GObject, { register, property } from "astal/gobject" + +@register() +class MyObj extends GObject.Object { + @property(String) + declare myProp: string + + @signal(String, Number) + declare mySignal: (a: string, b: number) => void +} +``` + +## Property decorator + +```ts +type PropertyDeclaration = + | GObject.ParamSpec + | { $gtype: GObject.GType } + +function property(declaration: PropertyDeclaration) +``` + +The `property` decorator can take any class that has a registered GType. +This includes the globally available `String`, `Number`, `Boolean` and `Object` +javascript constructors. They are mapped to their relative `GObject.ParamSpec`. + +The property decorator can be applied in the following ways: + +1. On a property declaration + +```ts {3,4} +@register() +class MyObj extends GObject.Object { + @property(String) + declare myProp: string +} +``` + +This will create a getter and setter for the property and will also +emit the notify signal when the value is set to a new value. + +:::info +The `declare` keyword is required so that the property declaration +is not transpiled into JavaScript, otherwise the initial value of the +property would be `undefined`. +::: + +:::warning +The value is checked by reference, this is important if your +property is an object type. + +```ts +const dict = obj.prop +dict["key"] = 0 +obj.prop = dict // This will not emit notify::prop // [!code error] +obj.prop = { ...dict } // This will emit notify::prop +``` + +::: + +If you want to set a custom default value, do so in the constructor of your class. + +```ts {7} +@register() +class MyObj extends GObject.Object { + @property(String) + declare myProp: string + + constructors() { + super({ myProp: "default-value" }) + } +} +``` + +2. On a getter + +```ts {3,4} +@register() +class MyObj extends GObject.Object { + @property(String) + get myProp () { + return "value" + } +} +``` + +This will create a read-only property. + +3. On a getter and setter + +```ts {5,6,10} +@register() +class MyObj extends GObject.Object { + declare private _prop: string + + @property(String) + get myProp () { + return "value" + } + + set myProp (v: string) { + if (v !== this._prop) { + this._prop = v + this.notify("my-prop") + } + } +} +``` + +This will create a read-write property. + +:::info +When defining getter/setters for the property, notify signal emission has to be done explicitly. +::: + +## Signal decorator + +```ts +function signal(...params: Array<{ $gtype: GObject.GType }) + +function signal(declaration?: SignalDeclaration) // Object you would pass to GObject.registerClass +``` + +You can apply the signal decorator to either a property declaration or a method. + +```ts {3,4,6,7} +@register() +class MyObj extends GObject.Object { + @signal(String, String) + declare mySig: (a: String, b: String) => void + + @signal(String, String) + mySig(a: string, b: string) { + // default signal handler + } +} +``` + +You can emit the signal by calling the signal method or using `emit`. + +```ts +const obj = new MyObj() +obj.connect("my-sig", (obj, a: string, b: string) => {}) + +obj.mySig("a", "b") +obj.emit("my-sig", "a", "b") +``` + +## Register decorator + +Every GObject subclass has to be registered. You can pass the same options +to this decorator as you would to `GObject.registerClass` + +```ts +@register({ GTypeName: "MyObj" }) +class MyObj extends GObject.Object { +} +``` diff --git a/docs/guide/ags/utilities.md b/docs/guide/ags/utilities.md index 42589d3..aae255c 100644 --- a/docs/guide/ags/utilities.md +++ b/docs/guide/ags/utilities.md @@ -161,7 +161,7 @@ execAsync(["bash", "-c", "/path/to/script.sh"]) :::warning `subprocess`, `exec`, and `execAsync` executes the passed executable as is. They are **not** executed in a shell environment, -they do **not** expand env variables like `$HOME`, +they do **not** expand ENV variables like `$HOME`, and they do **not** handle logical operators like `&&` and `||`. If you want bash, run them with bash. diff --git a/docs/guide/ags/variable.md b/docs/guide/ags/variable.md index 96e8d38..4207f61 100644 --- a/docs/guide/ags/variable.md +++ b/docs/guide/ags/variable.md @@ -4,17 +4,17 @@ import { Variable } from "astal" ``` -Variable is just a simple `GObject` that holds a value. -And has shortcuts for hooking up subprocesses. +Variable is just a simple object which holds a single value. +It also has some shortcuts for hooking up subprocesses, intervals and other gobjects. :::info The `Variable` object imported from the `"astal"` package is **not** [Astal.Variable](https://aylur.github.io/libastal/class.Variable.html). ::: -## Variable as state +## Example Usage ```typescript -const myvar = Variable<string>("initial-value") +const myvar = Variable("initial-value") // whenever its value changes, callback will be executed myvar.subscribe((value: string) => { @@ -35,43 +35,49 @@ Widget.Label({ ``` :::warning -Make sure to make the transform functions pure. The `.get()` function can be called -anytime by `astal` especially when `deriving`, so make sure there are no sideeffects. +Make sure to make the transform functions passed to `.as()` are pure. +The `.get()` function can be called anytime by `astal` especially when `deriving`, +so make sure there are no sideeffects. ::: -## Composing variables +## Variable Composition -Using `Variable.derive` we can compose both Variables and Bindings. +Using `Variable.derive` any `Subscribable` object can be composed. ```typescript -const v1: Variable<number> = Variable(2) -const v2: Variable<number> = Variable(3) +const v1: Variable<number> = Variable(1) +const v2: Binding<number> = bind(obj, "prop") +const v3: Subscribable<number> = { + get: () => 3, + subscribe: () => () => {}, +} // first argument is a list of dependencies // second argument is a transform function, // where the parameters are the values of the dependencies in the order they were passed -const v3: Variable<number> = Variable.derive([v1, v2], (v1, v2) => { - return v1 * v2 -}) - -const b1: Binding<string> = bind(obj, "prop") -const b2: Binding<string> = bind(obj, "prop") - -const b3: Variable<string> = Variable.derive([b1, b2], (b1, b2) => { - return `${b1}-${b2}` -}) +const v4: Variable<number> = Variable.derive( + [v1, v2, v3], + (v1: number, v2: number, v3: number) => { + return v1 * v2 * v3 + } +) ``` +:::info +The types are only for demonstration purposes, you do not have to declare +the type of a Variable, they will be inferred from their initial value. +::: + ## Subprocess shorthands -Using `.poll` and `.watch` we can start subprocess and capture their -output in `Variables`. They can poll and watch at the same time, but they -can only poll/watch one subprocess. +Using `.poll` and `.watch` we can start subprocesses and capture their +output. They can poll and watch at the same time, but they +can only poll/watch once. :::warning The command parameter is passed to [execAsync](/guide/ags/utilities#executing-external-commands-and-scripts) which means they are **not** executed in a shell environment, -they do **not** expand env variables like `$HOME`, +they do **not** expand ENV variables like `$HOME`, and they do **not** handle logical operators like `&&` and `||`. If you want bash, run them with bash. @@ -83,14 +89,14 @@ Variable("").poll(1000, ["bash", "-c", "command $VAR && command"]) ::: ```typescript -const myVar = Variable<number>(0) +const myVar = Variable(0) .poll(1000, "command", (out: string, prev: number) => parseInt(out)) .poll(1000, ["bash", "-c", "command"], (out, prev) => parseInt(out)) .poll(1000, (prev) => prev + 1) ``` ```typescript -const myVar = Variable<number>(0) +const myVar = Variable(0) .watch("command", (out: string, prev: number) => parseInt(out)) .watch(["bash", "-c", "command"], (out, prev) => parseInt(out)) ``` @@ -127,14 +133,20 @@ myVar.drop() ``` :::warning -Don't forget to drop them when they are defined inside widgets -with either `.poll`, `.watch` or `.observe` +Don't forget to drop derived variables or variables with +either `.poll`, `.watch` or `.observe` when they are defined inside closures. ```tsx function MyWidget() { const myvar = Variable().poll() - - return <box onDestroy={() => myvar.drop()} /> + const derived = Variable.derive() + + return <box + onDestroy={() => { + myvar.drop() + derived.drop() + }} + /> } ``` |