refactor(ags4): split up astalify code

This commit is contained in:
matt1432 2025-01-14 10:12:20 -05:00
parent e44065588d
commit c3c4054793
22 changed files with 223 additions and 224 deletions

View file

@ -0,0 +1,165 @@
import { property, register } from 'astal';
import { Gtk, hook } from 'astal/gtk4';
import { type Connectable, type Subscribable } from 'astal/binding';
import construct from './construct';
import setupControllers from './controller';
import {
type BindableProps,
dummyBuilder,
type MixinParams,
noImplicitDestroy,
setChildren,
childType,
} from './generics';
export default <
C extends new (...props: MixinParams) => Gtk.Widget,
ConstructorProps,
>(
cls: C,
clsName = cls.name,
) => {
@register({ GTypeName: `RealClass_${clsName}` })
class Widget extends cls {
declare private _css: string | undefined;
declare private _provider: Gtk.CssProvider | undefined;
@property(String)
get css(): string | undefined {
return this._css;
}
set css(value: string) {
if (!this._provider) {
this._provider = new Gtk.CssProvider();
this.get_style_context().add_provider(
this._provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER,
);
}
this._css = value;
this._provider.load_from_string(value);
}
declare private [childType]: string;
@property(String)
get type(): string { return this[childType]; }
set type(value: string) { this[childType] = value; }
@property(Object)
get children(): Gtk.Widget[] { return this.getChildren(this); }
set children(value: Gtk.Widget[]) { this.setChildren(this, value); }
declare private [noImplicitDestroy]: boolean;
@property(String)
get noImplicitDestroy(): boolean { return this[noImplicitDestroy]; }
set noImplicitDestroy(value: boolean) { this[noImplicitDestroy] = value; }
protected getChildren(widget: Gtk.Widget): Gtk.Widget[] {
if ('get_child' in widget && typeof widget.get_child == 'function') {
return widget.get_child() ? [widget.get_child()] : [];
}
const children: Gtk.Widget[] = [];
let ch = widget.get_first_child();
while (ch !== null) {
children.push(ch);
ch = ch.get_next_sibling();
}
return children;
}
protected setChildren(widget: Gtk.Widget, children: Gtk.Widget[]) {
for (const child of children) {
widget.vfunc_add_child(
dummyBuilder,
child,
childType in widget ? widget[childType] as string : null,
);
}
}
[setChildren](children: Gtk.Widget[]) {
for (const child of (this.getChildren(this))) {
child?.unparent();
if (!children.includes(child) && noImplicitDestroy in this) {
child.run_dispose();
}
}
this.setChildren(this, children);
}
hook(
object: Connectable,
signal: string,
callback: (self: this, ...args: unknown[]) => void,
): this;
hook(
object: Subscribable,
callback: (self: this, ...args: unknown[]) => void,
): this;
hook(
object: Connectable | Subscribable,
signalOrCallback: string | ((self: this, ...args: unknown[]) => void),
callback?: (self: this, ...args: unknown[]) => void,
) {
hook(this, object, signalOrCallback, callback);
return this;
}
constructor(...params: MixinParams) {
const props = params[0] || {};
super('cssName' in props ? { cssName: props.cssName } : {});
if ('cssName' in props) {
delete props.cssName;
}
if (props.noImplicitDestroy) {
this.noImplicitDestroy = true;
delete props.noImplicitDestroy;
}
if (props.type) {
this.type = props.type;
delete props.type;
}
construct(this, setupControllers(this, props));
}
}
type Constructor<Instance, Props> = new (...args: Props[]) => Instance;
type WidgetClass = Constructor<
Widget & Gtk.Widget & InstanceType<C>,
Partial<BindableProps<ConstructorProps>>
>;
// override the parameters of the `super` constructor
return Widget as unknown as WidgetClass;
};

View file

@ -0,0 +1,27 @@
import { Variable } from 'astal';
import { Binding } from 'astal/binding';
export const mergeBindings = <Value = unknown>(
array: (Value | Binding<Value> | Binding<Value[]>)[],
): Value[] | Binding<Value[]> => {
const getValues = (args: Value[]) => {
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 as Value[];
}
if (bindings.length === 1) {
return (bindings[0] as Binding<Value[]>).as(getValues);
}
return Variable.derive(bindings, getValues)();
};

View file

@ -0,0 +1,131 @@
import { execAsync } from 'astal';
import { type Gtk, type ConstructProps } from 'astal/gtk4';
import { Binding, kebabify, snakeify } from 'astal/binding';
import { mergeBindings } from './bindings';
import { type EventController } from './controller';
import { type AstalifyProps, type BindableProps, type GenericWidget, setChildren } from './generics';
export default <
Self extends GenericWidget,
Props extends Gtk.Widget.ConstructorProps,
>(
widget: Self,
props: Omit<
ConstructProps<Self, Props> & Partial<BindableProps<AstalifyProps>>,
keyof EventController<Self>
>,
) => {
type Key = keyof typeof props;
const keys = Object.keys(props) as Key[];
const entries = Object.entries(props) as [Key, unknown][];
const setProp = (prop: Key, value: Self[keyof Self]) => {
try {
const setter = `set_${snakeify(prop.toString())}` as keyof Self;
if (typeof widget[setter] === 'function') {
return widget[setter](value);
}
return (widget[prop as keyof Self] = value);
}
catch (error) {
console.error(`could not set property "${prop.toString()}" on ${widget}:`, error);
}
};
const children = props.children ?
props.children instanceof Binding ?
[props.children] as (Binding<Gtk.Widget[]> | Binding<Gtk.Widget> | Gtk.Widget)[] :
props.children as Gtk.Widget[] :
[];
if (props.child) {
children.unshift(props.child);
}
// remove undefined values
for (const [key, value] of entries) {
if (typeof value === 'undefined') {
delete props[key];
}
}
// collect bindings
const bindings: [Key, Binding<unknown>][] = [];
for (const key of keys) {
if (props[key] instanceof Binding) {
bindings.push([key, props[key]]);
delete props[key];
}
}
// collect signal handlers
const onHandlers: [string, string | (() => void)][] = [];
for (const key of keys) {
if (key.toString().startsWith('on')) {
const sig = kebabify(key.toString()).split('-').slice(1).join('-');
onHandlers.push([sig, props[key] as string | (() => void)]);
delete props[key];
}
}
// set children
const mergedChildren = mergeBindings<Gtk.Widget>(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 as Binding<Gtk.Widget[]>).subscribe((v: Gtk.Widget[]) => {
widget[setChildren](v);
}));
}
widget.connect('destroy', binding.subscribe((v: unknown) => {
setProp(prop, v as Self[keyof Self]);
}));
setProp(prop, binding.get() as Self[keyof Self]);
}
// filter undefined values
for (const [key, value] of entries) {
if (typeof value === 'undefined') {
delete props[key];
}
}
Object.assign(widget, props);
props.setup?.(widget);
return widget;
};

View file

@ -0,0 +1,120 @@
import { Gdk, Gtk } from 'astal/gtk4';
export interface 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
}
export default <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;
};

View file

@ -0,0 +1,28 @@
import { Gtk } from 'astal/gtk4';
import { Binding } from 'astal/binding';
// A mixin class must have a constructor with a single rest parameter of type 'any[]'
// eslint-disable-next-line "@typescript-eslint/no-explicit-any"
export type MixinParams = any[];
export type BindableChild = Gtk.Widget | Binding<Gtk.Widget>;
export type BindableProps<T> = {
[K in keyof T]: Binding<T[K]> | T[K];
};
export const noImplicitDestroy = Symbol('no no implicit destroy');
export const setChildren = Symbol('children setter method');
export const childType = Symbol('child type');
export const dummyBuilder = new Gtk.Builder();
export type GenericWidget = InstanceType<typeof Gtk.Widget> & {
[setChildren]: (children: Gtk.Widget[]) => void
};
export interface AstalifyProps {
css: string
child: Gtk.Widget
children: Gtk.Widget[]
}

View file

@ -0,0 +1,5 @@
import astalify from './astalify';
export default astalify;
export { type AstalifyProps, type BindableProps, childType } from './generics';