parent
e44065588d
commit
c3c4054793
22 changed files with 223 additions and 224 deletions
modules/ags/gtk4/widget/astalify
165
modules/ags/gtk4/widget/astalify/astalify.ts
Normal file
165
modules/ags/gtk4/widget/astalify/astalify.ts
Normal 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;
|
||||
};
|
27
modules/ags/gtk4/widget/astalify/bindings.ts
Normal file
27
modules/ags/gtk4/widget/astalify/bindings.ts
Normal 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)();
|
||||
};
|
131
modules/ags/gtk4/widget/astalify/construct.ts
Normal file
131
modules/ags/gtk4/widget/astalify/construct.ts
Normal 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;
|
||||
};
|
120
modules/ags/gtk4/widget/astalify/controller.ts
Normal file
120
modules/ags/gtk4/widget/astalify/controller.ts
Normal 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;
|
||||
};
|
28
modules/ags/gtk4/widget/astalify/generics.ts
Normal file
28
modules/ags/gtk4/widget/astalify/generics.ts
Normal 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[]
|
||||
}
|
5
modules/ags/gtk4/widget/astalify/index.ts
Normal file
5
modules/ags/gtk4/widget/astalify/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import astalify from './astalify';
|
||||
|
||||
export default astalify;
|
||||
|
||||
export { type AstalifyProps, type BindableProps, childType } from './generics';
|
Loading…
Add table
Add a link
Reference in a new issue