diff options
Diffstat (limited to 'lang/gjs/src/gtk4')
-rw-r--r-- | lang/gjs/src/gtk4/app.ts | 6 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/astalify.ts | 227 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/index.ts | 4 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/jsx-runtime.ts | 69 | ||||
-rw-r--r-- | lang/gjs/src/gtk4/widget.ts | 165 |
5 files changed, 466 insertions, 5 deletions
diff --git a/lang/gjs/src/gtk4/app.ts b/lang/gjs/src/gtk4/app.ts index 1c51772..7906993 100644 --- a/lang/gjs/src/gtk4/app.ts +++ b/lang/gjs/src/gtk4/app.ts @@ -4,4 +4,10 @@ import { mkApp } from "../_app" Gtk.init() +// users might want to use Adwaita in which case it has to be initialized +// it might be common pitfall to forget it because `App` is not `Adw.Application` +await import("gi://Adw?version=1") + .then(({ default: Adw }) => Adw.init()) + .catch(() => void 0) + export default mkApp(Astal.Application) diff --git a/lang/gjs/src/gtk4/astalify.ts b/lang/gjs/src/gtk4/astalify.ts index 6c8ea4d..644ac1a 100644 --- a/lang/gjs/src/gtk4/astalify.ts +++ b/lang/gjs/src/gtk4/astalify.ts @@ -1 +1,226 @@ -// TODO: +import { noImplicitDestroy, setChildren, type BindableProps, construct } from "../_astal.js" +import Gtk from "gi://Gtk?version=4.0" +import Gdk from "gi://Gdk?version=4.0" +import Binding from "../binding.js" + +export const type = Symbol("child type") +const dummyBulder = new Gtk.Builder + +function _getChildren(widget: Gtk.Widget): Array<Gtk.Widget> { + if ("get_child" in widget && typeof widget.get_child == "function") { + return widget.get_child() ? [widget.get_child()] : [] + } + + const children: Array<Gtk.Widget> = [] + let ch = widget.get_first_child() + while (ch !== null) { + children.push(ch) + ch = ch.get_next_sibling() + } + return children +} + +function _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( + dummyBulder, + child, + type in widget ? widget[type] as string : null, + ) + } +} + +type Config<T extends Gtk.Widget> = { + setChildren(widget: T, children: any[]): void + getChildren(widget: T): Array<Gtk.Widget> +} + +export default function astalify< + Widget extends Gtk.Widget, + Props extends Gtk.Widget.ConstructorProps = Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array<unknown>> = Record<`on${string}`, any[]>, +>(cls: { new(...args: any[]): Widget }, config: Partial<Config<Widget>> = {}) { + Object.assign(cls.prototype, { + [setChildren](children: any[]) { + const w = this as unknown as Widget + for (const child of (config.getChildren?.(w) || _getChildren(w))) { + if (child instanceof Gtk.Widget) { + child.unparent() + if (!children.includes(child) && noImplicitDestroy in this) + child.run_dispose() + } + } + + if (config.setChildren) { + config.setChildren(w, children) + } else { + _setChildren(w, children) + } + }, + }) + + return { + [cls.name]: ( + props: ConstructProps<Widget, Props, Signals> = {}, + ...children: any[] + ): Widget => { + const widget = new cls("cssName" in props ? { cssName: props.cssName } : {}) + + if ("cssName" in props) { + delete props.cssName + } + + if (props.noImplicitDestroy) { + Object.assign(widget, { [noImplicitDestroy]: true }) + delete props.noImplicitDestroy + } + + if (props.type) { + Object.assign(widget, { [type]: props.type }) + delete props.type + } + + if (children.length > 0) { + Object.assign(props, { children }) + } + + return construct(widget as any, setupControllers(widget, props as any)) + }, + }[cls.name] +} + +type SigHandler< + W extends InstanceType<typeof Gtk.Widget>, + Args extends Array<unknown>, +> = ((self: W, ...args: Args) => unknown) | string | string[] + +export { BindableProps } +export type BindableChild = Gtk.Widget | Binding<Gtk.Widget> + +export type ConstructProps< + Self extends InstanceType<typeof Gtk.Widget>, + Props extends Gtk.Widget.ConstructorProps, + Signals extends Record<`on${string}`, Array<unknown>> = Record<`on${string}`, any[]>, +> = Partial<{ + // @ts-expect-error can't assign to unknown, but it works as expected though + [S in keyof Signals]: SigHandler<Self, Signals[S]> +}> & Partial<{ + [Key in `on${string}`]: SigHandler<Self, any[]> +}> & Partial<BindableProps<Omit<Props, "cssName" | "css_name">>> & { + noImplicitDestroy?: true + type?: string + cssName?: string +} & EventController<Self> & { + onDestroy?: (self: Self) => unknown + setup?: (self: Self) => void +} + +type 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 +} + +function 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 +} diff --git a/lang/gjs/src/gtk4/index.ts b/lang/gjs/src/gtk4/index.ts index 3b1f737..2cc6862 100644 --- a/lang/gjs/src/gtk4/index.ts +++ b/lang/gjs/src/gtk4/index.ts @@ -1,9 +1,9 @@ import Astal from "gi://Astal?version=4.0" import Gtk from "gi://Gtk?version=4.0" import Gdk from "gi://Gdk?version=4.0" -// import astalify, { type ConstructProps } from "./astalify.js" +import astalify, { type ConstructProps } from "./astalify.js" export { Astal, Gtk, Gdk } export { default as App } from "./app.js" -// export { astalify, ConstructProps } +export { astalify, ConstructProps } // export * as Widget from "./widget.js" diff --git a/lang/gjs/src/gtk4/jsx-runtime.ts b/lang/gjs/src/gtk4/jsx-runtime.ts index 6c8ea4d..80a3e87 100644 --- a/lang/gjs/src/gtk4/jsx-runtime.ts +++ b/lang/gjs/src/gtk4/jsx-runtime.ts @@ -1 +1,68 @@ -// TODO: +import Gtk from "gi://Gtk?version=4.0" +import { type BindableChild } from "./astalify.js" +import { mergeBindings, jsx as _jsx } from "../_astal.js" +import * as Widget from "./widget.js" + +export function Fragment({ children = [], child }: { + child?: BindableChild + children?: Array<BindableChild> +}) { + if (child) children.push(child) + return mergeBindings(children) +} + +export function jsx( + ctor: keyof typeof ctors | typeof Gtk.Widget, + props: any, +) { + return _jsx(ctors, ctor as any, props) +} + +const ctors = { + box: Widget.Box, + button: Widget.Button, + centerbox: Widget.CenterBox, + // circularprogress: Widget.CircularProgress, + // drawingarea: Widget.DrawingArea, + entry: Widget.Entry, + image: Widget.Image, + label: Widget.Label, + levelbar: Widget.LevelBar, + overlay: Widget.Overlay, + revealer: Widget.Revealer, + slider: Widget.Slider, + stack: Widget.Stack, + switch: Widget.Switch, + window: Widget.Window, + menubutton: Widget.MenuButton, + popover: Widget.Popover, +} + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace JSX { + type Element = Gtk.Widget + type ElementClass = Gtk.Widget + interface IntrinsicElements { + box: Widget.BoxProps + button: Widget.ButtonProps + centerbox: Widget.CenterBoxProps + // circularprogress: Widget.CircularProgressProps + // drawingarea: Widget.DrawingAreaProps + entry: Widget.EntryProps + image: Widget.ImageProps + label: Widget.LabelProps + levelbar: Widget.LevelBarProps + overlay: Widget.OverlayProps + revealer: Widget.RevealerProps + slider: Widget.SliderProps + stack: Widget.StackProps + switch: Widget.SwitchProps + window: Widget.WindowProps + menubutton: Widget.MenuButtonProps + popover: Widget.PopoverProps + } + } +} + +export const jsxs = jsx diff --git a/lang/gjs/src/gtk4/widget.ts b/lang/gjs/src/gtk4/widget.ts index 6c8ea4d..10d4b9f 100644 --- a/lang/gjs/src/gtk4/widget.ts +++ b/lang/gjs/src/gtk4/widget.ts @@ -1 +1,164 @@ -// TODO: +import Astal from "gi://Astal?version=4.0" +import Gtk from "gi://Gtk?version=4.0" +import astalify, { type, type ConstructProps } from "./astalify.js" + +function filter(children: any[]) { + return children.flat(Infinity).map(ch => ch instanceof Gtk.Widget + ? ch + : new Gtk.Label({ visible: true, label: String(ch) })) +} + +// Box +Object.defineProperty(Astal.Box.prototype, "children", { + get() { return this.get_children() }, + set(v) { this.set_children(v) }, +}) + +export type BoxProps = ConstructProps<Astal.Box, Astal.Box.ConstructorProps> +export const Box = astalify<Astal.Box, Astal.Box.ConstructorProps>(Astal.Box, { + getChildren(self) { return self.get_children() }, + setChildren(self, children) { return self.set_children(filter(children)) }, +}) + +// Button +export type ButtonProps = ConstructProps<Gtk.Button, Gtk.Button.ConstructorProps, { + onClicked: [] +}> +export const Button = astalify<Gtk.Button, Gtk.Button.ConstructorProps>(Gtk.Button) + +// CenterBox +export type CenterBoxProps = ConstructProps<Gtk.CenterBox, Gtk.CenterBox.ConstructorProps> +export const CenterBox = astalify<Gtk.CenterBox, Gtk.CenterBox.ConstructorProps>(Gtk.CenterBox, { + getChildren(box) { + return [box.startWidget, box.centerWidget, box.endWidget] + }, + setChildren(box, children) { + 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 + }, +}) + +// TODO: CircularProgress +// TODO: DrawingArea + +// Entry +type EntrySignals = { + onActivate: [] + onNotifyText: [] +} + +export type EntryProps = ConstructProps<Gtk.Entry, Gtk.Entry.ConstructorProps, EntrySignals> +export const Entry = astalify<Gtk.Entry, Gtk.Entry.ConstructorProps, EntrySignals>(Gtk.Entry, { + getChildren() { return [] }, +}) + +// Image +export type ImageProps = ConstructProps<Gtk.Image, Gtk.Image.ConstructorProps> +export const Image = astalify<Gtk.Image, Gtk.Image.ConstructorProps>(Gtk.Image, { + getChildren() { return [] }, +}) + +// Label +export type LabelProps = ConstructProps<Gtk.Label, Gtk.Label.ConstructorProps> +export const Label = astalify<Gtk.Label, Gtk.Label.ConstructorProps>(Gtk.Label, { + getChildren() { return [] }, + setChildren(self, children) { self.label = String(children) }, +}) + +// LevelBar +export type LevelBarProps = ConstructProps<Gtk.LevelBar, Gtk.LevelBar.ConstructorProps> +export const LevelBar = astalify<Gtk.LevelBar, Gtk.LevelBar.ConstructorProps>(Gtk.LevelBar, { + getChildren() { return [] }, +}) + +// TODO: ListBox + +// Overlay +export type OverlayProps = ConstructProps<Gtk.Overlay, Gtk.Overlay.ConstructorProps> +export const Overlay = astalify<Gtk.Overlay, Gtk.Overlay.ConstructorProps>(Gtk.Overlay, { + getChildren(self) { + const children: Array<Gtk.Widget> = [] + let ch = self.get_first_child() + while (ch !== null) { + children.push(ch) + ch = ch.get_next_sibling() + } + + return children.filter(ch => ch !== self.child) + }, + setChildren(self, children) { + for (const child of filter(children)) { + const types = type in child + ? (child[type] as string).split(/\s+/) + : [] + + if (types.includes("overlay")) { + self.add_overlay(child) + } else { + self.set_child(child) + } + + self.set_measure_overlay(child, types.includes("measure")) + self.set_clip_overlay(child, types.includes("clip")) + } + }, +}) + +// Revealer +export type RevealerProps = ConstructProps<Gtk.Revealer, Gtk.Revealer.ConstructorProps> +export const Revealer = astalify<Gtk.Revealer, Gtk.Revealer.ConstructorProps>(Gtk.Revealer) + +// Slider +type SliderSignals = { + onChangeValue: [] +} + +export type SliderProps = ConstructProps<Astal.Slider, Astal.Slider.ConstructorProps, SliderSignals> +export const Slider = astalify<Astal.Slider, Astal.Slider.ConstructorProps, SliderSignals>(Astal.Slider, { + getChildren() { return [] }, +}) + +// Stack +export type StackProps = ConstructProps<Gtk.Stack, Gtk.Stack.ConstructorProps> +export const Stack = astalify<Gtk.Stack, Gtk.Stack.ConstructorProps>(Gtk.Stack, { + setChildren(self, children) { + for (const child of filter(children)) { + if (child.name != "" && child.name != null) { + self.add_named(child, child.name) + } else { + self.add_child(child) + } + } + }, +}) + +// Switch +export type SwitchProps = ConstructProps<Gtk.Switch, Gtk.Switch.ConstructorProps> +export const Switch = astalify<Gtk.Switch, Gtk.Switch.ConstructorProps>(Gtk.Switch, { + getChildren() { return [] }, +}) + +// Window +export type WindowProps = ConstructProps<Astal.Window, Astal.Window.ConstructorProps> +export const Window = astalify<Astal.Window, Astal.Window.ConstructorProps>(Astal.Window) + +// MenuButton +export type MenuButtonProps = ConstructProps<Gtk.MenuButton, Gtk.MenuButton.ConstructorProps> +export const MenuButton = astalify<Gtk.MenuButton, Gtk.MenuButton.ConstructorProps>(Gtk.MenuButton, { + getChildren(self) { return [self.popover, self.child] }, + setChildren(self, children) { + for (const child of filter(children)) { + if (child instanceof Gtk.Popover) { + self.set_popover(child) + } else { + self.set_child(child) + } + } + }, +}) + +// Popoper +export type PopoverProps = ConstructProps<Gtk.Popover, Gtk.Popover.ConstructorProps> +export const Popover = astalify<Gtk.Popover, Gtk.Popover.ConstructorProps>(Gtk.Popover) |