diff --git a/modules/ags/gtk4/widget/subclasses/_astal.ts b/modules/ags/gtk4/widget/subclasses/_astal.ts index 13186c5c..4c41f4e4 100644 --- a/modules/ags/gtk4/widget/subclasses/_astal.ts +++ b/modules/ags/gtk4/widget/subclasses/_astal.ts @@ -1,14 +1,44 @@ import { execAsync, Variable } from 'astal'; -import { Gtk } from 'astal/gtk4'; -import Binding, { Connectable, kebabify, snakeify, Subscribable } from 'astal/binding'; +import { type Gdk, type Gtk, type ConstructProps } from 'astal/gtk4'; +import { Binding, type Connectable, kebabify, snakeify, type Subscribable } from 'astal/binding'; + +export interface 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 +} + +export type BindableProps = { + [K in keyof T]: Binding | T[K]; +}; + +export interface AstalifyProps { + css: string + child: Gtk.Widget + children: Gtk.Widget[] +} export const noImplicitDestroy = Symbol('no no implicit destroy'); export const setChildren = Symbol('children setter method'); const mergeBindings = ( - array: (Value | Binding)[], + array: (Value | Binding | Binding)[], ): Value[] | Binding => { - const getValues = (...args: Value[]) => { + const getValues = (args: Value[]) => { let i = 0; return array.map((value) => value instanceof Binding ? @@ -23,26 +53,12 @@ const mergeBindings = ( } if (bindings.length === 1) { - return bindings[0].as(getValues); + return (bindings[0] as Binding).as(getValues); } return Variable.derive(bindings, getValues)(); }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const setProp = (obj: any, prop: string, value: unknown) => { - 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: Widget, object: Connectable | Subscribable, @@ -67,64 +83,82 @@ export const hook = ( } }; -export const construct = void - // eslint-disable-next-line @typescript-eslint/no-explicit-any -}>(widget: Widget, config: any) => { - // eslint-disable-next-line prefer-const - let { setup, child, children = [], ...props } = config; +export const construct = < + Self extends InstanceType & { + [setChildren]: (children: Gtk.Widget[]) => void + }, + Props extends Gtk.Widget.ConstructorProps, +>( + widget: Self, + props: Omit< + ConstructProps & Partial>, + keyof EventController + >, +) => { + type Key = keyof typeof props; + const keys = Object.keys(props) as Key[]; + const entries = Object.entries(props) as [Key, unknown][]; - if (children instanceof Binding) { - children = [children]; - } + const setProp = (prop: Key, value: Self[keyof Self]) => { + try { + const setter = `set_${snakeify(prop.toString())}` as keyof Self; - if (child) { - children.unshift(child); + 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 | Binding | Gtk.Widget)[] : + props.children as Gtk.Widget[] : + []; + + if (props.child) { + children.unshift(props.child); } // remove undefined values - for (const [key, value] of Object.entries(props)) { + for (const [key, value] of entries) { if (typeof value === 'undefined') { delete props[key]; } } // collect bindings - const bindings: [string, Binding][] = Object - .keys(props) - .reduce((acc: [string, Binding][], prop) => { - if (props[prop] instanceof Binding) { - const binding = props[prop]; + const bindings: [Key, Binding][] = []; - delete props[prop]; - - return [...acc, [prop, binding]]; - } - - return acc; - }, []); + 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 | (() => unknown)][] = Object - .keys(props) - .reduce((acc: [string, Binding][], key) => { - if (key.startsWith('on')) { - const sig = kebabify(key).split('-').slice(1).join('-'); - const handler = props[key]; + const onHandlers: [string, string | (() => void)][] = []; - delete props[key]; + for (const key of keys) { + if (key.toString().startsWith('on')) { + const sig = kebabify(key.toString()).split('-').slice(1).join('-'); - return [...acc, [sig, handler]]; - } - - return acc; - }, []); + onHandlers.push([sig, props[key] as string | (() => void)]); + delete props[key]; + } + } // 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); })); @@ -156,20 +190,20 @@ export const construct = { - setProp(widget, prop, v); + setProp(prop, v as Self[keyof Self]); })); - setProp(widget, prop, binding.get()); + setProp(prop, binding.get() as Self[keyof Self]); } // filter undefined values - for (const [key, value] of Object.entries(props)) { + for (const [key, value] of entries) { if (typeof value === 'undefined') { delete props[key]; } } Object.assign(widget, props); - setup?.(widget); + props.setup?.(widget); return widget; }; diff --git a/modules/ags/gtk4/widget/subclasses/astalify.ts b/modules/ags/gtk4/widget/subclasses/astalify.ts index 8e47169a..d6d7c36f 100644 --- a/modules/ags/gtk4/widget/subclasses/astalify.ts +++ b/modules/ags/gtk4/widget/subclasses/astalify.ts @@ -1,44 +1,26 @@ -import Gdk from 'gi://Gdk?version=4.0'; -import Gtk from 'gi://Gtk?version=4.0'; +// A mixin class must have a constructor with a single rest parameter of type 'any[]' +/* eslint "@typescript-eslint/no-explicit-any": ["error", { "ignoreRestArgs": true }] */ import { property, register } from 'astal'; +import { Gdk, Gtk } from 'astal/gtk4'; import Binding, { type Connectable, type Subscribable } from 'astal/binding'; + import { + type EventController, hook, noImplicitDestroy, setChildren, construct, } from './_astal'; +import { type AstalifyProps, type BindableProps } from './_astal'; +export { type AstalifyProps, type BindableProps }; + export type BindableChild = Gtk.Widget | Binding; -export interface AstalifyProps { - css: string - children: Gtk.Widget[] -} export const type = Symbol('child type'); const dummyBuilder = new Gtk.Builder(); -interface 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 -} - const setupControllers = (widget: Gtk.Widget, { onFocusEnter, onFocusLeave, @@ -79,7 +61,9 @@ const setupControllers = (widget: Gtk.Widget, { onKeyReleased(widget, val, code, state)); } - if (onKeyModifier) { key.connect('modifiers', (_, state) => onKeyModifier(widget, state)); } + if (onKeyModifier) { + key.connect('modifiers', (_, state) => onKeyModifier(widget, state)); + } } if (onLegacy || onButtonPressed || onButtonReleased) { @@ -105,11 +89,17 @@ const setupControllers = (widget: Gtk.Widget, { widget.add_controller(hover); - if (onHoverEnter) { hover.connect('enter', (_, x, y) => onHoverEnter(widget, x, y)); } + if (onHoverEnter) { + hover.connect('enter', (_, x, y) => onHoverEnter(widget, x, y)); + } - if (onHoverLeave) { hover.connect('leave', () => onHoverLeave(widget)); } + if (onHoverLeave) { + hover.connect('leave', () => onHoverLeave(widget)); + } - if (onMotion) { hover.connect('motion', (_, x, y) => onMotion(widget, x, y)); } + if (onMotion) { + hover.connect('motion', (_, x, y) => onMotion(widget, x, y)); + } } if (onScroll || onScrollDecelerate) { @@ -117,7 +107,9 @@ const setupControllers = (widget: Gtk.Widget, { widget.add_controller(scroll); - if (onScroll) { scroll.connect('scroll', (_, x, y) => onScroll(widget, x, y)); } + if (onScroll) { + scroll.connect('scroll', (_, x, y) => onScroll(widget, x, y)); + } if (onScrollDecelerate) { scroll.connect('decelerate', (_, x, y) => onScrollDecelerate(widget, x, y)); @@ -127,8 +119,10 @@ const setupControllers = (widget: Gtk.Widget, { return props; }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default Gtk.Widget>( +export default < + C extends new (...props: any[]) => Gtk.Widget, + ConstructorProps, +>( cls: C, clsName = cls.name, ) => { @@ -240,7 +234,6 @@ export default Gtk.Widget>( } - // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(...params: any[]) { const props = params[0] || {}; @@ -264,5 +257,12 @@ export default Gtk.Widget>( } } - return Widget; + type Constructor = new (...args: Props[]) => Instance; + + type WidgetClass = Constructor< + Widget & Gtk.Widget & InstanceType, + Partial> + >; + + return Widget as unknown as WidgetClass; }; diff --git a/modules/ags/gtk4/widget/subclasses/box.ts b/modules/ags/gtk4/widget/subclasses/box.ts index 1dfb8341..9a22016a 100644 --- a/modules/ags/gtk4/widget/subclasses/box.ts +++ b/modules/ags/gtk4/widget/subclasses/box.ts @@ -12,8 +12,7 @@ export type BoxProps = ConstructProps< @register({ GTypeName: 'Box' }) export class BoxClass extends astalify(Astal.Box) { constructor({ cssName = 'box', ...props }: BoxProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren(self: BoxClass) { diff --git a/modules/ags/gtk4/widget/subclasses/button.ts b/modules/ags/gtk4/widget/subclasses/button.ts index 9eff53bf..5308b7b9 100644 --- a/modules/ags/gtk4/widget/subclasses/button.ts +++ b/modules/ags/gtk4/widget/subclasses/button.ts @@ -16,8 +16,7 @@ export type ButtonProps = ConstructProps< @register({ GTypeName: 'Button' }) export class ButtonClass extends astalify(Gtk.Button) { constructor({ cssName = 'button', ...props }: ButtonProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } } diff --git a/modules/ags/gtk4/widget/subclasses/calendar.ts b/modules/ags/gtk4/widget/subclasses/calendar.ts index 8388472a..ede72aec 100644 --- a/modules/ags/gtk4/widget/subclasses/calendar.ts +++ b/modules/ags/gtk4/widget/subclasses/calendar.ts @@ -21,8 +21,7 @@ export type CalendarProps = ConstructProps< @register({ GTypeName: 'Calendar' }) export class CalendarClass extends astalify(Gtk.Calendar) { constructor({ cssName = 'calendar', ...props }: CalendarProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } } diff --git a/modules/ags/gtk4/widget/subclasses/centerbox.ts b/modules/ags/gtk4/widget/subclasses/centerbox.ts index defc50bd..e72de0a9 100644 --- a/modules/ags/gtk4/widget/subclasses/centerbox.ts +++ b/modules/ags/gtk4/widget/subclasses/centerbox.ts @@ -12,8 +12,7 @@ export type CenterBoxProps = ConstructProps< @register({ GTypeName: 'CenterBox' }) export class CenterBoxClass extends astalify(Gtk.CenterBox) { constructor({ cssName = 'centerbox', ...props }: CenterBoxProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren(box: CenterBoxClass) { diff --git a/modules/ags/gtk4/widget/subclasses/entry.ts b/modules/ags/gtk4/widget/subclasses/entry.ts index e5b1cf3e..32546625 100644 --- a/modules/ags/gtk4/widget/subclasses/entry.ts +++ b/modules/ags/gtk4/widget/subclasses/entry.ts @@ -17,8 +17,7 @@ export type EntryProps = ConstructProps< @register({ GTypeName: 'Entry' }) export class EntryClass extends astalify(Gtk.Entry) { constructor({ cssName = 'entry', ...props }: EntryProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren() { return []; } diff --git a/modules/ags/gtk4/widget/subclasses/image.ts b/modules/ags/gtk4/widget/subclasses/image.ts index 4c7a3aed..74a71f8c 100644 --- a/modules/ags/gtk4/widget/subclasses/image.ts +++ b/modules/ags/gtk4/widget/subclasses/image.ts @@ -12,8 +12,7 @@ export type ImageProps = ConstructProps< @register({ GTypeName: 'Image' }) export class ImageClass extends astalify(Gtk.Image) { constructor({ cssName = 'image', ...props }: ImageProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren() { return []; } diff --git a/modules/ags/gtk4/widget/subclasses/label.ts b/modules/ags/gtk4/widget/subclasses/label.ts index 216144b7..80168a62 100644 --- a/modules/ags/gtk4/widget/subclasses/label.ts +++ b/modules/ags/gtk4/widget/subclasses/label.ts @@ -12,8 +12,7 @@ export type LabelProps = ConstructProps< @register({ GTypeName: 'Label' }) export class LabelClass extends astalify(Gtk.Label) { constructor({ cssName = 'label', ...props }: LabelProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren() { return []; } diff --git a/modules/ags/gtk4/widget/subclasses/levelbar.ts b/modules/ags/gtk4/widget/subclasses/levelbar.ts index 10e8785c..5a10e367 100644 --- a/modules/ags/gtk4/widget/subclasses/levelbar.ts +++ b/modules/ags/gtk4/widget/subclasses/levelbar.ts @@ -12,8 +12,7 @@ export type LevelBarProps = ConstructProps< @register({ GTypeName: 'LevelBar' }) export class LevelBarClass extends astalify(Gtk.LevelBar) { constructor({ cssName = 'levelbar', ...props }: LevelBarProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren() { return []; } diff --git a/modules/ags/gtk4/widget/subclasses/menubutton.ts b/modules/ags/gtk4/widget/subclasses/menubutton.ts index a699a138..aa09f4f2 100644 --- a/modules/ags/gtk4/widget/subclasses/menubutton.ts +++ b/modules/ags/gtk4/widget/subclasses/menubutton.ts @@ -12,8 +12,7 @@ export type MenuButtonProps = ConstructProps< @register({ GTypeName: 'MenuButton' }) export class MenuButtonClass extends astalify(Gtk.MenuButton) { constructor({ cssName = 'menubutton', ...props }: MenuButtonProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren(self: MenuButtonClass) { diff --git a/modules/ags/gtk4/widget/subclasses/overlay.ts b/modules/ags/gtk4/widget/subclasses/overlay.ts index 7af05faa..ca0e90d5 100644 --- a/modules/ags/gtk4/widget/subclasses/overlay.ts +++ b/modules/ags/gtk4/widget/subclasses/overlay.ts @@ -12,8 +12,7 @@ export type OverlayProps = ConstructProps< @register({ GTypeName: 'Overlay' }) export class OverlayClass extends astalify(Gtk.Overlay) { constructor({ cssName = 'overlay', ...props }: OverlayProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren(self: OverlayClass) { diff --git a/modules/ags/gtk4/widget/subclasses/popover.ts b/modules/ags/gtk4/widget/subclasses/popover.ts index 557bac6f..f334b3cb 100644 --- a/modules/ags/gtk4/widget/subclasses/popover.ts +++ b/modules/ags/gtk4/widget/subclasses/popover.ts @@ -12,8 +12,7 @@ export type PopoverProps = ConstructProps< @register({ GTypeName: 'Popover' }) export class PopoverClass extends astalify(Gtk.Popover) { constructor({ cssName = 'popover', ...props }: PopoverProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } } diff --git a/modules/ags/gtk4/widget/subclasses/revealer.ts b/modules/ags/gtk4/widget/subclasses/revealer.ts index af5a4308..9608355e 100644 --- a/modules/ags/gtk4/widget/subclasses/revealer.ts +++ b/modules/ags/gtk4/widget/subclasses/revealer.ts @@ -12,8 +12,7 @@ export type RevealerProps = ConstructProps< @register({ GTypeName: 'Revealer' }) export class RevealerClass extends astalify(Gtk.Revealer) { constructor({ cssName = 'revealer', ...props }: RevealerProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } } diff --git a/modules/ags/gtk4/widget/subclasses/slider.ts b/modules/ags/gtk4/widget/subclasses/slider.ts index b5725b1e..8f53ed5a 100644 --- a/modules/ags/gtk4/widget/subclasses/slider.ts +++ b/modules/ags/gtk4/widget/subclasses/slider.ts @@ -16,8 +16,7 @@ export type SliderProps = ConstructProps< @register({ GTypeName: 'Slider' }) export class SliderClass extends astalify(Astal.Slider) { constructor({ cssName = 'slider', ...props }: SliderProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren() { return []; } diff --git a/modules/ags/gtk4/widget/subclasses/stack.ts b/modules/ags/gtk4/widget/subclasses/stack.ts index 9cb2aabe..b1bf835f 100644 --- a/modules/ags/gtk4/widget/subclasses/stack.ts +++ b/modules/ags/gtk4/widget/subclasses/stack.ts @@ -12,8 +12,7 @@ export type StackProps = ConstructProps< @register({ GTypeName: 'Stack' }) export class StackClass extends astalify(Gtk.Stack) { constructor({ cssName = 'stack', ...props }: StackProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } setChildren(self: StackClass, children: Gtk.Widget[]) { diff --git a/modules/ags/gtk4/widget/subclasses/switch.ts b/modules/ags/gtk4/widget/subclasses/switch.ts index 06eeb104..383c1bb1 100644 --- a/modules/ags/gtk4/widget/subclasses/switch.ts +++ b/modules/ags/gtk4/widget/subclasses/switch.ts @@ -12,8 +12,7 @@ export type SwitchProps = ConstructProps< @register({ GTypeName: 'Switch' }) export class SwitchClass extends astalify(Gtk.Switch) { constructor({ cssName = 'switch', ...props }: SwitchProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } getChildren() { return []; } diff --git a/modules/ags/gtk4/widget/subclasses/window.ts b/modules/ags/gtk4/widget/subclasses/window.ts index 8124b2b3..9febebfb 100644 --- a/modules/ags/gtk4/widget/subclasses/window.ts +++ b/modules/ags/gtk4/widget/subclasses/window.ts @@ -12,8 +12,7 @@ export type WindowProps = ConstructProps< @register({ GTypeName: 'Window' }) export class WindowClass extends astalify(Astal.Window) { constructor({ cssName = 'window', ...props }: WindowProps = {}) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - super({ cssName, ...props as any }); + super({ cssName, ...props }); } }