feat(ags4): implement independent astalify
All checks were successful
Discord / discord commits (push) Has been skipped
All checks were successful
Discord / discord commits (push) Has been skipped
This commit is contained in:
parent
f876d070f8
commit
0aeedb3042
2 changed files with 168 additions and 1 deletions
167
modules/ags/gtk4/widget/subclasses/_astal.ts
Normal file
167
modules/ags/gtk4/widget/subclasses/_astal.ts
Normal file
|
@ -0,0 +1,167 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { execAsync, Variable } from 'astal';
|
||||
import Binding, { Connectable, kebabify, snakeify, Subscribable } from 'astal/binding';
|
||||
|
||||
export const noImplicitDestroy = Symbol('no no implicit destroy');
|
||||
export const setChildren = Symbol('children setter method');
|
||||
|
||||
const mergeBindings = (array: any[]) => {
|
||||
const 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)();
|
||||
};
|
||||
|
||||
const 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 const 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 const construct = <Widget extends Connectable & {
|
||||
[setChildren]: (children: any[]) => void
|
||||
}>(widget: Widget, config: any) => {
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { setup, child, children = [], ...props } = config;
|
||||
|
||||
if (children instanceof Binding) {
|
||||
children = [children];
|
||||
}
|
||||
|
||||
if (child) {
|
||||
children.unshift(child);
|
||||
}
|
||||
|
||||
// remove undefined values
|
||||
for (const [key, value] of Object.entries(props)) {
|
||||
if (typeof value === 'undefined') {
|
||||
delete props[key];
|
||||
}
|
||||
}
|
||||
|
||||
// collect bindings
|
||||
const bindings: [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: [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 (typeof value === 'undefined') {
|
||||
delete props[key];
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(widget, props);
|
||||
setup?.(widget);
|
||||
|
||||
return widget;
|
||||
};
|
|
@ -10,7 +10,7 @@ import {
|
|||
noImplicitDestroy,
|
||||
setChildren,
|
||||
construct,
|
||||
} from 'astal/_astal';
|
||||
} from './_astal';
|
||||
|
||||
export type BindableChild = Gtk.Widget | Binding<Gtk.Widget>;
|
||||
|
||||
|
|
Loading…
Reference in a new issue