From 2f09ed83386b334f0dfb7f376b99739b15e49fc9 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sun, 24 Nov 2024 00:04:07 +0000 Subject: gjs gtk4 support export jsx-runtime --- flake.lock | 6 +- lang/gjs/eslint.config.mjs | 1 + lang/gjs/meson.build | 6 +- lang/gjs/package.json | 8 +- lang/gjs/src/_app.ts | 9 +- lang/gjs/src/_astal.ts | 180 +++++++++++++++++++++++++++ lang/gjs/src/binding.ts | 8 +- lang/gjs/src/file.ts | 6 +- lang/gjs/src/gobject.ts | 10 +- lang/gjs/src/gtk3/astalify.ts | 225 +++++++-------------------------- lang/gjs/src/gtk3/jsx-runtime.ts | 31 +---- lang/gjs/src/gtk3/widget.ts | 20 +++ lang/gjs/src/gtk4/app.ts | 6 + lang/gjs/src/gtk4/astalify.ts | 227 +++++++++++++++++++++++++++++++++- lang/gjs/src/gtk4/index.ts | 4 +- lang/gjs/src/gtk4/jsx-runtime.ts | 69 ++++++++++- lang/gjs/src/gtk4/widget.ts | 165 +++++++++++++++++++++++- lang/gjs/src/index.ts | 4 +- lang/gjs/src/package.json | 22 ++++ lang/gjs/src/process.ts | 9 +- lang/gjs/src/variable.ts | 13 +- lang/gjs/tsconfig.json | 2 +- lib/astal/gtk4/src/meson.build | 2 + lib/astal/gtk4/src/widget/box.vala | 35 ++++++ lib/astal/gtk4/src/widget/slider.vala | 67 ++++++++++ nix/devshell.nix | 19 ++- 26 files changed, 895 insertions(+), 259 deletions(-) create mode 100644 lang/gjs/src/_astal.ts create mode 100644 lang/gjs/src/package.json create mode 100644 lib/astal/gtk4/src/widget/box.vala create mode 100644 lib/astal/gtk4/src/widget/slider.vala diff --git a/flake.lock b/flake.lock index eec56ea..def3031 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1729256560, - "narHash": "sha256-/uilDXvCIEs3C9l73JTACm4quuHUsIHcns1c+cHUJwA=", + "lastModified": 1732837521, + "narHash": "sha256-jNRNr49UiuIwaarqijgdTR2qLPifxsVhlJrKzQ8XUIE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4c2fcb090b1f3e5b47eaa7bd33913b574a11e0a0", + "rev": "970e93b9f82e2a0f3675757eb0bfc73297cc6370", "type": "github" }, "original": { diff --git a/lang/gjs/eslint.config.mjs b/lang/gjs/eslint.config.mjs index 05e49ee..5e32355 100644 --- a/lang/gjs/eslint.config.mjs +++ b/lang/gjs/eslint.config.mjs @@ -15,5 +15,6 @@ export default tseslint.config({ rules: { "@typescript-eslint/no-explicit-any": "off", "@stylistic/new-parens": "off", + "@stylistic/brace-style": ["error", "1tbs", { allowSingleLine: true }], }, }) diff --git a/lang/gjs/meson.build b/lang/gjs/meson.build index 51496dc..402bc55 100644 --- a/lang/gjs/meson.build +++ b/lang/gjs/meson.build @@ -7,15 +7,17 @@ dependency('astal-3.0') install_data( [ + 'src/_app.ts', + 'src/_astal.ts', 'src/binding.ts', 'src/file.ts', 'src/gobject.ts', 'src/index.ts', + 'src/overrides.ts', 'src/process.ts', 'src/time.ts', 'src/variable.ts', - 'src/overrides.ts', - 'src/_app.ts', + 'src/package.json', ], install_dir: dest, ) diff --git a/lang/gjs/package.json b/lang/gjs/package.json index 43a7702..e3b7761 100644 --- a/lang/gjs/package.json +++ b/lang/gjs/package.json @@ -18,6 +18,12 @@ ".": "./index.ts", "./gtk3": "./src/gtk3/index.ts", "./gtk4": "./src/gtk4/index.ts", + "./gtk3/app": "./src/gtk3/app.ts", + "./gtk4/app": "./src/gtk4/app.ts", + "./gtk3/widget": "./src/gtk3/widget.ts", + "./gtk4/widget": "./src/gtk4/widget.ts", + "./gtk3/jsx-runtime": "./src/gtk3/jsx-runtime.ts", + "./gtk4/jsx-runtime": "./src/gtk4/jsx-runtime.ts", "./binding": "./src/binding.ts", "./file": "./src/file.ts", "./gobject": "./src/gobject.ts", @@ -42,6 +48,6 @@ }, "scripts": { "lint": "eslint . --fix", - "types": "ts-for-gir generate -o @girs" + "types": "ts-for-gir generate -o @girs --ignoreVersionConflicts" } } diff --git a/lang/gjs/src/_app.ts b/lang/gjs/src/_app.ts index 3dadd04..46497c1 100644 --- a/lang/gjs/src/_app.ts +++ b/lang/gjs/src/_app.ts @@ -53,8 +53,7 @@ export function mkApp(App: App3 | App4) { ${body.includes(";") ? body : `return ${body};`} })`) fn()().then(res).catch(rej) - } - catch (error) { + } catch (error) { rej(error) } }) @@ -69,8 +68,7 @@ export function mkApp(App: App3 | App4) { IO.write_sock_finish(res), ) }) - } - else { + } else { super.vfunc_request(msg, conn) } } @@ -102,8 +100,7 @@ export function mkApp(App: App3 | App4) { try { app.acquire_socket() - } - catch (error) { + } catch (error) { return client(msg => IO.send_message(app.instanceName, msg)!, ...programArgs) } diff --git a/lang/gjs/src/_astal.ts b/lang/gjs/src/_astal.ts new file mode 100644 index 0000000..cfd8c9f --- /dev/null +++ b/lang/gjs/src/_astal.ts @@ -0,0 +1,180 @@ +import Variable from "./variable.js" +import { execAsync } from "./process.js" +import Binding, { Connectable, kebabify, snakeify, Subscribable } from "./binding.js" + +export const noImplicitDestroy = Symbol("no no implicit destroy") +export const setChildren = Symbol("children setter method") + +export 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)() +} + +export function setProp(obj: any, prop: string, value: any) { + try { + const setter = `set_${snakeify(prop)}` + if (typeof obj[setter] === "function") + return obj[setter](value) + + return (obj[prop] = value) + } catch (error) { + console.error(`could not set property "${prop}" on ${obj}:`, error) + } +} + +export type BindableProps = { + [K in keyof T]: Binding | T[K]; +} + +export function hook( + widget: Widget, + object: Connectable | Subscribable, + signalOrCallback: string | ((self: Widget, ...args: any[]) => void), + callback?: (self: Widget, ...args: any[]) => void, +) { + if (typeof object.connect === "function" && callback) { + const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { + callback(widget, ...args) + }) + widget.connect("destroy", () => { + (object.disconnect as Connectable["disconnect"])(id) + }) + } else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { + const unsub = object.subscribe((...args: unknown[]) => { + signalOrCallback(widget, ...args) + }) + widget.connect("destroy", unsub) + } +} + +export function construct void }>(widget: Widget, config: any) { + const { setup, child, children = [], ...props } = config + + if (child) + children.unshift(child) + + // collect bindings + const bindings: Array<[string, Binding]> = Object + .keys(props) + .reduce((acc: any, prop) => { + if (props[prop] instanceof Binding) { + const binding = props[prop] + delete props[prop] + return [...acc, [prop, binding]] + } + return acc + }, []) + + // collect signal handlers + const onHandlers: Array<[string, string | (() => unknown)]> = 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 + }, []) + + // set children + const mergedChildren = mergeBindings(children.flat(Infinity)) + if (mergedChildren instanceof Binding) { + widget[setChildren](mergedChildren.get()) + widget.connect("destroy", mergedChildren.subscribe((v) => { + widget[setChildren](v) + })) + } else { + if (mergedChildren.length > 0) { + widget[setChildren](mergedChildren) + } + } + + // setup signal handlers + for (const [signal, callback] of onHandlers) { + const sig = signal.startsWith("notify") + ? signal.replace("-", "::") + : signal + + if (typeof callback === "function") { + widget.connect(sig, callback) + } else { + widget.connect(sig, () => execAsync(callback) + .then(print).catch(console.error)) + } + } + + // setup bindings handlers + for (const [prop, binding] of bindings) { + if (prop === "child" || prop === "children") { + widget.connect("destroy", binding.subscribe((v: any) => { + widget[setChildren](v) + })) + } + widget.connect("destroy", binding.subscribe((v: any) => { + setProp(widget, prop, v) + })) + setProp(widget, prop, binding.get()) + } + + // filter undefined values + for (const [key, value] of Object.entries(props)) { + if (value === undefined) { + delete props[key] + } + } + + Object.assign(widget, props) + setup?.(widget) + return widget +} + +function isArrowFunction(func: any): func is (args: any) => any { + return !Object.hasOwn(func, "prototype") +} + +export function jsx( + ctors: Record any)>, + ctor: string | ((props: any) => any) | { new(props: any): any }, + { children, ...props }: any, +) { + children ??= [] + + if (!Array.isArray(children)) + children = [children] + + children = children.filter(Boolean) + + if (children.length === 1) + props.child = children[0] + else if (children.length > 1) + props.children = children + + if (typeof ctor === "string") { + if (isArrowFunction(ctors[ctor])) + return ctors[ctor](props) + + return new ctors[ctor](props) + } + + if (isArrowFunction(ctor)) + return ctor(props) + + return new ctor(props) +} diff --git a/lang/gjs/src/binding.ts b/lang/gjs/src/binding.ts index 95d905f..19a55cf 100644 --- a/lang/gjs/src/binding.ts +++ b/lang/gjs/src/binding.ts @@ -20,7 +20,7 @@ export interface Connectable { [key: string]: any } -export default class Binding { +export class Binding { private transformFn = (v: any) => v #emitter: Subscribable | Connectable @@ -46,7 +46,7 @@ export default class Binding { return `Binding<${this.#emitter}${this.#prop ? `, "${this.#prop}"` : ""}>` } - as(fn: (v: Value) => T): Binding { + as(fn: (v: Value) => T | Binding): Binding { const bind = new Binding(this.#emitter, this.#prop) bind.transformFn = (v: Value) => fn(this.transformFn(v)) return bind as unknown as Binding @@ -72,8 +72,7 @@ export default class Binding { return this.#emitter.subscribe(() => { callback(this.get()) }) - } - else if (typeof this.#emitter.connect === "function") { + } else if (typeof this.#emitter.connect === "function") { const signal = `notify::${this.#prop}` const id = this.#emitter.connect(signal, () => { callback(this.get()) @@ -87,3 +86,4 @@ export default class Binding { } export const { bind } = Binding +export default Binding diff --git a/lang/gjs/src/file.ts b/lang/gjs/src/file.ts index 6ad8be3..4220d9d 100644 --- a/lang/gjs/src/file.ts +++ b/lang/gjs/src/file.ts @@ -12,8 +12,7 @@ export function readFileAsync(path: string): Promise { Astal.read_file_async(path, (_, res) => { try { resolve(Astal.read_file_finish(res) || "") - } - catch (error) { + } catch (error) { reject(error) } }) @@ -29,8 +28,7 @@ export function writeFileAsync(path: string, content: string): Promise { Astal.write_file_async(path, content, (_, res) => { try { resolve(Astal.write_file_finish(res)) - } - catch (error) { + } catch (error) { reject(error) } }) diff --git a/lang/gjs/src/gobject.ts b/lang/gjs/src/gobject.ts index b744cfb..1d6b9a1 100644 --- a/lang/gjs/src/gobject.ts +++ b/lang/gjs/src/gobject.ts @@ -90,9 +90,7 @@ export function property(declaration: PropertyDeclaration = Object) { }) target.constructor[meta].Properties[kebabify(prop)] = pspec(name, ParamFlags.READWRITE, declaration) - } - - else { + } else { let flags = 0 if (desc.get) flags |= ParamFlags.READABLE if (desc.set) flags |= ParamFlags.WRITABLE @@ -124,8 +122,7 @@ export function signal( target.constructor[meta].Signals[name] = { param_types: arr, } - } - else { + } else { target.constructor[meta].Signals[name] = declaration || { param_types: [], } @@ -137,8 +134,7 @@ export function signal( this.emit(name, ...args) }, }) - } - else { + } else { const og: ((...args: any[]) => void) = desc.value desc.value = function (...args: any[]) { // @ts-expect-error not typed diff --git a/lang/gjs/src/gtk3/astalify.ts b/lang/gjs/src/gtk3/astalify.ts index 6973805..92ffb6f 100644 --- a/lang/gjs/src/gtk3/astalify.ts +++ b/lang/gjs/src/gtk3/astalify.ts @@ -1,45 +1,11 @@ +import { hook, noImplicitDestroy, setChildren, mergeBindings, type BindableProps, construct } from "../_astal.js" import Astal from "gi://Astal?version=3.0" import Gtk from "gi://Gtk?version=3.0" import Gdk from "gi://Gdk?version=3.0" import GObject from "gi://GObject" -import { execAsync } from "../process.js" -import Variable from "../variable.js" -import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "../binding.js" +import Binding, { type Connectable, type Subscribable } from "../binding.js" -export 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 { - // the setter method has to be used because - // array like properties are not bound correctly as props - const setter = `set_${snakeify(prop)}` - if (typeof obj[setter] === "function") - return obj[setter](value) - - return (obj[prop] = value) - } - catch (error) { - console.error(`could not set property "${prop}" on ${obj}:`, error) - } -} +export { BindableProps, mergeBindings } export default function astalify< C extends { new(...args: any[]): Gtk.Widget }, @@ -65,60 +31,44 @@ export default function astalify< get_click_through(): boolean { return this.clickThrough } set_click_through(clickThrough: boolean) { this.clickThrough = clickThrough } - declare private __no_implicit_destroy: boolean - get noImplicitDestroy(): boolean { return this.__no_implicit_destroy } - set noImplicitDestroy(value: boolean) { this.__no_implicit_destroy = value } + declare private [noImplicitDestroy]: boolean + get noImplicitDestroy(): boolean { return this[noImplicitDestroy] } + set noImplicitDestroy(value: boolean) { this[noImplicitDestroy] = value } + + protected getChildren(): Array { + if (this instanceof Gtk.Bin) { + return this.get_child() ? [this.get_child()!] : [] + } else if (this instanceof Gtk.Container) { + return this.get_children() + } + return [] + } - _setChildren(children: Gtk.Widget[]) { + protected setChildren(children: any[]) { children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget ? ch : new Gtk.Label({ visible: true, label: String(ch) })) - // remove - if (this instanceof Gtk.Bin) { - const ch = this.get_child() - if (ch) - this.remove(ch) - if (ch && !children.includes(ch) && !this.noImplicitDestroy) - ch?.destroy() + if (this instanceof Gtk.Container) { + for (const ch of children) + this.add(ch) + } else { + throw Error(`can not add children to ${this.constructor.name}`) } - else if (this instanceof Gtk.Container) { - for (const ch of this.get_children()) { + } + + [setChildren](children: any[]) { + // remove + if (this instanceof Gtk.Container) { + for (const ch of this.getChildren()) { this.remove(ch) if (!children.includes(ch) && !this.noImplicitDestroy) ch?.destroy() } } - // TODO: add more container types - if (this instanceof Astal.Box) { - this.set_children(children) - } - - else if (this instanceof Astal.Stack) { - this.set_children(children) - } - - else if (this instanceof Astal.CenterBox) { - this.startWidget = children[0] - this.centerWidget = children[1] - this.endWidget = children[2] - } - - else if (this instanceof Astal.Overlay) { - const [child, ...overlays] = children - this.set_child(child) - this.set_overlays(overlays) - } - - else if (this instanceof Gtk.Container) { - for (const ch of children) - this.add(ch) - } - - else { - throw Error(`can not add children to ${this.constructor.name}, it is not a container widget`) - } + // append + this.setChildren(children) } toggleClassName(cn: string, cond = true) { @@ -139,96 +89,15 @@ export default function astalify< signalOrCallback: string | ((self: this, ...args: any[]) => void), callback?: (self: this, ...args: any[]) => void, ) { - if (typeof object.connect === "function" && callback) { - const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => { - callback(this, ...args) - }) - this.connect("destroy", () => { - (object.disconnect as Connectable["disconnect"])(id) - }) - } - - else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") { - const unsub = object.subscribe((...args: unknown[]) => { - signalOrCallback(this, ...args) - }) - this.connect("destroy", unsub) - } - + hook(this, object, signalOrCallback, callback) return this } constructor(...params: any[]) { super() - const [config] = params - - const { setup, child, children = [], ...props } = config + const props = params[0] || {} props.visible ??= true - - if (child) - children.unshift(child) - - // collect bindings - const bindings = Object.keys(props).reduce((acc: any, prop) => { - if (props[prop] instanceof Binding) { - const binding = props[prop] - delete props[prop] - return [...acc, [prop, binding]] - } - return acc - }, []) - - // collect signal handlers - 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 - }, []) - - // set children - const mergedChildren = mergeBindings(children.flat(Infinity)) - if (mergedChildren instanceof Binding) { - this._setChildren(mergedChildren.get()) - this.connect("destroy", mergedChildren.subscribe((v) => { - this._setChildren(v) - })) - } - else { - if (mergedChildren.length > 0) { - this._setChildren(mergedChildren) - } - } - - // setup signal handlers - for (const [signal, callback] of onHandlers) { - if (typeof callback === "function") { - this.connect(signal, callback) - } - else { - this.connect(signal, () => execAsync(callback) - .then(print).catch(console.error)) - } - } - - // setup bindings handlers - for (const [prop, binding] of bindings) { - if (prop === "child" || prop === "children") { - this.connect("destroy", binding.subscribe((v: any) => { - this._setChildren(v) - })) - } - this.connect("destroy", binding.subscribe((v: any) => { - setProp(this, prop, v) - })) - setProp(this, prop, binding.get()) - } - - Object.assign(this, props) - setup?.(this) + construct(this, props) } } @@ -256,15 +125,13 @@ export default function astalify< return Widget } -export type BindableProps = { - [K in keyof T]: Binding | T[K]; -} - type SigHandler< W extends InstanceType, Args extends Array, > = ((self: W, ...args: Args) => unknown) | string | string[] +export type BindableChild = Gtk.Widget | Binding + export type ConstructProps< Self extends InstanceType, Props extends Gtk.Widget.ConstructorProps, @@ -274,23 +141,21 @@ export type ConstructProps< [S in keyof Signals]: SigHandler }> & Partial<{ [Key in `on${string}`]: SigHandler -}> & BindableProps & { +}> & BindableProps & { - onDestroy?: (self: Self) => unknown - onDraw?: (self: Self) => unknown - onKeyPressEvent?: (self: Self, event: Gdk.Event) => unknown - onKeyReleaseEvent?: (self: Self, event: Gdk.Event) => unknown - onButtonPressEvent?: (self: Self, event: Gdk.Event) => unknown - onButtonReleaseEvent?: (self: Self, event: Gdk.Event) => unknown - onRealize?: (self: Self) => unknown - setup?: (self: Self) => void -} - -export type BindableChild = Gtk.Widget | Binding +}>> & Partial<{ + onDestroy: (self: Self) => unknown + onDraw: (self: Self) => unknown + onKeyPressEvent: (self: Self, event: Gdk.Event) => unknown + onKeyReleaseEvent: (self: Self, event: Gdk.Event) => unknown + onButtonPressEvent: (self: Self, event: Gdk.Event) => unknown + onButtonReleaseEvent: (self: Self, event: Gdk.Event) => unknown + onRealize: (self: Self) => unknown + setup: (self: Self) => void +}> type Cursor = | "default" diff --git a/lang/gjs/src/gtk3/jsx-runtime.ts b/lang/gjs/src/gtk3/jsx-runtime.ts index 9da4bb6..f37c695 100644 --- a/lang/gjs/src/gtk3/jsx-runtime.ts +++ b/lang/gjs/src/gtk3/jsx-runtime.ts @@ -1,11 +1,8 @@ import Gtk from "gi://Gtk?version=3.0" -import { mergeBindings, type BindableChild } from "./astalify.js" +import { type BindableChild } from "./astalify.js" +import { mergeBindings, jsx as _jsx } from "../_astal.js" import * as Widget from "./widget.js" -function isArrowFunction(func: any): func is (args: any) => any { - return !Object.hasOwn(func, "prototype") -} - export function Fragment({ children = [], child }: { child?: BindableChild children?: Array @@ -16,29 +13,9 @@ export function Fragment({ children = [], child }: { export function jsx( ctor: keyof typeof ctors | typeof Gtk.Widget, - { children, ...props }: any, + props: any, ) { - children ??= [] - - if (!Array.isArray(children)) - children = [children] - - children = children.filter(Boolean) - - if (children.length === 1) - props.child = children[0] - else if (children.length > 1) - props.children = children - - if (typeof ctor === "string") { - return new ctors[ctor](props) - } - - if (isArrowFunction(ctor)) - return ctor(props) - - // @ts-expect-error can be class or function - return new ctor(props) + return _jsx(ctors, ctor as any, props) } const ctors = { diff --git a/lang/gjs/src/gtk3/widget.ts b/lang/gjs/src/gtk3/widget.ts index b4e8497..759a4b1 100644 --- a/lang/gjs/src/gtk3/widget.ts +++ b/lang/gjs/src/gtk3/widget.ts @@ -4,6 +4,12 @@ import Gtk from "gi://Gtk?version=3.0" import GObject from "gi://GObject" import astalify, { type ConstructProps, type BindableChild } from "./astalify.js" +function filter(children: any[]) { + return children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) +} + // Box Object.defineProperty(Astal.Box.prototype, "children", { get() { return this.get_children() }, @@ -14,6 +20,7 @@ export type BoxProps = ConstructProps export class Box extends astalify(Astal.Box) { static { GObject.registerClass({ GTypeName: "Box" }, this) } constructor(props?: BoxProps, ...children: Array) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { this.set_children(filter(children)) } } // Button @@ -35,6 +42,12 @@ export type CenterBoxProps = ConstructProps) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { + const ch = filter(children) + this.startWidget = ch[0] || new Gtk.Box + this.centerWidget = ch[1] || new Gtk.Box + this.endWidget = ch[2] || new Gtk.Box + } } // CircularProgress @@ -91,6 +104,7 @@ export type LabelProps = ConstructProps export class Label extends astalify(Astal.Label) { static { GObject.registerClass({ GTypeName: "Label" }, this) } constructor(props?: LabelProps) { super(props as any) } + protected setChildren(children: any[]): void { this.label = String(children) } } // LevelBar @@ -112,6 +126,11 @@ export type OverlayProps = ConstructProps) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { + const [child, ...overlays] = filter(children) + this.set_child(child) + this.set_overlays(overlays) + } } // Revealer @@ -142,6 +161,7 @@ export type StackProps = ConstructProps export class Stack extends astalify(Astal.Stack) { static { GObject.registerClass({ GTypeName: "Stack" }, this) } constructor(props?: StackProps, ...children: Array) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { this.set_children(filter(children)) } } // Switch diff --git a/lang/gjs/src/gtk4/app.ts b/lang/gjs/src/gtk4/app.ts index 1c51772..7906993 100644 --- a/lang/gjs/src/gtk4/app.ts +++ b/lang/gjs/src/gtk4/app.ts @@ -4,4 +4,10 @@ import { mkApp } from "../_app" Gtk.init() +// users might want to use Adwaita in which case it has to be initialized +// it might be common pitfall to forget it because `App` is not `Adw.Application` +await import("gi://Adw?version=1") + .then(({ default: Adw }) => Adw.init()) + .catch(() => void 0) + export default mkApp(Astal.Application) diff --git a/lang/gjs/src/gtk4/astalify.ts b/lang/gjs/src/gtk4/astalify.ts index 6c8ea4d..644ac1a 100644 --- a/lang/gjs/src/gtk4/astalify.ts +++ b/lang/gjs/src/gtk4/astalify.ts @@ -1 +1,226 @@ -// TODO: +import { noImplicitDestroy, setChildren, type BindableProps, construct } from "../_astal.js" +import Gtk from "gi://Gtk?version=4.0" +import Gdk from "gi://Gdk?version=4.0" +import Binding from "../binding.js" + +export const type = Symbol("child type") +const dummyBulder = new Gtk.Builder + +function _getChildren(widget: Gtk.Widget): Array { + if ("get_child" in widget && typeof widget.get_child == "function") { + return widget.get_child() ? [widget.get_child()] : [] + } + + const children: Array = [] + let ch = widget.get_first_child() + while (ch !== null) { + children.push(ch) + ch = ch.get_next_sibling() + } + return children +} + +function _setChildren(widget: Gtk.Widget, children: any[]) { + children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) + + for (const child of children) { + widget.vfunc_add_child( + dummyBulder, + child, + type in widget ? widget[type] as string : null, + ) + } +} + +type Config = { + setChildren(widget: T, children: any[]): void + getChildren(widget: T): Array +} + +export default function astalify< + Widget extends Gtk.Widget, + Props extends Gtk.Widget.ConstructorProps = Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array> = Record<`on${string}`, any[]>, +>(cls: { new(...args: any[]): Widget }, config: Partial> = {}) { + Object.assign(cls.prototype, { + [setChildren](children: any[]) { + const w = this as unknown as Widget + for (const child of (config.getChildren?.(w) || _getChildren(w))) { + if (child instanceof Gtk.Widget) { + child.unparent() + if (!children.includes(child) && noImplicitDestroy in this) + child.run_dispose() + } + } + + if (config.setChildren) { + config.setChildren(w, children) + } else { + _setChildren(w, children) + } + }, + }) + + return { + [cls.name]: ( + props: ConstructProps = {}, + ...children: any[] + ): Widget => { + const widget = new cls("cssName" in props ? { cssName: props.cssName } : {}) + + if ("cssName" in props) { + delete props.cssName + } + + if (props.noImplicitDestroy) { + Object.assign(widget, { [noImplicitDestroy]: true }) + delete props.noImplicitDestroy + } + + if (props.type) { + Object.assign(widget, { [type]: props.type }) + delete props.type + } + + if (children.length > 0) { + Object.assign(props, { children }) + } + + return construct(widget as any, setupControllers(widget, props as any)) + }, + }[cls.name] +} + +type SigHandler< + W extends InstanceType, + Args extends Array, +> = ((self: W, ...args: Args) => unknown) | string | string[] + +export { BindableProps } +export type BindableChild = Gtk.Widget | Binding + +export type ConstructProps< + Self extends InstanceType, + Props extends Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array> = Record<`on${string}`, any[]>, +> = Partial<{ + // @ts-expect-error can't assign to unknown, but it works as expected though + [S in keyof Signals]: SigHandler +}> & Partial<{ + [Key in `on${string}`]: SigHandler +}> & Partial>> & { + noImplicitDestroy?: true + type?: string + cssName?: string +} & EventController & { + onDestroy?: (self: Self) => unknown + setup?: (self: Self) => void +} + +type EventController = { + onFocusEnter?: (self: Self) => void + onFocusLeave?: (self: Self) => void + + onKeyPressed?: (self: Self, keyval: number, keycode: number, state: Gdk.ModifierType) => void + onKeyReleased?: (self: Self, keyval: number, keycode: number, state: Gdk.ModifierType) => void + onKeyModifier?: (self: Self, state: Gdk.ModifierType) => void + + onLegacy?: (self: Self, event: Gdk.Event) => void + onButtonPressed?: (self: Self, state: Gdk.ButtonEvent) => void + onButtonReleased?: (self: Self, state: Gdk.ButtonEvent) => void + + onHoverEnter?: (self: Self, x: number, y: number) => void + onHoverLeave?: (self: Self) => void + onMotion?: (self: Self, x: number, y: number) => void + + onScroll?: (self: Self, dx: number, dy: number) => void + onScrollDecelerate?: (self: Self, vel_x: number, vel_y: number) => void +} + +function setupControllers(widget: Gtk.Widget, { + onFocusEnter, + onFocusLeave, + onKeyPressed, + onKeyReleased, + onKeyModifier, + onLegacy, + onButtonPressed, + onButtonReleased, + onHoverEnter, + onHoverLeave, + onMotion, + onScroll, + onScrollDecelerate, + ...props +}: EventController & T) { + if (onFocusEnter || onFocusLeave) { + const focus = new Gtk.EventControllerFocus + widget.add_controller(focus) + + if (onFocusEnter) + focus.connect("focus-enter", () => onFocusEnter(widget)) + + if (onFocusLeave) + focus.connect("focus-leave", () => onFocusLeave(widget)) + } + + if (onKeyPressed || onKeyReleased || onKeyModifier) { + const key = new Gtk.EventControllerKey + widget.add_controller(key) + + if (onKeyPressed) + key.connect("key-pressed", (_, val, code, state) => onKeyPressed(widget, val, code, state)) + + if (onKeyReleased) + key.connect("key-released", (_, val, code, state) => onKeyReleased(widget, val, code, state)) + + if (onKeyModifier) + key.connect("modifiers", (_, state) => onKeyModifier(widget, state)) + } + + if (onLegacy || onButtonPressed || onButtonReleased) { + const legacy = new Gtk.EventControllerLegacy + widget.add_controller(legacy) + + legacy.connect("event", (_, event) => { + if (event.get_event_type() === Gdk.EventType.BUTTON_PRESS) { + onButtonPressed?.(widget, event as Gdk.ButtonEvent) + } + + if (event.get_event_type() === Gdk.EventType.BUTTON_RELEASE) { + onButtonReleased?.(widget, event as Gdk.ButtonEvent) + } + + onLegacy?.(widget, event) + }) + } + + if (onMotion || onHoverEnter || onHoverLeave) { + const hover = new Gtk.EventControllerMotion + widget.add_controller(hover) + + if (onHoverEnter) + hover.connect("enter", (_, x, y) => onHoverEnter(widget, x, y)) + + if (onHoverLeave) + hover.connect("leave", () => onHoverLeave(widget)) + + if (onMotion) + hover.connect("motion", (_, x, y) => onMotion(widget, x, y)) + } + + if (onScroll || onScrollDecelerate) { + const scroll = new Gtk.EventControllerScroll + widget.add_controller(scroll) + + if (onScroll) + scroll.connect("scroll", (_, x, y) => onScroll(widget, x, y)) + + if (onScrollDecelerate) + scroll.connect("decelerate", (_, x, y) => onScrollDecelerate(widget, x, y)) + } + + return props +} diff --git a/lang/gjs/src/gtk4/index.ts b/lang/gjs/src/gtk4/index.ts index 3b1f737..2cc6862 100644 --- a/lang/gjs/src/gtk4/index.ts +++ b/lang/gjs/src/gtk4/index.ts @@ -1,9 +1,9 @@ import Astal from "gi://Astal?version=4.0" import Gtk from "gi://Gtk?version=4.0" import Gdk from "gi://Gdk?version=4.0" -// import astalify, { type ConstructProps } from "./astalify.js" +import astalify, { type ConstructProps } from "./astalify.js" export { Astal, Gtk, Gdk } export { default as App } from "./app.js" -// export { astalify, ConstructProps } +export { astalify, ConstructProps } // export * as Widget from "./widget.js" diff --git a/lang/gjs/src/gtk4/jsx-runtime.ts b/lang/gjs/src/gtk4/jsx-runtime.ts index 6c8ea4d..80a3e87 100644 --- a/lang/gjs/src/gtk4/jsx-runtime.ts +++ b/lang/gjs/src/gtk4/jsx-runtime.ts @@ -1 +1,68 @@ -// TODO: +import Gtk from "gi://Gtk?version=4.0" +import { type BindableChild } from "./astalify.js" +import { mergeBindings, jsx as _jsx } from "../_astal.js" +import * as Widget from "./widget.js" + +export function Fragment({ children = [], child }: { + child?: BindableChild + children?: Array +}) { + if (child) children.push(child) + return mergeBindings(children) +} + +export function jsx( + ctor: keyof typeof ctors | typeof Gtk.Widget, + props: any, +) { + return _jsx(ctors, ctor as any, props) +} + +const ctors = { + box: Widget.Box, + button: Widget.Button, + centerbox: Widget.CenterBox, + // circularprogress: Widget.CircularProgress, + // drawingarea: Widget.DrawingArea, + entry: Widget.Entry, + image: Widget.Image, + label: Widget.Label, + levelbar: Widget.LevelBar, + overlay: Widget.Overlay, + revealer: Widget.Revealer, + slider: Widget.Slider, + stack: Widget.Stack, + switch: Widget.Switch, + window: Widget.Window, + menubutton: Widget.MenuButton, + popover: Widget.Popover, +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + type Element = Gtk.Widget + type ElementClass = Gtk.Widget + interface IntrinsicElements { + box: Widget.BoxProps + button: Widget.ButtonProps + centerbox: Widget.CenterBoxProps + // circularprogress: Widget.CircularProgressProps + // drawingarea: Widget.DrawingAreaProps + entry: Widget.EntryProps + image: Widget.ImageProps + label: Widget.LabelProps + levelbar: Widget.LevelBarProps + overlay: Widget.OverlayProps + revealer: Widget.RevealerProps + slider: Widget.SliderProps + stack: Widget.StackProps + switch: Widget.SwitchProps + window: Widget.WindowProps + menubutton: Widget.MenuButtonProps + popover: Widget.PopoverProps + } + } +} + +export const jsxs = jsx diff --git a/lang/gjs/src/gtk4/widget.ts b/lang/gjs/src/gtk4/widget.ts index 6c8ea4d..10d4b9f 100644 --- a/lang/gjs/src/gtk4/widget.ts +++ b/lang/gjs/src/gtk4/widget.ts @@ -1 +1,164 @@ -// TODO: +import Astal from "gi://Astal?version=4.0" +import Gtk from "gi://Gtk?version=4.0" +import astalify, { type, type ConstructProps } from "./astalify.js" + +function filter(children: any[]) { + return children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) +} + +// Box +Object.defineProperty(Astal.Box.prototype, "children", { + get() { return this.get_children() }, + set(v) { this.set_children(v) }, +}) + +export type BoxProps = ConstructProps +export const Box = astalify(Astal.Box, { + getChildren(self) { return self.get_children() }, + setChildren(self, children) { return self.set_children(filter(children)) }, +}) + +// Button +export type ButtonProps = ConstructProps +export const Button = astalify(Gtk.Button) + +// CenterBox +export type CenterBoxProps = ConstructProps +export const CenterBox = astalify(Gtk.CenterBox, { + getChildren(box) { + return [box.startWidget, box.centerWidget, box.endWidget] + }, + setChildren(box, children) { + const ch = filter(children) + box.startWidget = ch[0] || new Gtk.Box + box.centerWidget = ch[1] || new Gtk.Box + box.endWidget = ch[2] || new Gtk.Box + }, +}) + +// TODO: CircularProgress +// TODO: DrawingArea + +// Entry +type EntrySignals = { + onActivate: [] + onNotifyText: [] +} + +export type EntryProps = ConstructProps +export const Entry = astalify(Gtk.Entry, { + getChildren() { return [] }, +}) + +// Image +export type ImageProps = ConstructProps +export const Image = astalify(Gtk.Image, { + getChildren() { return [] }, +}) + +// Label +export type LabelProps = ConstructProps +export const Label = astalify(Gtk.Label, { + getChildren() { return [] }, + setChildren(self, children) { self.label = String(children) }, +}) + +// LevelBar +export type LevelBarProps = ConstructProps +export const LevelBar = astalify(Gtk.LevelBar, { + getChildren() { return [] }, +}) + +// TODO: ListBox + +// Overlay +export type OverlayProps = ConstructProps +export const Overlay = astalify(Gtk.Overlay, { + getChildren(self) { + const children: Array = [] + let ch = self.get_first_child() + while (ch !== null) { + children.push(ch) + ch = ch.get_next_sibling() + } + + return children.filter(ch => ch !== self.child) + }, + setChildren(self, children) { + for (const child of filter(children)) { + const types = type in child + ? (child[type] as string).split(/\s+/) + : [] + + if (types.includes("overlay")) { + self.add_overlay(child) + } else { + self.set_child(child) + } + + self.set_measure_overlay(child, types.includes("measure")) + self.set_clip_overlay(child, types.includes("clip")) + } + }, +}) + +// Revealer +export type RevealerProps = ConstructProps +export const Revealer = astalify(Gtk.Revealer) + +// Slider +type SliderSignals = { + onChangeValue: [] +} + +export type SliderProps = ConstructProps +export const Slider = astalify(Astal.Slider, { + getChildren() { return [] }, +}) + +// Stack +export type StackProps = ConstructProps +export const Stack = astalify(Gtk.Stack, { + setChildren(self, children) { + for (const child of filter(children)) { + if (child.name != "" && child.name != null) { + self.add_named(child, child.name) + } else { + self.add_child(child) + } + } + }, +}) + +// Switch +export type SwitchProps = ConstructProps +export const Switch = astalify(Gtk.Switch, { + getChildren() { return [] }, +}) + +// Window +export type WindowProps = ConstructProps +export const Window = astalify(Astal.Window) + +// MenuButton +export type MenuButtonProps = ConstructProps +export const MenuButton = astalify(Gtk.MenuButton, { + getChildren(self) { return [self.popover, self.child] }, + setChildren(self, children) { + for (const child of filter(children)) { + if (child instanceof Gtk.Popover) { + self.set_popover(child) + } else { + self.set_child(child) + } + } + }, +}) + +// Popoper +export type PopoverProps = ConstructProps +export const Popover = astalify(Gtk.Popover) diff --git a/lang/gjs/src/index.ts b/lang/gjs/src/index.ts index 8fe8d01..f448af9 100644 --- a/lang/gjs/src/index.ts +++ b/lang/gjs/src/index.ts @@ -4,5 +4,5 @@ export * from "./process.js" export * from "./time.js" export * from "./file.js" export * from "./gobject.js" -export { bind, default as Binding } from "./binding.js" -export { Variable } from "./variable.js" +export { Binding, bind } from "./binding.js" +export { Variable, derive } from "./variable.js" diff --git a/lang/gjs/src/package.json b/lang/gjs/src/package.json new file mode 100644 index 0000000..b792213 --- /dev/null +++ b/lang/gjs/src/package.json @@ -0,0 +1,22 @@ +{ + "name": "astal", + "type": "module", + "license": "LGPL-2.1", + "exports": { + ".": "./index.ts", + "./gtk3": "./gtk3/index.ts", + "./gtk4": "./gtk4/index.ts", + "./gtk3/app": "./gtk3/app.ts", + "./gtk4/app": "./gtk4/app.ts", + "./gtk3/widget": "./gtk3/widget.ts", + "./gtk4/widget": "./gtk4/widget.ts", + "./gtk3/jsx-runtime": "./gtk3/jsx-runtime.ts", + "./gtk4/jsx-runtime": "./gtk4/jsx-runtime.ts", + "./binding": "./binding.ts", + "./file": "./file.ts", + "./gobject": "./gobject.ts", + "./process": "./process.ts", + "./time": "./time.ts", + "./variable": "./variable.ts" + } +} diff --git a/lang/gjs/src/process.ts b/lang/gjs/src/process.ts index c41adc1..6e3a4a9 100644 --- a/lang/gjs/src/process.ts +++ b/lang/gjs/src/process.ts @@ -50,18 +50,15 @@ export function execAsync(cmd: string | string[]): Promise { Astal.Process.exec_asyncv(cmd, (_, res) => { try { resolve(Astal.Process.exec_asyncv_finish(res)) - } - catch (error) { + } catch (error) { reject(error) } }) - } - else { + } else { Astal.Process.exec_async(cmd, (_, res) => { try { resolve(Astal.Process.exec_finish(res)) - } - catch (error) { + } catch (error) { reject(error) } }) diff --git a/lang/gjs/src/variable.ts b/lang/gjs/src/variable.ts index 9b3d3d2..016d73a 100644 --- a/lang/gjs/src/variable.ts +++ b/lang/gjs/src/variable.ts @@ -60,13 +60,11 @@ class VariableWrapper extends Function { if (v instanceof Promise) { v.then(v => this.set(v)) .catch(err => this.variable.emit("error", err)) - } - else { + } else { this.set(v) } }) - } - else if (this.pollExec) { + } else if (this.pollExec) { this._poll = interval(this.pollInterval, () => { execAsync(this.pollExec!) .then(v => this.set(this.pollTransform!(v, this.get()))) @@ -143,8 +141,7 @@ class VariableWrapper extends Function { if (typeof exec === "function") { this.pollFn = exec delete this.pollExec - } - else { + } else { this.pollExec = exec delete this.pollFn } @@ -188,8 +185,7 @@ class VariableWrapper extends Function { const id = o.connect(s, set) this.onDropped(() => o.disconnect(id)) } - } - else { + } else { if (typeof sigOrFn === "string") { const id = objs.connect(sigOrFn, set) this.onDropped(() => objs.disconnect(id)) @@ -227,4 +223,5 @@ export const Variable = new Proxy(VariableWrapper as any, { new(init: T): Variable } +export const { derive } = Variable export default Variable diff --git a/lang/gjs/tsconfig.json b/lang/gjs/tsconfig.json index 7a3a8c8..571c756 100644 --- a/lang/gjs/tsconfig.json +++ b/lang/gjs/tsconfig.json @@ -13,7 +13,7 @@ "@girs", "src/*.ts", // "src/gtk3/*", - // "src/gtk4/*", + "src/gtk4/*", "index.ts", ] } diff --git a/lib/astal/gtk4/src/meson.build b/lib/astal/gtk4/src/meson.build index 8aac969..7b9c1e0 100644 --- a/lib/astal/gtk4/src/meson.build +++ b/lib/astal/gtk4/src/meson.build @@ -25,6 +25,8 @@ deps = [ ] sources = [config] + files( + 'widget/box.vala', + 'widget/slider.vala', 'widget/window.vala', 'application.vala', ) diff --git a/lib/astal/gtk4/src/widget/box.vala b/lib/astal/gtk4/src/widget/box.vala new file mode 100644 index 0000000..eb52f9c --- /dev/null +++ b/lib/astal/gtk4/src/widget/box.vala @@ -0,0 +1,35 @@ +public class Astal.Box : Gtk.Box { + public List children { + set { + foreach (var child in children) { + remove(child); + } + foreach (var child in value) { + append(child); + } + } + owned get { + var list = new List(); + var child = get_first_child(); + while (child != null) { + list.append(child); + child = child.get_next_sibling(); + } + return list; + } + } + + public Gtk.Widget? child { + owned get { + foreach (var child in children) { + return child; + } + return null; + } + set { + var list = new List(); + list.append(child); + this.children = children; + } + } +} diff --git a/lib/astal/gtk4/src/widget/slider.vala b/lib/astal/gtk4/src/widget/slider.vala new file mode 100644 index 0000000..f8002ec --- /dev/null +++ b/lib/astal/gtk4/src/widget/slider.vala @@ -0,0 +1,67 @@ +public class Astal.Slider : Gtk.Scale { + private Gtk.EventControllerLegacy controller; + private bool dragging; + + construct { + draw_value = false; + + if (adjustment == null) + adjustment = new Gtk.Adjustment(0,0,0,0,0,0); + + if (max == 0 && min == 0) { + max = 1; + } + + if (step == 0) { + step = 0.05; + } + + add_controller(controller); + controller.event.connect((event) => { + var type = event.get_event_type(); + if (type == Gdk.EventType.BUTTON_PRESS || + type == Gdk.EventType.KEY_PRESS || + type == Gdk.EventType.TOUCH_BEGIN) { + dragging = true; + } + if (type == Gdk.EventType.BUTTON_RELEASE || + type == Gdk.EventType.KEY_RELEASE || + type == Gdk.EventType.TOUCH_END) { + dragging = false; + } + }); + } + + + /** + * Value of this slider. Defaults to `0`. + */ + public double value { + get { return adjustment.value; } + set { if (!dragging) adjustment.value = value; } + } + + /** + * Minimum possible value of this slider. Defaults to `0`. + */ + public double min { + get { return adjustment.lower; } + set { adjustment.lower = value; } + } + + /** + * Maximum possible value of this slider. Defaults to `1`. + */ + public double max { + get { return adjustment.upper; } + set { adjustment.upper = value; } + } + + /** + * Size of step increments. Defaults to `0.05`. + */ + public double step { + get { return adjustment.step_increment; } + set { adjustment.step_increment = value; } + } +} diff --git a/nix/devshell.nix b/nix/devshell.nix index 66c46e5..9217a8d 100644 --- a/nix/devshell.nix +++ b/nix/devshell.nix @@ -40,21 +40,34 @@ libdbusmenu-gtk3 wayland blueprint-compiler + libadwaita dart-sass lua python gjs ]; + + lsp = with pkgs; [ + nodejs + mesonlsp + vala-language-server + vtsls + vscode-langservers-extracted + ]; in { default = pkgs.mkShell { - inherit buildInputs; + packages = buildInputs ++ lsp; }; astal = pkgs.mkShell { - buildInputs = + packages = buildInputs + ++ lsp ++ builtins.attrValues ( - builtins.removeAttrs self.packages.${pkgs.system} ["docs"] + builtins.removeAttrs self.packages.${pkgs.system} [ + "docs" + "cava" # FIXME: temporary autoreconf + ] ); }; } -- cgit v1.2.3 From 5a16cd2809d4d6dfa54c7995966630cefa3fe400 Mon Sep 17 00:00:00 2001 From: Aylur Date: Fri, 20 Dec 2024 23:42:59 +0100 Subject: fix: slider style --- lib/astal/gtk4/src/application.vala | 11 ++++------- lib/astal/gtk4/src/widget/slider.vala | 4 +--- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/astal/gtk4/src/application.vala b/lib/astal/gtk4/src/application.vala index fadf705..fe5dd8d 100644 --- a/lib/astal/gtk4/src/application.vala +++ b/lib/astal/gtk4/src/application.vala @@ -146,13 +146,10 @@ public class Astal.Application : Gtk.Application, AstalIO.Application { if (reset) reset_css(); - try { - if (FileUtils.test(style, FileTest.EXISTS)) - provider.load_from_path(style); - else - provider.load_from_string(style); - } catch (Error err) { - critical(err.message); + if (FileUtils.test(style, FileTest.EXISTS)) { + provider.load_from_path(style); + } else { + provider.load_from_string(style); } Gtk.StyleContext.add_provider_for_display( diff --git a/lib/astal/gtk4/src/widget/slider.vala b/lib/astal/gtk4/src/widget/slider.vala index f8002ec..ca026a2 100644 --- a/lib/astal/gtk4/src/widget/slider.vala +++ b/lib/astal/gtk4/src/widget/slider.vala @@ -3,8 +3,6 @@ public class Astal.Slider : Gtk.Scale { private bool dragging; construct { - draw_value = false; - if (adjustment == null) adjustment = new Gtk.Adjustment(0,0,0,0,0,0); @@ -16,6 +14,7 @@ public class Astal.Slider : Gtk.Scale { step = 0.05; } + controller = new Gtk.EventControllerLegacy(); add_controller(controller); controller.event.connect((event) => { var type = event.get_event_type(); @@ -32,7 +31,6 @@ public class Astal.Slider : Gtk.Scale { }); } - /** * Value of this slider. Defaults to `0`. */ -- cgit v1.2.3 From ed23ce29a8db636da1a89501c2103b62070de655 Mon Sep 17 00:00:00 2001 From: Aylur Date: Sat, 21 Dec 2024 00:02:35 +0100 Subject: export hook and widget --- lang/gjs/src/gtk4/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lang/gjs/src/gtk4/index.ts b/lang/gjs/src/gtk4/index.ts index 2cc6862..51c75d2 100644 --- a/lang/gjs/src/gtk4/index.ts +++ b/lang/gjs/src/gtk4/index.ts @@ -6,4 +6,5 @@ import astalify, { type ConstructProps } from "./astalify.js" export { Astal, Gtk, Gdk } export { default as App } from "./app.js" export { astalify, ConstructProps } -// export * as Widget from "./widget.js" +export * as Widget from "./widget.js" +export { hook } from "../_astal" -- cgit v1.2.3 From 27544705dfe77b50f919a166f06d5b96560dd2fd Mon Sep 17 00:00:00 2001 From: Aylur Date: Sat, 21 Dec 2024 00:25:04 +0100 Subject: fix types --- lang/gjs/src/gtk4/widget.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lang/gjs/src/gtk4/widget.ts b/lang/gjs/src/gtk4/widget.ts index 10d4b9f..bd9091b 100644 --- a/lang/gjs/src/gtk4/widget.ts +++ b/lang/gjs/src/gtk4/widget.ts @@ -21,10 +21,12 @@ export const Box = astalify(Astal.Box, { }) // Button -export type ButtonProps = ConstructProps -export const Button = astalify(Gtk.Button) +} + +export type ButtonProps = ConstructProps +export const Button = astalify(Gtk.Button) // CenterBox export type CenterBoxProps = ConstructProps -- cgit v1.2.3 From f00e558c6af391bc7ae0b64190da37f876f12c79 Mon Sep 17 00:00:00 2001 From: Aylur Date: Wed, 25 Dec 2024 02:36:28 +0100 Subject: docs: add gtk4 jsx documentation --- docs/guide/getting-started/introduction.md | 12 +- docs/guide/typescript/cli-app.md | 6 +- docs/guide/typescript/first-widgets.md | 24 +- docs/guide/typescript/theming.md | 15 +- docs/guide/typescript/widget.md | 379 +++++++++++++++++++++++- lang/gjs/src/gtk3/index.ts | 1 + lang/gjs/src/gtk3/jsx-runtime.ts | 2 +- lang/gjs/src/gtk3/widget.ts | 2 - lang/gjs/tsconfig.json | 2 +- lib/astal/gtk3/src/widget/circularprogress.vala | 2 +- lib/astal/gtk4/src/widget/box.vala | 15 + 11 files changed, 424 insertions(+), 36 deletions(-) diff --git a/docs/guide/getting-started/introduction.md b/docs/guide/getting-started/introduction.md index 782c069..43a7bd8 100644 --- a/docs/guide/getting-started/introduction.md +++ b/docs/guide/getting-started/introduction.md @@ -2,13 +2,15 @@ ## What is Astal? -Astal (_meaning "desk"_) is a suite of libraries in Vala and C. +Astal (_meaning "desk"_) is a suite of libraries written in Vala and C. The core library [astal3](https://aylur.github.io/libastal/astal3) and -[astal4](https://aylur.github.io/libastal/astal4) (not yet available) -has some Gtk widgets that come packaged, +[astal4](https://aylur.github.io/libastal/astal4) +have some Gtk widgets that come packaged, the most important one being the [Window](https://aylur.github.io/libastal/astal3/class.Window.html) which is the main toplevel component using [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell). This is what allows us to use Gtk as shell components on Wayland. -The other part of the core library [astal-io](https://aylur.github.io/libastal/astal-io) +The other component is [Application](https://aylur.github.io/libastal/astal3/class.Application.html) +which provides a way to send messages from the cli to running Astal instances. +The other part of the core library is [astal-io](https://aylur.github.io/libastal/astal-io) which contains some utility GLib shortcut for running external processes, reading, writing and monitoring files, timeout and interval functions. @@ -23,4 +25,4 @@ or an applauncher, but gave up because writing a workspace widget, implementing the notification daemon or handling a search filter was too much of a hassle? Astal libraries have you [covered](../libraries/references#astal-libraries), you don't have to worry about these, -you just define the layout, style it with CSS and that's it. +you just define the layout, style with CSS hook up the state from libraries you want and that's it. diff --git a/docs/guide/typescript/cli-app.md b/docs/guide/typescript/cli-app.md index 9b299aa..41b1d7c 100644 --- a/docs/guide/typescript/cli-app.md +++ b/docs/guide/typescript/cli-app.md @@ -26,7 +26,7 @@ App.start({ ## Instance identifier -You can run multiple instance by defining a unique instance name. +You can run multiple instances by defining a unique instance name. ```ts App.start({ @@ -44,7 +44,7 @@ you can do so by sending a message. App.start({ requestHandler(request: string, res: (response: any) => void) { if (request == "say hi") { - res("hi cli") + return res("hi cli") } res("unknown command") }, @@ -140,7 +140,7 @@ App.start({ // every subsequent calls client(message: (msg: string) => string, ...args: Array) { const res = message("you can message the main instance") - console.log(res) + print(res) }, // this runs in the main instance diff --git a/docs/guide/typescript/first-widgets.md b/docs/guide/typescript/first-widgets.md index 77b2f61..9b8bf32 100644 --- a/docs/guide/typescript/first-widgets.md +++ b/docs/guide/typescript/first-widgets.md @@ -71,7 +71,7 @@ function MyButton(): JSX.Element { } ``` -```ts [MyButton.ts] +```ts [MyButton.ts (gtk3)] import { Widget } from "astal/gtk3" function MyButton(): Widget.Button { @@ -82,6 +82,17 @@ function MyButton(): Widget.Button { } ``` +```ts [MyButton.ts (gtk4)] +import { Widget } from "astal/gtk4" + +function MyButton(): Widget.Button { + return Widget.Button( + { onClicked: "echo hello" }, + Widget.Label({ label: "Click me!" }), + ) +} +``` + ::: :::info @@ -218,6 +229,14 @@ Their types are not generated, but written by hand, which means not all of them Refer to the Gtk and Astal docs to have a full list of them. ::: +:::info +Attributes prefixed with `onNotify` will connect to a `notify::` signal of the widget. + +```tsx + print("switched to", self.active)}> +``` +::: + ## How properties are passed Using JSX, a custom widget will always have a single object as its parameter. @@ -413,8 +432,7 @@ function Parent(props: { :::tip If you have a widget where you pass widgets in various ways, you can -wrap `child` in `children` in a [`Subscribable`](./faq#custom-widgets-with-bindable-properties) and handle all cases -as if they were bindings. +wrap `child` and `children` props in a [`Subscribable`](./faq#custom-widgets-with-bindable-properties) and handle all cases as if they were bindings. ::: :::info diff --git a/docs/guide/typescript/theming.md b/docs/guide/typescript/theming.md index 5944c4e..3cafa7d 100644 --- a/docs/guide/typescript/theming.md +++ b/docs/guide/typescript/theming.md @@ -1,24 +1,25 @@ # Theming -Since the widget toolkit is **GTK3** theming is done with **CSS**. +Since the widget toolkit is **GTK** theming is done with **CSS**. - [CSS tutorial](https://www.w3schools.com/css/) -- [GTK CSS Overview wiki](https://docs.gtk.org/gtk3/css-overview.html) -- [GTK CSS Properties Overview wiki](https://docs.gtk.org/gtk3/css-properties.html) +- [GTK3 CSS Overview wiki](https://docs.gtk.org/gtk3/css-overview.html) +- [GTK3 CSS Properties Overview wiki](https://docs.gtk.org/gtk3/css-properties.html) +- [GTK4 CSS Overview wiki](https://docs.gtk.org/gtk4/css-overview.html) +- [GTK4 CSS Properties Overview wiki](https://docs.gtk.org/gtk4/css-properties.html) :::warning GTK is not the web While most features are implemented in GTK, you can't assume anything that works on the web will work with GTK. -Refer to the [GTK docs](https://docs.gtk.org/gtk3/css-overview.html) -to see what is available. +Refer to the GTK docs to see what is available. ::: -So far every widget you made used your default GTK3 theme. +So far every widget you made used your default GTK theme. To make them more custom, you can apply stylesheets to them. ## From file at startup -You can pass a path to a file or css as a string in `App.start` +You can pass a path to a file or CSS as a string in `App.start` :::code-group diff --git a/docs/guide/typescript/widget.md b/docs/guide/typescript/widget.md index 7ed69e3..43c8721 100644 --- a/docs/guide/typescript/widget.md +++ b/docs/guide/typescript/widget.md @@ -6,15 +6,21 @@ These are properties that Astal additionally adds to Gtk.Widgets -- className: `string` - List of class CSS selectors separated by white space. -- css: `string` - Inline CSS. e.g `label { color: white; }`. If no selector is specified `*` will be assumed. e.g `color: white;` will be inferred as `* { color: white; }`. -- cursor: `string` - Cursor style when hovering over widgets that have hover states, e.g it won't work on labels. [list of valid values](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html). -- clickThrough: `boolean` - Lets click events through. +- `className`: `string` - List of class CSS selectors separated by white space. +- `css`: `string` - Inline CSS. e.g `label { color: white; }`. If no selector is specified `*` will be assumed. e.g `color: white;` will be inferred as `* { color: white; }`. +- `cursor`: `string` - Cursor style when hovering over widgets that have hover states, e.g it won't work on labels. [list of valid values](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html). +- `clickThrough`: `boolean` - Lets click events through. To have a full list of available properties, reference the documentation of the widget. - [Astal3 widgets](https://aylur.github.io/libastal/astal3/index.html#classes) -- [Gtk widgets](https://docs.gtk.org/gtk3/#classes) +- [Gtk3 widgets](https://docs.gtk.org/gtk3/#classes) + +Most common ones you will use frequently are + - [halign](https://docs.gtk.org/gtk3/property.Widget.halign.html) + - [valign](https://docs.gtk.org/gtk3/property.Widget.valign.html) + - [hexpand](https://docs.gtk.org/gtk3/property.Widget.hexpand.html) + - [vexpand](https://docs.gtk.org/gtk3/property.Widget.vexpand.html) ### Additional widget methods @@ -27,7 +33,7 @@ without `setup` ```tsx function MyWidget() { - const button = Widget.Button() + const button = new Widget.Button() // setup button return button } @@ -94,14 +100,14 @@ function MyWidget() { ### How to use non builtin Gtk widgets -Using the `Widget.astalify` mixin you can subclass widgets +Using the `astalify` mixin you can subclass widgets to behave like builtin widgets. The `astalify` mixin will apply the following: - set `visible` to true by default (Gtk3 widgets are invisible by default) - make gobject properties accept and consume `Binding` objects - add properties and methods listed above -- sets up signal handlers that are passed as props prefixed with `on` +- set up signal handlers that are passed as props prefixed with `on` ```tsx import GObject from "gi://GObject" @@ -135,7 +141,7 @@ function MyWidget() { alpha: 0.5, })} onColorSet={(self) => { - console.log(self.rgba) + print(self.rgba) }} /> } @@ -144,7 +150,7 @@ function MyWidget() { :::info Signal properties have to be annotated manually for TypeScript. You can reference [Gtk3](https://gjs-docs.gnome.org/gtk30~3.0/) -and [Astal](https://aylur.github.io/libastal/index.html#classes) for available signals. +and [Astal3](https://aylur.github.io/libastal/astal3/#classes) for available signals. ::: ### TypeScript @@ -189,28 +195,375 @@ export default function ToggleButton(btnprops: ToggleButtonProps) { ### Builtin Widgets -You can check the [source code](https://github.com/aylur/astal/blob/main/lang/gjs/src/gtk3/index.ts) to have a full list of builtin widgets. - These widgets are available by default in JSX. - box: [Astal.Box](https://aylur.github.io/libastal/astal3/class.Box.html) + ```tsx + Horizontal Box + ``` + ```tsx + Vertical Box + ``` - button: [Astal.Button](https://aylur.github.io/libastal/astal3/class.Button.html) + ```tsx + + ``` - centerbox: [Astal.CenterBox](https://aylur.github.io/libastal/astal3/class.CenterBox.html) + ```tsx + +