summaryrefslogtreecommitdiff
path: root/core/gjs/src/astalify.ts
diff options
context:
space:
mode:
Diffstat (limited to 'core/gjs/src/astalify.ts')
-rw-r--r--core/gjs/src/astalify.ts331
1 files changed, 331 insertions, 0 deletions
diff --git a/core/gjs/src/astalify.ts b/core/gjs/src/astalify.ts
new file mode 100644
index 0000000..be395ee
--- /dev/null
+++ b/core/gjs/src/astalify.ts
@@ -0,0 +1,331 @@
+import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "./binding.js"
+import { Astal, Gtk, Gdk } from "./imports.js"
+import { execAsync } from "./process.js"
+import Variable from "./variable.js"
+
+Object.defineProperty(Astal.Box.prototype, "children", {
+ get() { return this.get_children() },
+ set(v) { this.set_children(v) },
+})
+
+function setChildren(parent: Gtk.Widget, children: Gtk.Widget[]) {
+ children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget
+ ? ch
+ : new Gtk.Label({ visible: true, label: String(ch) }))
+
+ // remove
+ if (parent instanceof Gtk.Bin) {
+ const ch = parent.get_child()
+ if (ch)
+ parent.remove(ch)
+ }
+
+ // FIXME: add rest of the edge cases like Stack
+ if (parent instanceof Astal.Box) {
+ parent.set_children(children)
+ }
+
+ else if (parent instanceof Astal.CenterBox) {
+ parent.startWidget = children[0]
+ parent.centerWidget = children[1]
+ parent.endWidget = children[2]
+ }
+
+ else if (parent instanceof Astal.Overlay) {
+ const [child, ...overlays] = children
+ parent.set_child(child)
+ parent.set_overlays(overlays)
+ }
+
+ else if (parent instanceof Gtk.Container) {
+ for (const ch of children)
+ parent.add(ch)
+ }
+}
+
+function mergeBindings(array: any[]) {
+ function getValues(...args: any[]) {
+ let i = 0
+ return array.map(value => value instanceof Binding
+ ? args[i++]
+ : value,
+ )
+ }
+
+ const bindings = array.filter(i => i instanceof Binding)
+
+ if (bindings.length === 0)
+ return array
+
+ if (bindings.length === 1)
+ return bindings[0].as(getValues)
+
+ return Variable.derive(bindings, getValues)()
+}
+
+function setProp(obj: any, prop: string, value: any) {
+ try {
+ const setter = `set_${snakeify(prop)}`
+ if (typeof obj[setter] === "function")
+ return obj[setter](value)
+
+ if (Object.hasOwn(obj, prop))
+ return (obj[prop] = value)
+ }
+ catch (error) {
+ console.error(`could not set property "${prop}" on ${obj}:`, error)
+ }
+
+ console.error(`could not set property "${prop}" on ${obj}`)
+}
+
+export type Widget<C extends InstanceType<typeof Gtk.Widget>> = C & {
+ className: string
+ css: string
+ cursor: Cursor
+ clickThrough: boolean
+ toggleClassName(name: string, on?: boolean): void
+ hook(
+ object: Connectable,
+ signal: string,
+ callback: (self: Widget<C>, ...args: any[]) => void,
+ ): Widget<C>
+ hook(
+ object: Subscribable,
+ callback: (self: Widget<C>, ...args: any[]) => void,
+ ): Widget<C>
+}
+
+function hook(
+ self: Gtk.Widget,
+ object: Connectable | Subscribable,
+ signalOrCallback: string | ((self: Gtk.Widget, ...args: any[]) => void),
+ callback?: (self: Gtk.Widget, ...args: any[]) => void,
+) {
+ if (typeof object.connect === "function" && callback) {
+ const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => {
+ callback(self, ...args)
+ })
+ self.connect("destroy", () => {
+ (object.disconnect as Connectable["disconnect"])(id)
+ })
+ }
+
+ else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") {
+ const unsub = object.subscribe((...args: unknown[]) => {
+ signalOrCallback(self, ...args)
+ })
+ self.connect("destroy", unsub)
+ }
+
+ return self
+}
+
+function ctor(self: any, config: any = {}, children: any = []) {
+ const { setup, ...props } = config
+ props.visible ??= true
+
+ const bindings = Object.keys(props).reduce((acc: any, prop) => {
+ if (props[prop] instanceof Binding) {
+ const binding = props[prop]
+ setProp(self, prop, binding.get())
+ delete props[prop]
+ return [...acc, [prop, binding]]
+ }
+ return acc
+ }, [])
+
+ const onHandlers = Object.keys(props).reduce((acc: any, key) => {
+ if (key.startsWith("on")) {
+ const sig = kebabify(key).split("-").slice(1).join("-")
+ const handler = props[key]
+ delete props[key]
+ return [...acc, [sig, handler]]
+ }
+ return acc
+ }, [])
+
+ Object.assign(self, props)
+
+ for (const [signal, callback] of onHandlers) {
+ if (typeof callback === "function") {
+ self.connect(signal, callback)
+ }
+ else {
+ self.connect(signal, () => execAsync(callback)
+ .then(print).catch(console.error))
+ }
+ }
+
+ for (const [prop, bind] of bindings) {
+ if (prop === "child" || prop === "children") {
+ self.connect("destroy", bind.subscribe((v: any) => {
+ setChildren(self, v)
+ }))
+ }
+ self.connect("destroy", bind.subscribe((v: any) => {
+ setProp(self, prop, v)
+ }))
+ }
+
+ children = mergeBindings(children.flat(Infinity))
+ if (children instanceof Binding) {
+ setChildren(self, children.get())
+ self.connect("destroy", children.subscribe((v) => {
+ setChildren(self, v)
+ }))
+ }
+ else {
+ if (children.length > 0)
+ setChildren(self, children)
+ }
+
+ setup?.(self)
+ return self
+}
+
+function proxify<
+ C extends typeof Gtk.Widget,
+>(klass: C) {
+ Object.defineProperty(klass.prototype, "className", {
+ get() { return Astal.widget_get_class_names(this).join(" ") },
+ set(v) { Astal.widget_set_class_names(this, v.split(/\s+/)) },
+ })
+
+ Object.defineProperty(klass.prototype, "css", {
+ get() { return Astal.widget_get_css(this) },
+ set(v) { Astal.widget_set_css(this, v) },
+ })
+
+ Object.defineProperty(klass.prototype, "cursor", {
+ get() { return Astal.widget_get_cursor(this) },
+ set(v) { Astal.widget_set_cursor(this, v) },
+ })
+
+ Object.defineProperty(klass.prototype, "clickThrough", {
+ get() { return Astal.widget_get_click_through(this) },
+ set(v) { Astal.widget_set_click_through(this, v) },
+ })
+
+ Object.assign(klass.prototype, {
+ hook: function (obj: any, sig: any, callback: any) {
+ return hook(this as InstanceType<C>, obj, sig, callback)
+ },
+ toggleClassName: function name(cn: string, cond = true) {
+ Astal.widget_toggle_class_name(this as InstanceType<C>, cn, cond)
+ },
+ set_class_name: function (name: string) {
+ // @ts-expect-error unknown key
+ this.className = name
+ },
+ set_css: function (css: string) {
+ // @ts-expect-error unknown key
+ this.css = css
+ },
+ set_cursor: function (cursor: string) {
+ // @ts-expect-error unknown key
+ this.cursor = cursor
+ },
+ set_click_through: function (clickThrough: boolean) {
+ // @ts-expect-error unknown key
+ this.clickThrough = clickThrough
+ },
+ })
+
+ const proxy = new Proxy(klass, {
+ construct(_, [conf, ...children]) {
+ // @ts-expect-error abstract class
+ return ctor(new klass(), conf, children)
+ },
+ apply(_t, _a, [conf, ...children]) {
+ // @ts-expect-error abstract class
+ return ctor(new klass(), conf, children)
+ },
+ })
+
+ return proxy
+}
+
+export default function astalify<
+ C extends typeof Gtk.Widget,
+ P extends Record<string, any>,
+ N extends string = "Widget",
+>(klass: C) {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ type Astal<N> = Omit<C, "new"> & {
+ new(props?: P, ...children: Gtk.Widget[]): Widget<InstanceType<C>>
+ (props?: P, ...children: Gtk.Widget[]): Widget<InstanceType<C>>
+ }
+
+ return proxify(klass) as unknown as Astal<N>
+}
+
+type BindableProps<T> = {
+ [K in keyof T]: Binding<T[K]> | T[K];
+}
+
+type SigHandler<
+ W extends InstanceType<typeof Gtk.Widget>,
+ Args extends Array<unknown>,
+> = ((self: Widget<W>, ...args: Args) => unknown) | string | string[]
+
+export type ConstructProps<
+ Self extends InstanceType<typeof Gtk.Widget>,
+ Props extends Gtk.Widget.ConstructorProps,
+ Signals extends Record<`on${string}`, Array<unknown>> = Record<`on${string}`, any[]>,
+> = Partial<{
+ // @ts-expect-error can't assign to unknown, but it works as expected though
+ [S in keyof Signals]: SigHandler<Self, Signals[S]>
+}> & Partial<{
+ [Key in `on${string}`]: SigHandler<Self, any[]>
+}> & BindableProps<Partial<Props> & {
+ className?: string
+ css?: string
+ cursor?: string
+ clickThrough?: boolean
+}> & {
+ onDestroy?: (self: Widget<Self>) => unknown
+ onDraw?: (self: Widget<Self>) => unknown
+ onKeyPressEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown
+ onKeyReleaseEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown
+ onButtonPressEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown
+ onButtonReleaseEvent?: (self: Widget<Self>, event: Gdk.Event) => unknown
+ onRealize?: (self: Widget<Self>) => unknown
+ setup?: (self: Widget<Self>) => void
+}
+
+type Cursor =
+ | "default"
+ | "help"
+ | "pointer"
+ | "context-menu"
+ | "progress"
+ | "wait"
+ | "cell"
+ | "crosshair"
+ | "text"
+ | "vertical-text"
+ | "alias"
+ | "copy"
+ | "no-drop"
+ | "move"
+ | "not-allowed"
+ | "grab"
+ | "grabbing"
+ | "all-scroll"
+ | "col-resize"
+ | "row-resize"
+ | "n-resize"
+ | "e-resize"
+ | "s-resize"
+ | "w-resize"
+ | "ne-resize"
+ | "nw-resize"
+ | "sw-resize"
+ | "se-resize"
+ | "ew-resize"
+ | "ns-resize"
+ | "nesw-resize"
+ | "nwse-resize"
+ | "zoom-in"
+ | "zoom-out"