summaryrefslogtreecommitdiff
path: root/lang/gjs/src/gtk4
diff options
context:
space:
mode:
Diffstat (limited to 'lang/gjs/src/gtk4')
-rw-r--r--lang/gjs/src/gtk4/app.ts6
-rw-r--r--lang/gjs/src/gtk4/astalify.ts227
-rw-r--r--lang/gjs/src/gtk4/index.ts4
-rw-r--r--lang/gjs/src/gtk4/jsx-runtime.ts69
-rw-r--r--lang/gjs/src/gtk4/widget.ts165
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)