diff options
-rw-r--r-- | flake.lock | 6 | ||||
-rw-r--r-- | lang/gjs/eslint.config.mjs | 1 | ||||
-rw-r--r-- | lang/gjs/meson.build | 6 | ||||
-rw-r--r-- | lang/gjs/package.json | 8 | ||||
-rw-r--r-- | lang/gjs/src/_app.ts | 9 | ||||
-rw-r--r-- | lang/gjs/src/_astal.ts | 188 | ||||
-rw-r--r-- | lang/gjs/src/binding.ts | 8 | ||||
-rw-r--r-- | lang/gjs/src/file.ts | 6 | ||||
-rw-r--r-- | lang/gjs/src/gobject.ts | 10 | ||||
-rw-r--r-- | lang/gjs/src/gtk3/astalify.ts | 232 | ||||
-rw-r--r-- | lang/gjs/src/gtk3/jsx-runtime.ts | 31 | ||||
-rw-r--r-- | lang/gjs/src/gtk3/widget.ts | 20 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/app.ts | 6 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/astalify.ts | 227 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/index.ts | 4 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/jsx-runtime.ts | 69 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/widget.ts | 165 | ||||
-rw-r--r-- | lang/gjs/src/index.ts | 4 | ||||
-rw-r--r-- | lang/gjs/src/package.json | 22 | ||||
-rw-r--r-- | lang/gjs/src/process.ts | 9 | ||||
-rw-r--r-- | lang/gjs/src/variable.ts | 13 | ||||
-rw-r--r-- | lang/gjs/tsconfig.json | 2 | ||||
-rw-r--r-- | lib/astal/gtk4/src/meson.build | 2 | ||||
-rw-r--r-- | lib/astal/gtk4/src/widget/box.vala | 35 | ||||
-rw-r--r-- | lib/astal/gtk4/src/widget/slider.vala | 67 | ||||
-rw-r--r-- | nix/devshell.nix | 19 |
26 files changed, 903 insertions, 266 deletions
@@ -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..6f3285b --- /dev/null +++ b/lang/gjs/src/_astal.ts @@ -0,0 +1,188 @@ +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<T> = { + [K in keyof T]: Binding<T[K]> | T[K]; +} + +export function hook<Widget extends Connectable>( + 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<Widget extends Connectable & { [setChildren]: (children: any[]) => void }>(widget: Widget, config: any) { + const { setup, child, children = [], ...props } = config + + if (child) { + children.unshift(child) + } + + // remove undefined values + for (const [key, value] of Object.entries(props)) { + if (value === undefined) { + delete props[key] + } + } + + // collect bindings + const bindings: Array<[string, Binding<any>]> = 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<string, { new(props: any): any } | ((props: any) => 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<Value> { +export class Binding<Value> { private transformFn = (v: any) => v #emitter: Subscribable<Value> | Connectable @@ -46,7 +46,7 @@ export default class Binding<Value> { return `Binding<${this.#emitter}${this.#prop ? `, "${this.#prop}"` : ""}>` } - as<T>(fn: (v: Value) => T): Binding<T> { + as<T>(fn: (v: Value) => T | Binding<T>): Binding<T> { const bind = new Binding(this.#emitter, this.#prop) bind.transformFn = (v: Value) => fn(this.transformFn(v)) return bind as unknown as Binding<T> @@ -72,8 +72,7 @@ export default class Binding<Value> { 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<Value> { } 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<string> { 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<void> { 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 6bd9969..7a5105f 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 9e6f022..9cab5b2 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,63 +31,47 @@ 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 } set actionGroup([prefix, group]: ActionGroup) { this.insert_action_group(prefix, group) } set_action_group(actionGroup: ActionGroup) { this.actionGroup = actionGroup } - _setChildren(children: Gtk.Widget[]) { + protected getChildren(): Array<Gtk.Widget> { + if (this instanceof Gtk.Bin) { + return this.get_child() ? [this.get_child()!] : [] + } else if (this instanceof Gtk.Container) { + return this.get_children() + } + return [] + } + + 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) { @@ -142,103 +92,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 - - // remove undefined values - for (const [key, value] of Object.entries(props)) { - if (value === undefined) { - delete props[key] - } - } - - 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) } } @@ -266,15 +128,13 @@ export default function astalify< return Widget } -export 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: W, ...args: Args) => unknown) | string | string[] +export type BindableChild = Gtk.Widget | Binding<Gtk.Widget> + export type ConstructProps< Self extends InstanceType<typeof Gtk.Widget>, Props extends Gtk.Widget.ConstructorProps, @@ -284,23 +144,21 @@ export type ConstructProps< [S in keyof Signals]: SigHandler<Self, Signals[S]> }> & Partial<{ [Key in `on${string}`]: SigHandler<Self, any[]> -}> & BindableProps<Partial<Props> & { +}> & BindableProps<Partial<Props & { className?: string css?: string cursor?: string clickThrough?: boolean -}> & { - 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<Gtk.Widget> +}>> & 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 19a3b7d..cc97f2e 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<BindableChild> @@ -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 b3c4a4d..10e4842 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<Box, Astal.Box.ConstructorProps> export class Box extends astalify(Astal.Box) { static { GObject.registerClass({ GTypeName: "Box" }, this) } constructor(props?: BoxProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { this.set_children(filter(children)) } } // Button @@ -35,6 +42,12 @@ export type CenterBoxProps = ConstructProps<CenterBox, Astal.CenterBox.Construct export class CenterBox extends astalify(Astal.CenterBox) { static { GObject.registerClass({ GTypeName: "CenterBox" }, this) } constructor(props?: CenterBoxProps, ...children: Array<BindableChild>) { 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<Label, Astal.Label.ConstructorProps> 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 @@ -121,6 +135,11 @@ export type OverlayProps = ConstructProps<Overlay, Astal.Overlay.ConstructorProp export class Overlay extends astalify(Astal.Overlay) { static { GObject.registerClass({ GTypeName: "Overlay" }, this) } constructor(props?: OverlayProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) } + protected setChildren(children: any[]): void { + const [child, ...overlays] = filter(children) + this.set_child(child) + this.set_overlays(overlays) + } } // Revealer @@ -151,6 +170,7 @@ export type StackProps = ConstructProps<Stack, Astal.Stack.ConstructorProps> export class Stack extends astalify(Astal.Stack) { static { GObject.registerClass({ GTypeName: "Stack" }, this) } constructor(props?: StackProps, ...children: Array<BindableChild>) { 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<Gtk.Widget> { + if ("get_child" in widget && typeof widget.get_child == "function") { + return widget.get_child() ? [widget.get_child()] : [] + } + + const children: Array<Gtk.Widget> = [] + 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<T extends Gtk.Widget> = { + setChildren(widget: T, children: any[]): void + getChildren(widget: T): Array<Gtk.Widget> +} + +export default function astalify< + Widget extends Gtk.Widget, + Props extends Gtk.Widget.ConstructorProps = Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array<unknown>> = Record<`on${string}`, any[]>, +>(cls: { new(...args: any[]): Widget }, config: Partial<Config<Widget>> = {}) { + 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<Widget, Props, Signals> = {}, + ...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<typeof Gtk.Widget>, + Args extends Array<unknown>, +> = ((self: W, ...args: Args) => unknown) | string | string[] + +export { BindableProps } +export type BindableChild = Gtk.Widget | Binding<Gtk.Widget> + +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[]> +}> & Partial<BindableProps<Omit<Props, "cssName" | "css_name">>> & { + noImplicitDestroy?: true + type?: string + cssName?: string +} & EventController<Self> & { + onDestroy?: (self: Self) => unknown + setup?: (self: Self) => void +} + +type EventController<Self extends Gtk.Widget> = { + 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<T>(widget: Gtk.Widget, { + onFocusEnter, + onFocusLeave, + onKeyPressed, + onKeyReleased, + onKeyModifier, + onLegacy, + onButtonPressed, + onButtonReleased, + onHoverEnter, + onHoverLeave, + onMotion, + onScroll, + onScrollDecelerate, + ...props +}: EventController<Gtk.Widget> & 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<BindableChild> +}) { + 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<Astal.Box, Astal.Box.ConstructorProps> +export const Box = astalify<Astal.Box, Astal.Box.ConstructorProps>(Astal.Box, { + getChildren(self) { return self.get_children() }, + setChildren(self, children) { return self.set_children(filter(children)) }, +}) + +// Button +export type ButtonProps = ConstructProps<Gtk.Button, Gtk.Button.ConstructorProps, { + onClicked: [] +}> +export const Button = astalify<Gtk.Button, Gtk.Button.ConstructorProps>(Gtk.Button) + +// CenterBox +export type CenterBoxProps = ConstructProps<Gtk.CenterBox, Gtk.CenterBox.ConstructorProps> +export const CenterBox = astalify<Gtk.CenterBox, Gtk.CenterBox.ConstructorProps>(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<Gtk.Entry, Gtk.Entry.ConstructorProps, EntrySignals> +export const Entry = astalify<Gtk.Entry, Gtk.Entry.ConstructorProps, EntrySignals>(Gtk.Entry, { + getChildren() { return [] }, +}) + +// Image +export type ImageProps = ConstructProps<Gtk.Image, Gtk.Image.ConstructorProps> +export const Image = astalify<Gtk.Image, Gtk.Image.ConstructorProps>(Gtk.Image, { + getChildren() { return [] }, +}) + +// Label +export type LabelProps = ConstructProps<Gtk.Label, Gtk.Label.ConstructorProps> +export const Label = astalify<Gtk.Label, Gtk.Label.ConstructorProps>(Gtk.Label, { + getChildren() { return [] }, + setChildren(self, children) { self.label = String(children) }, +}) + +// LevelBar +export type LevelBarProps = ConstructProps<Gtk.LevelBar, Gtk.LevelBar.ConstructorProps> +export const LevelBar = astalify<Gtk.LevelBar, Gtk.LevelBar.ConstructorProps>(Gtk.LevelBar, { + getChildren() { return [] }, +}) + +// TODO: ListBox + +// Overlay +export type OverlayProps = ConstructProps<Gtk.Overlay, Gtk.Overlay.ConstructorProps> +export const Overlay = astalify<Gtk.Overlay, Gtk.Overlay.ConstructorProps>(Gtk.Overlay, { + getChildren(self) { + const children: Array<Gtk.Widget> = [] + 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<Gtk.Revealer, Gtk.Revealer.ConstructorProps> +export const Revealer = astalify<Gtk.Revealer, Gtk.Revealer.ConstructorProps>(Gtk.Revealer) + +// Slider +type SliderSignals = { + onChangeValue: [] +} + +export type SliderProps = ConstructProps<Astal.Slider, Astal.Slider.ConstructorProps, SliderSignals> +export const Slider = astalify<Astal.Slider, Astal.Slider.ConstructorProps, SliderSignals>(Astal.Slider, { + getChildren() { return [] }, +}) + +// Stack +export type StackProps = ConstructProps<Gtk.Stack, Gtk.Stack.ConstructorProps> +export const Stack = astalify<Gtk.Stack, Gtk.Stack.ConstructorProps>(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<Gtk.Switch, Gtk.Switch.ConstructorProps> +export const Switch = astalify<Gtk.Switch, Gtk.Switch.ConstructorProps>(Gtk.Switch, { + getChildren() { return [] }, +}) + +// Window +export type WindowProps = ConstructProps<Astal.Window, Astal.Window.ConstructorProps> +export const Window = astalify<Astal.Window, Astal.Window.ConstructorProps>(Astal.Window) + +// MenuButton +export type MenuButtonProps = ConstructProps<Gtk.MenuButton, Gtk.MenuButton.ConstructorProps> +export const MenuButton = astalify<Gtk.MenuButton, Gtk.MenuButton.ConstructorProps>(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<Gtk.Popover, Gtk.Popover.ConstructorProps> +export const Popover = astalify<Gtk.Popover, Gtk.Popover.ConstructorProps>(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<string> { 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<T> 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<T> 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<T> 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<T>(init: T): Variable<T> } +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<weak Gtk.Widget> children { + set { + foreach (var child in children) { + remove(child); + } + foreach (var child in value) { + append(child); + } + } + owned get { + var list = new List<weak Gtk.Widget>(); + 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<weak Gtk.Widget>(); + 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 + ] ); }; } |