From 800af1021c67ffe0ddcaed37ab09179d33102e35 Mon Sep 17 00:00:00 2001 From: Aylur Date: Thu, 10 Oct 2024 15:11:46 +0000 Subject: docs: gobject, variable, binding page --- docs/guide/ags/binding.md | 234 ++++++++++++++++++++++++++++++++++++++++ docs/guide/ags/faq.md | 189 +++++++------------------------- docs/guide/ags/first-widgets.md | 41 +++---- docs/guide/ags/gobject.md | 166 ++++++++++++++++++++++++++++ docs/guide/ags/utilities.md | 2 +- docs/guide/ags/variable.md | 72 +++++++------ 6 files changed, 502 insertions(+), 202 deletions(-) create mode 100644 docs/guide/ags/binding.md create mode 100644 docs/guide/ags/gobject.md (limited to 'docs/guide/ags') 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 { + private transformFn: (v: any) => unknown + private emitter: Subscribable | Connectable + private prop?: string + + as(fn: (v: Value) => T): Binding + 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(obj: Subscribable): Binding + +function bind< + Obj extends Connectable, + Prop extends keyof Obj, +>(obj: Obj, prop: Prop): Binding +``` + +## Subscribable and Connectable interface + +Any object implementing one of these interfaces can be used with `bind`. + +```ts +interface Subscribable { + 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 implements Subscribable { + #subs = new Set<(v: Array<[K, T]>) => void>() + #map: Map + + #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` can be used as an alternative to `Variable>`. + +```tsx +function MappedBox() { + const map = new VarMap([ + [1, ] + [2, ] + ]) + + const conns = [ + gobject.connect("added", (_, id) => map.set(id, MyWidget({ id }))), + gobject.connect("removed", (_, id) => map.delete(id, MyWidget({ id }))), + ] + + return conns.map(id => gobject.disconnect(id))}> + {bind(map).as(arr => arr.sort(([a], [b]) => a - b).map(([,w]) => w))} + +} +``` + +## 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 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 } 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 `-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 { - 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