feat(ags4): start subclassing widgets
All checks were successful
Discord / discord commits (push) Has been skipped

This commit is contained in:
matt1432 2025-01-01 02:55:15 -05:00
parent dc410ebf59
commit 362bc7314d
8 changed files with 357 additions and 42 deletions

View file

@ -2,7 +2,7 @@ import { App } from 'astal/gtk4';
import style from './style.scss';
import Bar from './widget/Bar';
import Bar from './widget/bar';
App.start({

View file

@ -1,13 +1,12 @@
import { App, Astal, Gtk } from 'astal/gtk4';
import { Variable } from 'astal';
import { StyledBox } from './styled-box';
import { Box, Button, CenterBox } from './subclasses';
const { EXCLUSIVE } = Astal.Exclusivity;
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor;
const { CENTER } = Gtk.Align;
const time = Variable(0);
setInterval(() => {
@ -16,10 +15,10 @@ setInterval(() => {
export default () => {
const styledBox = (
<StyledBox
<Box
css={time().as((t) => `* { background: red; min-height: 10px; min-width: ${t}px; }`)}
/>
) as StyledBox;
) as Box;
return (
<window
@ -29,8 +28,8 @@ export default () => {
anchor={TOP | LEFT | RIGHT}
application={App}
>
<centerbox cssName="centerbox">
<box />
<CenterBox cssName="centerbox">
<Button onClicked="echo hi" />
{styledBox}
@ -43,7 +42,7 @@ export default () => {
<Gtk.Calendar />
</popover>
</menubutton>
</centerbox>
</CenterBox>
</window>
);
};

View file

@ -1,34 +0,0 @@
import { astalify, Gtk } from 'astal/gtk4';
import { property, register } from 'astal';
@register({ GTypeName: 'StyledBox' })
class StyledBoxClass extends Gtk.Box {
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);
}
}
export type StyledBox = StyledBoxClass;
export const StyledBox = astalify<
StyledBoxClass,
Gtk.Box.ConstructorProps & { css: string }
>(StyledBoxClass);

View file

@ -0,0 +1,272 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import Gdk from 'gi://Gdk?version=4.0';
import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk?version=4.0';
import Binding, { type Connectable, type Subscribable } from 'astal/binding';
import {
hook,
noImplicitDestroy,
setChildren,
construct,
} from 'astal/_astal';
export type BindableChild = Gtk.Widget | Binding<Gtk.Widget>;
export const filter = (children: any[]) => {
return children.flat(Infinity).map((ch) => ch instanceof Gtk.Widget ?
ch :
new Gtk.Label({ visible: true, label: String(ch) }));
};
const dummyBuilder = new Gtk.Builder();
const type = Symbol('child type');
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
}
const 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;
};
export default <
C extends new(...args: any[]) => Gtk.Widget,
>(cls: C, clsName = cls.name) => {
class Widget extends cls {
declare private _css: string | undefined;
declare private _provider: Gtk.CssProvider | undefined;
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 [type]: string;
get type(): string { return this[type]; }
set type(value: string) { this[type] = value; }
get children(): Gtk.Widget[] { return this.getChildren(this); }
set children(value: Gtk.Widget[]) { this.setChildren(this, value); }
declare private [noImplicitDestroy]: boolean;
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: 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(
dummyBuilder,
child,
type in widget ? widget[type] as string : null,
);
}
}
[setChildren](children: any[]) {
const w = this as unknown as Widget;
for (const child of (this.getChildren(w))) {
if (child instanceof Gtk.Widget) {
child.unparent();
if (!children.includes(child) && noImplicitDestroy in this) { child.run_dispose(); }
}
}
this.setChildren(w, children);
}
hook(
object: Connectable,
signal: string,
callback: (self: this, ...args: any[]) => void,
): this;
hook(
object: Subscribable,
callback: (self: this, ...args: any[]) => void,
): this;
hook(
object: Connectable | Subscribable,
signalOrCallback: string | ((self: this, ...args: any[]) => void),
callback?: (self: this, ...args: any[]) => void,
) {
hook(this, object, signalOrCallback, callback);
return this;
}
constructor(...params: any[]) {
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 as any, setupControllers(this, props));
}
}
GObject.registerClass({
GTypeName: `RealClass_${clsName}`,
Properties: {
'css': GObject.ParamSpec.string(
'css', '', '', GObject.ParamFlags.READWRITE, '',
),
'type': GObject.ParamSpec.string(
'type', '', '', GObject.ParamFlags.READWRITE, '',
),
'no-implicit-destroy': GObject.ParamSpec.boolean(
'no-implicit-destroy', '', '', GObject.ParamFlags.READWRITE, false,
),
},
}, Widget);
return Widget;
};

View file

@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { register } from 'astal';
import { Astal, type ConstructProps } from 'astal/gtk4';
import astalify, { filter } from './astalify';
export type BoxProps = ConstructProps<
Box,
Astal.Box.ConstructorProps & { css: string }
>;
@register({ GTypeName: 'Box' })
export class Box extends astalify(Astal.Box) {
constructor(props?: BoxProps) { super(props as any); }
getChildren(self: Box) {
return self.get_children();
}
setChildren(self: Box, children: any[]) {
return self.set_children(filter(children));
}
}

View file

@ -0,0 +1,21 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { register } from 'astal';
import { Gtk, type ConstructProps } from 'astal/gtk4';
import astalify from './astalify';
type ButtonSignals = Record<`on${string}`, unknown[]> & {
onClicked: []
};
export type ButtonProps = ConstructProps<
Button,
Gtk.Button.ConstructorProps & { css: string },
ButtonSignals
>;
@register({ GTypeName: 'Button' })
export class Button extends astalify(Gtk.Button) {
constructor(props?: ButtonProps) { super(props as any); }
}

View file

@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { register } from 'astal';
import { Gtk, type ConstructProps } from 'astal/gtk4';
import astalify, { filter } from './astalify';
export type CenterBoxProps = ConstructProps<
CenterBox,
Gtk.CenterBox.ConstructorProps & { css: string }
>;
@register({ GTypeName: 'CenterBox' })
export class CenterBox extends astalify(Gtk.CenterBox) {
constructor(props?: CenterBoxProps) { super(props as any); }
getChildren(box: CenterBox) {
return [box.startWidget, box.centerWidget, box.endWidget];
}
setChildren(box: CenterBox, children: any[]) {
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();
}
}

View file

@ -0,0 +1,3 @@
export * from './box';
export * from './button';
export * from './centerbox';