summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-12-25 02:38:27 +0100
committerGitHub <[email protected]>2024-12-25 02:38:27 +0100
commit37f0d24178a1516eb45eb639640e07c5dc3b8e81 (patch)
tree28ff8d1030be1919c00152e99b4ab9c229b0f01b
parent553b2186db47fb34602d4e949c1e40a018238d7a (diff)
parent0f2fefd2053203e1bfe4d66eb4e37dea07369890 (diff)
Merge pull request #196 from Aylur/feat/jsx-gtk4
Add jsx support for gtk4
-rw-r--r--docs/guide/getting-started/introduction.md12
-rw-r--r--docs/guide/typescript/cli-app.md6
-rw-r--r--docs/guide/typescript/first-widgets.md24
-rw-r--r--docs/guide/typescript/theming.md15
-rw-r--r--docs/guide/typescript/widget.md381
-rw-r--r--lang/gjs/eslint.config.mjs1
-rw-r--r--lang/gjs/meson.build6
-rw-r--r--lang/gjs/package.json8
-rw-r--r--lang/gjs/src/_app.ts9
-rw-r--r--lang/gjs/src/_astal.ts188
-rw-r--r--lang/gjs/src/binding.ts8
-rw-r--r--lang/gjs/src/file.ts6
-rw-r--r--lang/gjs/src/gobject.ts10
-rw-r--r--lang/gjs/src/gtk3/astalify.ts232
-rw-r--r--lang/gjs/src/gtk3/index.ts1
-rw-r--r--lang/gjs/src/gtk3/jsx-runtime.ts31
-rw-r--r--lang/gjs/src/gtk3/widget.ts20
-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.ts7
-rw-r--r--lang/gjs/src/gtk4/jsx-runtime.ts69
-rw-r--r--lang/gjs/src/gtk4/widget.ts167
-rw-r--r--lang/gjs/src/index.ts4
-rw-r--r--lang/gjs/src/package.json22
-rw-r--r--lang/gjs/src/process.ts9
-rw-r--r--lang/gjs/src/variable.ts13
-rw-r--r--lib/astal/gtk3/src/widget/circularprogress.vala2
-rw-r--r--lib/astal/gtk4/src/application.vala11
-rw-r--r--lib/astal/gtk4/src/meson.build2
-rw-r--r--lib/astal/gtk4/src/widget/box.vala50
-rw-r--r--lib/astal/gtk4/src/widget/slider.vala65
-rw-r--r--nix/devshell.nix19
32 files changed, 1329 insertions, 302 deletions
diff --git a/docs/guide/getting-started/introduction.md b/docs/guide/getting-started/introduction.md
index 782c069..43a7bd8 100644
--- a/docs/guide/getting-started/introduction.md
+++ b/docs/guide/getting-started/introduction.md
@@ -2,13 +2,15 @@
## What is Astal?
-Astal (_meaning "desk"_) is a suite of libraries in Vala and C.
+Astal (_meaning "desk"_) is a suite of libraries written in Vala and C.
The core library [astal3](https://aylur.github.io/libastal/astal3) and
-[astal4](https://aylur.github.io/libastal/astal4) (not yet available)
-has some Gtk widgets that come packaged,
+[astal4](https://aylur.github.io/libastal/astal4)
+have some Gtk widgets that come packaged,
the most important one being the [Window](https://aylur.github.io/libastal/astal3/class.Window.html) which is the main toplevel component using [gtk-layer-shell](https://github.com/wmww/gtk-layer-shell).
This is what allows us to use Gtk as shell components on Wayland.
-The other part of the core library [astal-io](https://aylur.github.io/libastal/astal-io)
+The other component is [Application](https://aylur.github.io/libastal/astal3/class.Application.html)
+which provides a way to send messages from the cli to running Astal instances.
+The other part of the core library is [astal-io](https://aylur.github.io/libastal/astal-io)
which contains some utility GLib shortcut for running external processes,
reading, writing and monitoring files, timeout and interval functions.
@@ -23,4 +25,4 @@ or an applauncher, but gave up because writing a workspace widget,
implementing the notification daemon or handling a search filter was too much of a hassle?
Astal libraries have you [covered](../libraries/references#astal-libraries), you don't have to worry about these,
-you just define the layout, style it with CSS and that's it.
+you just define the layout, style with CSS hook up the state from libraries you want and that's it.
diff --git a/docs/guide/typescript/cli-app.md b/docs/guide/typescript/cli-app.md
index 9b299aa..41b1d7c 100644
--- a/docs/guide/typescript/cli-app.md
+++ b/docs/guide/typescript/cli-app.md
@@ -26,7 +26,7 @@ App.start({
## Instance identifier
-You can run multiple instance by defining a unique instance name.
+You can run multiple instances by defining a unique instance name.
```ts
App.start({
@@ -44,7 +44,7 @@ you can do so by sending a message.
App.start({
requestHandler(request: string, res: (response: any) => void) {
if (request == "say hi") {
- res("hi cli")
+ return res("hi cli")
}
res("unknown command")
},
@@ -140,7 +140,7 @@ App.start({
// every subsequent calls
client(message: (msg: string) => string, ...args: Array<string>) {
const res = message("you can message the main instance")
- console.log(res)
+ print(res)
},
// this runs in the main instance
diff --git a/docs/guide/typescript/first-widgets.md b/docs/guide/typescript/first-widgets.md
index 77b2f61..9b8bf32 100644
--- a/docs/guide/typescript/first-widgets.md
+++ b/docs/guide/typescript/first-widgets.md
@@ -71,7 +71,7 @@ function MyButton(): JSX.Element {
}
```
-```ts [MyButton.ts]
+```ts [MyButton.ts (gtk3)]
import { Widget } from "astal/gtk3"
function MyButton(): Widget.Button {
@@ -82,6 +82,17 @@ function MyButton(): Widget.Button {
}
```
+```ts [MyButton.ts (gtk4)]
+import { Widget } from "astal/gtk4"
+
+function MyButton(): Widget.Button {
+ return Widget.Button(
+ { onClicked: "echo hello" },
+ Widget.Label({ label: "Click me!" }),
+ )
+}
+```
+
:::
:::info
@@ -218,6 +229,14 @@ Their types are not generated, but written by hand, which means not all of them
Refer to the Gtk and Astal docs to have a full list of them.
:::
+:::info
+Attributes prefixed with `onNotify` will connect to a `notify::` signal of the widget.
+
+```tsx
+<switch onNotifyActive={self => print("switched to", self.active)}>
+```
+:::
+
## How properties are passed
Using JSX, a custom widget will always have a single object as its parameter.
@@ -413,8 +432,7 @@ function Parent(props: {
:::tip
If you have a widget where you pass widgets in various ways, you can
-wrap `child` in `children` in a [`Subscribable`](./faq#custom-widgets-with-bindable-properties) and handle all cases
-as if they were bindings.
+wrap `child` and `children` props in a [`Subscribable`](./faq#custom-widgets-with-bindable-properties) and handle all cases as if they were bindings.
:::
:::info
diff --git a/docs/guide/typescript/theming.md b/docs/guide/typescript/theming.md
index 5944c4e..3cafa7d 100644
--- a/docs/guide/typescript/theming.md
+++ b/docs/guide/typescript/theming.md
@@ -1,24 +1,25 @@
# Theming
-Since the widget toolkit is **GTK3** theming is done with **CSS**.
+Since the widget toolkit is **GTK** theming is done with **CSS**.
- [CSS tutorial](https://www.w3schools.com/css/)
-- [GTK CSS Overview wiki](https://docs.gtk.org/gtk3/css-overview.html)
-- [GTK CSS Properties Overview wiki](https://docs.gtk.org/gtk3/css-properties.html)
+- [GTK3 CSS Overview wiki](https://docs.gtk.org/gtk3/css-overview.html)
+- [GTK3 CSS Properties Overview wiki](https://docs.gtk.org/gtk3/css-properties.html)
+- [GTK4 CSS Overview wiki](https://docs.gtk.org/gtk4/css-overview.html)
+- [GTK4 CSS Properties Overview wiki](https://docs.gtk.org/gtk4/css-properties.html)
:::warning GTK is not the web
While most features are implemented in GTK,
you can't assume anything that works on the web will work with GTK.
-Refer to the [GTK docs](https://docs.gtk.org/gtk3/css-overview.html)
-to see what is available.
+Refer to the GTK docs to see what is available.
:::
-So far every widget you made used your default GTK3 theme.
+So far every widget you made used your default GTK theme.
To make them more custom, you can apply stylesheets to them.
## From file at startup
-You can pass a path to a file or css as a string in `App.start`
+You can pass a path to a file or CSS as a string in `App.start`
:::code-group
diff --git a/docs/guide/typescript/widget.md b/docs/guide/typescript/widget.md
index 7ed69e3..7e57c01 100644
--- a/docs/guide/typescript/widget.md
+++ b/docs/guide/typescript/widget.md
@@ -6,15 +6,21 @@
These are properties that Astal additionally adds to Gtk.Widgets
-- className: `string` - List of class CSS selectors separated by white space.
-- css: `string` - Inline CSS. e.g `label { color: white; }`. If no selector is specified `*` will be assumed. e.g `color: white;` will be inferred as `* { color: white; }`.
-- cursor: `string` - Cursor style when hovering over widgets that have hover states, e.g it won't work on labels. [list of valid values](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html).
-- clickThrough: `boolean` - Lets click events through.
+- `className`: `string` - List of class CSS selectors separated by white space.
+- `css`: `string` - Inline CSS. e.g `label { color: white; }`. If no selector is specified `*` will be assumed. e.g `color: white;` will be inferred as `* { color: white; }`.
+- `cursor`: `string` - Cursor style when hovering over widgets that have hover states, e.g it won't work on labels. [list of valid values](https://docs.gtk.org/gdk3/ctor.Cursor.new_from_name.html).
+- `clickThrough`: `boolean` - Lets click events through.
To have a full list of available properties, reference the documentation of the widget.
- [Astal3 widgets](https://aylur.github.io/libastal/astal3/index.html#classes)
-- [Gtk widgets](https://docs.gtk.org/gtk3/#classes)
+- [Gtk3 widgets](https://docs.gtk.org/gtk3/#classes)
+
+Most common ones you will use frequently are
+ - [halign](https://docs.gtk.org/gtk3/property.Widget.halign.html)
+ - [valign](https://docs.gtk.org/gtk3/property.Widget.valign.html)
+ - [hexpand](https://docs.gtk.org/gtk3/property.Widget.hexpand.html)
+ - [vexpand](https://docs.gtk.org/gtk3/property.Widget.vexpand.html)
### Additional widget methods
@@ -27,7 +33,7 @@ without `setup`
```tsx
function MyWidget() {
- const button = Widget.Button()
+ const button = new Widget.Button()
// setup button
return button
}
@@ -94,14 +100,14 @@ function MyWidget() {
### How to use non builtin Gtk widgets
-Using the `Widget.astalify` mixin you can subclass widgets
+Using the `astalify` mixin you can subclass widgets
to behave like builtin widgets.
The `astalify` mixin will apply the following:
- set `visible` to true by default (Gtk3 widgets are invisible by default)
- make gobject properties accept and consume `Binding` objects
- add properties and methods listed above
-- sets up signal handlers that are passed as props prefixed with `on`
+- set up signal handlers that are passed as props prefixed with `on`
```tsx
import GObject from "gi://GObject"
@@ -135,7 +141,7 @@ function MyWidget() {
alpha: 0.5,
})}
onColorSet={(self) => {
- console.log(self.rgba)
+ print(self.rgba)
}}
/>
}
@@ -144,7 +150,7 @@ function MyWidget() {
:::info
Signal properties have to be annotated manually for TypeScript.
You can reference [Gtk3](https://gjs-docs.gnome.org/gtk30~3.0/)
-and [Astal](https://aylur.github.io/libastal/index.html#classes) for available signals.
+and [Astal3](https://aylur.github.io/libastal/astal3/#classes) for available signals.
:::
### TypeScript
@@ -189,28 +195,377 @@ export default function ToggleButton(btnprops: ToggleButtonProps) {
### Builtin Widgets
-You can check the [source code](https://github.com/aylur/astal/blob/main/lang/gjs/src/gtk3/index.ts) to have a full list of builtin widgets.
-
These widgets are available by default in JSX.
- box: [Astal.Box](https://aylur.github.io/libastal/astal3/class.Box.html)
+ ```tsx
+ <box>Horizontal Box</box>
+ ```
+ ```tsx
+ <box orientation={1}>Vertical Box</box>
+ ```
- button: [Astal.Button](https://aylur.github.io/libastal/astal3/class.Button.html)
+ ```tsx
+ <button onClicked={self => print(self, "was clicked")}>
+ Click Me
+ </button>
+ ```
- centerbox: [Astal.CenterBox](https://aylur.github.io/libastal/astal3/class.CenterBox.html)
+ ```tsx
+ <centerbox orientation={1}>
+ <label vexpand valign={Gtk.Align.START} label="Start Widget" />
+ <label label="Center Widget" />
+ <label vexpand valign={Gtk.Align.END} label="End Widget" />
+ </box>
+ ```
- circularprogress: [Astal.CircularProgress](https://aylur.github.io/libastal/astal3/class.CircularProgress.html)
+ ```tsx
+ <circularprogress value={.5} startAt={0.75} endAt={0.75}>
+ <icon />
+ </circularprogress>
+ ```
+ ```css
+ circularprogress {
+ color: green;
+ background-color: black;
+ font-size: 6px;
+ margin: 2px;
+ min-width: 32px;
+ }
+ ```
+
- drawingarea: [Gtk.DrawingArea](https://docs.gtk.org/gtk3/class.DrawingArea.html)
+ ```tsx
+ <drawingarea onDraw={drawingFunction} />
+ ```
+
- entry: [Gtk.Entry](https://docs.gtk.org/gtk3/class.Entry.html)
+ ```tsx
+ <window keymode={Astal.Keymode.ON_DEMAND}>
+ <entry
+ onChanged={self => print("text changed", self.text)}
+ onActivate={self => print("enter", self.text)}
+ />
+ </window>
+ ```
+
- eventbox: [Astal.EventBox](https://aylur.github.io/libastal/astal3/class.EventBox.html)
+ ```tsx
+ <eventbox
+ onClick={(_, event) => {
+ print(event.modifier, event.button)
+ }}
+ />
+ ```
+
- icon: [Astal.Icon](https://aylur.github.io/libastal/astal3/class.Icon.html)
+ ```tsx
+ <icon icon={GLib.get_os_info("LOGO") || "missing-symbolic"} />
+ ```
+ ```css
+ icon {
+ font-size: 16px;
+ }
+ ```
+
- label: [Astal.Label](https://aylur.github.io/libastal/astal3/class.Label.html)
+ ```tsx
+ <label label="hello" maxWidthChars={16} wrap />
+ ```
+
- levelbar: [Astal.LevelBar](https://aylur.github.io/libastal/astal3/class.LevelBar.html)
+ ```tsx
+ <levelbar value={0.5} widthRequest={200} />
+ ```
+
- overlay: [Astal.Overlay](https://aylur.github.io/libastal/astal3/class.Overlay.html)
+ ```tsx
+ <overlay>
+ <box heightRequest={40} widthRequest={40}>Child</box>
+ <box className="overlay" valign={Gtk.Align.START} halign={Gtk.Align.END}>1</box>
+ </overlay>
+ ```
+
- revealer: [Gtk.Revealer](https://docs.gtk.org/gtk3/class.Revealer.html)
+ ```tsx
+ <revealer
+ setup={self => timeout(500, () => self.revealChild = true)}
+ transitionType={Gtk.RevealerTransitionType.SLIDE_UP}>
+ <label label="Child" />
+ </revealer>
+ ```
+
- scrollable: [Astal.Scrollable](https://aylur.github.io/libastal/astal3/class.Scrollable.html)
+ ```tsx
+ <scrollable heightRequest={100}>
+ <box orientation={1}>
+ {Array.from({ length: 10 }, (_, i) => (
+ <button>{i}</button>
+ ))}
+ </box>
+ </scrollable>
+ ```
+
- slider: [Astal.Slider](https://aylur.github.io/libastal/astal3/class.Slider.html)
+ ```tsx
+ <slider widthRequest={100} onDragged={self => print("new value", self.value)} />
+ ```
+
- stack: [Astal.Stack](https://aylur.github.io/libastal/astal3/class.Stack.html)
+ ```tsx
+ <stack visibleChildName="child2">
+ <label name="child1" label="child1" />
+ <label name="child2" label="child2" />
+ </stack>
+ ```
+
- switch: [Gtk.Switch](https://docs.gtk.org/gtk3/class.Switch.html)
+ ```tsx
+ <switch onNotifyActive={self => print(self.active)} />
+ ```
+
- window: [Astal.Window](https://aylur.github.io/libastal/astal3/class.Window.html)
+ ```tsx
+ <window
+ className="Bar"
+ name="bar"
+ namespace="bar"
+ application={App}
+ monitor={0}
+ anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT}
+ exclusivity={Astal.Exclusivity.EXCLUSIVE}
+ keymode={Astal.Keymode.ON_DEMAND}
+ >
+ <centerbox />
+ </window>
+ ```
## Gtk4
-🚧 Work in Progress 🚧
+The Gtk4 js library does not add any additional properties to the widgets,
+but it still has some additional properties that the constructors handle.
+
+- `type`: `string` an arbitrary string that the [Buildable](https://docs.gtk.org/gtk4/iface.Buildable.html) interface uses.
+- event handlers for [EventControllers](https://docs.gtk.org/gtk4/class.EventController.html)
+ ```ts
+ 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
+ }
+ ```
+
+- `setup`: `(self): void` setup function that runs after constructor
+ ```tsx
+ // without `setup`
+ function MyWidget() {
+ const button = Widget.Button()
+ // setup button
+ return button
+ }
+
+ // using `setup`
+ function MyWidget() {
+ function setup(button: Widget.Button) {
+ // setup button
+ }
+
+ return <buttons setup={setup} />
+ }
+ ```
+
+There is also a `hook` utility
+
+```tsx
+// without `hook`
+function MyWidget() {
+ const id = gobject.connect("signal", callback)
+ const unsub = variable.subscribe(callback)
+
+ return <box
+ onDestroy={() => {
+ gobject.disconnect(id)
+ unsub()
+ }}
+ />
+}
+
+// with `hook`
+import { hook } from "astal/gtk4"
+
+function MyWidget() {
+ return <box
+ setup={(self) => {
+ self.hook(gobject, "signal", callback)
+ self.hook(variable, callback)
+ }}
+ />
+}
+```
+
+### How to use non builtin Gtk widgets
+
+Using the `astalify` function you can create wrappers around widget constructors
+to make them behave like builtin widgets.
+The `astalify` function will do the followings:
+
+- make `gobject` properties accept and consume `Binding` objects
+- handle properties listed above
+- set up signal handlers that are passed as props prefixed with `on`
+
+```tsx
+import GObject from "gi://GObject"
+import { Gtk, astalify, type ConstructProps } from "astal/gtk4"
+
+type CalendarProps = ConstructProps<Gtk.Calendar, Gtk.Calendar.ConstructorProps>
+const Calendar = astalify<Gtk.Calendar, Gtk.Calendar.ConstructorProps>(Gtk.Calendar, {
+ // if it is a container widget, define children setter and getter here
+ getChildren(self) { return [] },
+ setChildren(self, children) {},
+})
+
+function MyWidget() {
+ function setup(button: Gtk.Calendar) {
+
+ }
+
+ return <Calendar
+ setup={setup}
+ onDaySelected={(self) => {
+ print(self.day)
+ }}
+ />
+}
+```
+
+### Builtin Widgets
+
+These widgets are available by default in JSX.
+
+- box: [Astal.Box](https://aylur.github.io/libastal/astal4/class.Box.html)
+ ```tsx
+ <box>Horizontal Box</box>
+ ```
+ ```tsx
+ <box orientation={1}>Vertical Box</box>
+ ```
+- button: [Gtk.Button](https://docs.gtk.org/gtk4/class.Button.html)
+ ```tsx
+ <button onClicked={self => print(self, "was clicked")}>
+ Click Me
+ </button>
+ ```
+- centerbox: [Gtk.CenterBox](https://docs.gtk.org/gtk4/class.CenterBox.html)
+ ```tsx
+ <centerbox orientation={1}>
+ <label label="Start Widget" />
+ <label label="Center Widget" />
+ <label label="End Widget" />
+ </box>
+ ```
+- entry: [Gtk.Entry](https://docs.gtk.org/gtk4/class.Entry.html)
+ ```tsx
+ <window keymode={Astal.Keymode.ON_DEMAND}>
+ <entry
+ onNotifyText={self => print("text changed", self.text)}
+ onActivate={self => print("enter", self.text)}
+ />
+ </window>
+ ```
+
+- image: [Gtk.Image](https://docs.gtk.org/gtk4/class.Image.html)
+ ```tsx
+ <image iconName={GLib.get_os_info("LOGO") || "missing-symbolic"} />
+ ```
+ ```css
+ image {
+ -gtk-icon-size: 16px;
+ }
+ ```
+
+- label: [Gtk.Label](https://docs.gtk.org/gtk4/class.Label.html)
+ ```tsx
+ <label label="hello" maxWidthChars={16} wrap />
+ ```
+
+- levelbar: [Gtk.LevelBar](https://docs.gtk.org/gtk4/class.LevelBar.html)
+ ```tsx
+ <levelbar value={0.5} widthRequest={200} />
+ ```
+
+- overlay: [Gtk.Overlay](https://docs.gtk.org/gtk4/class.Overlay.html)
+ ```tsx
+ <overlay>
+ <box heightRequest={40} widthRequest={40}>Child</box>
+ <box type="overlay measure" >1</box>
+ <box type="overlay clip" >2</box>
+ <box type="overlay clip measure" >3</box>
+ </overlay>
+ ```
+
+- revealer: [Gtk.Revealer](https://docs.gtk.org/gtk4/class.Revealer.html)
+ ```tsx
+ <revealer
+ setup={self => timeout(500, () => self.revealChild = true)}
+ transitionType={Gtk.RevealerTransitionType.SLIDE_UP}>
+ <label label="Child" />
+ </revealer>
+ ```
+
+- slider: [Astal.Slider](https://aylur.github.io/libastal/astal4/class.Slider.html)
+ ```tsx
+ <slider widthRequest={100} onNotifyValue={self => print("new value", self.value)} />
+ ```
+
+- stack: [Gtk.Stack](https://docs.gtk.org/gtk4/class.Stack.html)
+ ```tsx
+ <stack visibleChildName="child2">
+ <label name="child1" label="child1" />
+ <label name="child2" label="child2" />
+ </stack>
+ ```
+
+- switch: [Gtk.Switch](https://docs.gtk.org/gtk4/class.Switch.html)
+ ```tsx
+ <switch onNotifyActive={self => print(self.active)} />
+ ```
+
+- menubutton: [Gtk.MenuButton](https://docs.gtk.org/gtk4/class.MenuButton.html) and popover: [Gtk.Popover](https://docs.gtk.org/gtk4/class.Popover.html)
+ ```tsx
+ <menubutton>
+ <label label="Button Content" />
+ <popover>
+ <label label="Popover Content" />
+ </popover>
+ </menubutton>
+ ```
+
+- window: [Astal.Window](https://aylur.github.io/libastal/astal4/class.Window.html)
+ ```tsx
+ <window
+ className="Bar"
+ name="bar"
+ namespace="bar"
+ application={App}
+ monitor={0}
+ anchor={Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT}
+ exclusivity={Astal.Exclusivity.EXCLUSIVE}
+ keymode={Astal.Keymode.ON_DEMAND}
+ >
+ <centerbox />
+ </window>
+ ```
diff --git a/lang/gjs/eslint.config.mjs b/lang/gjs/eslint.config.mjs
index 05e49ee..5e32355 100644
--- a/lang/gjs/eslint.config.mjs
+++ b/lang/gjs/eslint.config.mjs
@@ -15,5 +15,6 @@ export default tseslint.config({
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@stylistic/new-parens": "off",
+ "@stylistic/brace-style": ["error", "1tbs", { allowSingleLine: true }],
},
})
diff --git a/lang/gjs/meson.build b/lang/gjs/meson.build
index 51496dc..402bc55 100644
--- a/lang/gjs/meson.build
+++ b/lang/gjs/meson.build
@@ -7,15 +7,17 @@ dependency('astal-3.0')
install_data(
[
+ 'src/_app.ts',
+ 'src/_astal.ts',
'src/binding.ts',
'src/file.ts',
'src/gobject.ts',
'src/index.ts',
+ 'src/overrides.ts',
'src/process.ts',
'src/time.ts',
'src/variable.ts',
- 'src/overrides.ts',
- 'src/_app.ts',
+ 'src/package.json',
],
install_dir: dest,
)
diff --git a/lang/gjs/package.json b/lang/gjs/package.json
index 43a7702..e3b7761 100644
--- a/lang/gjs/package.json
+++ b/lang/gjs/package.json
@@ -18,6 +18,12 @@
".": "./index.ts",
"./gtk3": "./src/gtk3/index.ts",
"./gtk4": "./src/gtk4/index.ts",
+ "./gtk3/app": "./src/gtk3/app.ts",
+ "./gtk4/app": "./src/gtk4/app.ts",
+ "./gtk3/widget": "./src/gtk3/widget.ts",
+ "./gtk4/widget": "./src/gtk4/widget.ts",
+ "./gtk3/jsx-runtime": "./src/gtk3/jsx-runtime.ts",
+ "./gtk4/jsx-runtime": "./src/gtk4/jsx-runtime.ts",
"./binding": "./src/binding.ts",
"./file": "./src/file.ts",
"./gobject": "./src/gobject.ts",
@@ -42,6 +48,6 @@
},
"scripts": {
"lint": "eslint . --fix",
- "types": "ts-for-gir generate -o @girs"
+ "types": "ts-for-gir generate -o @girs --ignoreVersionConflicts"
}
}
diff --git a/lang/gjs/src/_app.ts b/lang/gjs/src/_app.ts
index 3dadd04..46497c1 100644
--- a/lang/gjs/src/_app.ts
+++ b/lang/gjs/src/_app.ts
@@ -53,8 +53,7 @@ export function mkApp(App: App3 | App4) {
${body.includes(";") ? body : `return ${body};`}
})`)
fn()().then(res).catch(rej)
- }
- catch (error) {
+ } catch (error) {
rej(error)
}
})
@@ -69,8 +68,7 @@ export function mkApp(App: App3 | App4) {
IO.write_sock_finish(res),
)
})
- }
- else {
+ } else {
super.vfunc_request(msg, conn)
}
}
@@ -102,8 +100,7 @@ export function mkApp(App: App3 | App4) {
try {
app.acquire_socket()
- }
- catch (error) {
+ } catch (error) {
return client(msg => IO.send_message(app.instanceName, msg)!, ...programArgs)
}
diff --git a/lang/gjs/src/_astal.ts b/lang/gjs/src/_astal.ts
new file mode 100644
index 0000000..6f3285b
--- /dev/null
+++ b/lang/gjs/src/_astal.ts
@@ -0,0 +1,188 @@
+import Variable from "./variable.js"
+import { execAsync } from "./process.js"
+import Binding, { Connectable, kebabify, snakeify, Subscribable } from "./binding.js"
+
+export const noImplicitDestroy = Symbol("no no implicit destroy")
+export const setChildren = Symbol("children setter method")
+
+export function mergeBindings(array: any[]) {
+ function getValues(...args: any[]) {
+ 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
+
+ if (bindings.length === 1)
+ return bindings[0].as(getValues)
+
+ return Variable.derive(bindings, getValues)()
+}
+
+export function setProp(obj: any, prop: string, value: any) {
+ 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 type BindableProps<T> = {
+ [K in keyof T]: Binding<T[K]> | T[K];
+}
+
+export function hook<Widget extends Connectable>(
+ widget: Widget,
+ object: Connectable | Subscribable,
+ signalOrCallback: string | ((self: Widget, ...args: any[]) => void),
+ callback?: (self: Widget, ...args: any[]) => void,
+) {
+ if (typeof object.connect === "function" && callback) {
+ const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => {
+ callback(widget, ...args)
+ })
+ widget.connect("destroy", () => {
+ (object.disconnect as Connectable["disconnect"])(id)
+ })
+ } else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") {
+ const unsub = object.subscribe((...args: unknown[]) => {
+ signalOrCallback(widget, ...args)
+ })
+ widget.connect("destroy", unsub)
+ }
+}
+
+export function construct<Widget extends Connectable & { [setChildren]: (children: any[]) => void }>(widget: Widget, config: any) {
+ const { setup, child, children = [], ...props } = config
+
+ if (child) {
+ children.unshift(child)
+ }
+
+ // remove undefined values
+ for (const [key, value] of Object.entries(props)) {
+ if (value === undefined) {
+ delete props[key]
+ }
+ }
+
+ // collect bindings
+ const bindings: Array<[string, Binding<any>]> = Object
+ .keys(props)
+ .reduce((acc: any, prop) => {
+ if (props[prop] instanceof Binding) {
+ const binding = props[prop]
+ delete props[prop]
+ return [...acc, [prop, binding]]
+ }
+ return acc
+ }, [])
+
+ // collect signal handlers
+ const onHandlers: Array<[string, string | (() => unknown)]> = Object
+ .keys(props)
+ .reduce((acc: any, key) => {
+ if (key.startsWith("on")) {
+ const sig = kebabify(key).split("-").slice(1).join("-")
+ const handler = props[key]
+ delete props[key]
+ return [...acc, [sig, handler]]
+ }
+ return acc
+ }, [])
+
+ // 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)
+ }))
+ } 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.subscribe((v: any) => {
+ widget[setChildren](v)
+ }))
+ }
+ widget.connect("destroy", binding.subscribe((v: any) => {
+ setProp(widget, prop, v)
+ }))
+ setProp(widget, prop, binding.get())
+ }
+
+ // filter undefined values
+ for (const [key, value] of Object.entries(props)) {
+ if (value === undefined) {
+ delete props[key]
+ }
+ }
+
+ Object.assign(widget, props)
+ setup?.(widget)
+ return widget
+}
+
+function isArrowFunction(func: any): func is (args: any) => any {
+ return !Object.hasOwn(func, "prototype")
+}
+
+export function jsx(
+ ctors: Record<string, { new(props: any): any } | ((props: any) => any)>,
+ ctor: string | ((props: any) => any) | { new(props: any): any },
+ { children, ...props }: any,
+) {
+ children ??= []
+
+ if (!Array.isArray(children))
+ children = [children]
+
+ children = children.filter(Boolean)
+
+ if (children.length === 1)
+ props.child = children[0]
+ else if (children.length > 1)
+ props.children = children
+
+ if (typeof ctor === "string") {
+ if (isArrowFunction(ctors[ctor]))
+ return ctors[ctor](props)
+
+ return new ctors[ctor](props)
+ }
+
+ if (isArrowFunction(ctor))
+ return ctor(props)
+
+ return new ctor(props)
+}
diff --git a/lang/gjs/src/binding.ts b/lang/gjs/src/binding.ts
index 95d905f..19a55cf 100644
--- a/lang/gjs/src/binding.ts
+++ b/lang/gjs/src/binding.ts
@@ -20,7 +20,7 @@ export interface Connectable {
[key: string]: any
}
-export default class Binding<Value> {
+export class Binding<Value> {
private transformFn = (v: any) => v
#emitter: Subscribable<Value> | Connectable
@@ -46,7 +46,7 @@ export default class Binding<Value> {
return `Binding<${this.#emitter}${this.#prop ? `, "${this.#prop}"` : ""}>`
}
- as<T>(fn: (v: Value) => T): Binding<T> {
+ as<T>(fn: (v: Value) => T | Binding<T>): Binding<T> {
const bind = new Binding(this.#emitter, this.#prop)
bind.transformFn = (v: Value) => fn(this.transformFn(v))
return bind as unknown as Binding<T>
@@ -72,8 +72,7 @@ export default class Binding<Value> {
return this.#emitter.subscribe(() => {
callback(this.get())
})
- }
- else if (typeof this.#emitter.connect === "function") {
+ } else if (typeof this.#emitter.connect === "function") {
const signal = `notify::${this.#prop}`
const id = this.#emitter.connect(signal, () => {
callback(this.get())
@@ -87,3 +86,4 @@ export default class Binding<Value> {
}
export const { bind } = Binding
+export default Binding
diff --git a/lang/gjs/src/file.ts b/lang/gjs/src/file.ts
index 6ad8be3..4220d9d 100644
--- a/lang/gjs/src/file.ts
+++ b/lang/gjs/src/file.ts
@@ -12,8 +12,7 @@ export function readFileAsync(path: string): Promise<string> {
Astal.read_file_async(path, (_, res) => {
try {
resolve(Astal.read_file_finish(res) || "")
- }
- catch (error) {
+ } catch (error) {
reject(error)
}
})
@@ -29,8 +28,7 @@ export function writeFileAsync(path: string, content: string): Promise<void> {
Astal.write_file_async(path, content, (_, res) => {
try {
resolve(Astal.write_file_finish(res))
- }
- catch (error) {
+ } catch (error) {
reject(error)
}
})
diff --git a/lang/gjs/src/gobject.ts b/lang/gjs/src/gobject.ts
index 6bd9969..7a5105f 100644
--- a/lang/gjs/src/gobject.ts
+++ b/lang/gjs/src/gobject.ts
@@ -90,9 +90,7 @@ export function property(declaration: PropertyDeclaration = Object) {
})
target.constructor[meta].Properties[kebabify(prop)] = pspec(name, ParamFlags.READWRITE, declaration)
- }
-
- else {
+ } else {
let flags = 0
if (desc.get) flags |= ParamFlags.READABLE
if (desc.set) flags |= ParamFlags.WRITABLE
@@ -124,8 +122,7 @@ export function signal(
target.constructor[meta].Signals[name] = {
param_types: arr,
}
- }
- else {
+ } else {
target.constructor[meta].Signals[name] = declaration || {
param_types: [],
}
@@ -137,8 +134,7 @@ export function signal(
this.emit(name, ...args)
},
})
- }
- else {
+ } else {
const og: ((...args: any[]) => void) = desc.value
desc.value = function (...args: any[]) {
// @ts-expect-error not typed
diff --git a/lang/gjs/src/gtk3/astalify.ts b/lang/gjs/src/gtk3/astalify.ts
index 9e6f022..9cab5b2 100644
--- a/lang/gjs/src/gtk3/astalify.ts
+++ b/lang/gjs/src/gtk3/astalify.ts
@@ -1,45 +1,11 @@
+import { hook, noImplicitDestroy, setChildren, mergeBindings, type BindableProps, construct } from "../_astal.js"
import Astal from "gi://Astal?version=3.0"
import Gtk from "gi://Gtk?version=3.0"
import Gdk from "gi://Gdk?version=3.0"
import GObject from "gi://GObject"
-import { execAsync } from "../process.js"
-import Variable from "../variable.js"
-import Binding, { kebabify, snakeify, type Connectable, type Subscribable } from "../binding.js"
+import Binding, { type Connectable, type Subscribable } from "../binding.js"
-export function mergeBindings(array: any[]) {
- function getValues(...args: any[]) {
- 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
-
- if (bindings.length === 1)
- return bindings[0].as(getValues)
-
- return Variable.derive(bindings, getValues)()
-}
-
-function setProp(obj: any, prop: string, value: any) {
- try {
- // the setter method has to be used because
- // array like properties are not bound correctly as props
- 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 { BindableProps, mergeBindings }
export default function astalify<
C extends { new(...args: any[]): Gtk.Widget },
@@ -65,63 +31,47 @@ export default function astalify<
get_click_through(): boolean { return this.clickThrough }
set_click_through(clickThrough: boolean) { this.clickThrough = clickThrough }
- declare private __no_implicit_destroy: boolean
- get noImplicitDestroy(): boolean { return this.__no_implicit_destroy }
- set noImplicitDestroy(value: boolean) { this.__no_implicit_destroy = value }
+ declare private [noImplicitDestroy]: boolean
+ get noImplicitDestroy(): boolean { return this[noImplicitDestroy] }
+ set noImplicitDestroy(value: boolean) { this[noImplicitDestroy] = value }
set actionGroup([prefix, group]: ActionGroup) { this.insert_action_group(prefix, group) }
set_action_group(actionGroup: ActionGroup) { this.actionGroup = actionGroup }
- _setChildren(children: Gtk.Widget[]) {
+ protected getChildren(): Array<Gtk.Widget> {
+ if (this instanceof Gtk.Bin) {
+ return this.get_child() ? [this.get_child()!] : []
+ } else if (this instanceof Gtk.Container) {
+ return this.get_children()
+ }
+ return []
+ }
+
+ protected setChildren(children: any[]) {
children = children.flat(Infinity).map(ch => ch instanceof Gtk.Widget
? ch
: new Gtk.Label({ visible: true, label: String(ch) }))
- // remove
- if (this instanceof Gtk.Bin) {
- const ch = this.get_child()
- if (ch)
- this.remove(ch)
- if (ch && !children.includes(ch) && !this.noImplicitDestroy)
- ch?.destroy()
+ if (this instanceof Gtk.Container) {
+ for (const ch of children)
+ this.add(ch)
+ } else {
+ throw Error(`can not add children to ${this.constructor.name}`)
}
- else if (this instanceof Gtk.Container) {
- for (const ch of this.get_children()) {
+ }
+
+ [setChildren](children: any[]) {
+ // remove
+ if (this instanceof Gtk.Container) {
+ for (const ch of this.getChildren()) {
this.remove(ch)
if (!children.includes(ch) && !this.noImplicitDestroy)
ch?.destroy()
}
}
- // TODO: add more container types
- if (this instanceof Astal.Box) {
- this.set_children(children)
- }
-
- else if (this instanceof Astal.Stack) {
- this.set_children(children)
- }
-
- else if (this instanceof Astal.CenterBox) {
- this.startWidget = children[0]
- this.centerWidget = children[1]
- this.endWidget = children[2]
- }
-
- else if (this instanceof Astal.Overlay) {
- const [child, ...overlays] = children
- this.set_child(child)
- this.set_overlays(overlays)
- }
-
- else if (this instanceof Gtk.Container) {
- for (const ch of children)
- this.add(ch)
- }
-
- else {
- throw Error(`can not add children to ${this.constructor.name}, it is not a container widget`)
- }
+ // append
+ this.setChildren(children)
}
toggleClassName(cn: string, cond = true) {
@@ -142,103 +92,15 @@ export default function astalify<
signalOrCallback: string | ((self: this, ...args: any[]) => void),
callback?: (self: this, ...args: any[]) => void,
) {
- if (typeof object.connect === "function" && callback) {
- const id = object.connect(signalOrCallback, (_: any, ...args: unknown[]) => {
- callback(this, ...args)
- })
- this.connect("destroy", () => {
- (object.disconnect as Connectable["disconnect"])(id)
- })
- }
-
- else if (typeof object.subscribe === "function" && typeof signalOrCallback === "function") {
- const unsub = object.subscribe((...args: unknown[]) => {
- signalOrCallback(this, ...args)
- })
- this.connect("destroy", unsub)
- }
-
+ hook(this, object, signalOrCallback, callback)
return this
}
constructor(...params: any[]) {
super()
- const [config] = params
-
- const { setup, child, children = [], ...props } = config
+ const props = params[0] || {}
props.visible ??= true
-
- // remove undefined values
- for (const [key, value] of Object.entries(props)) {
- if (value === undefined) {
- delete props[key]
- }
- }
-
- if (child)
- children.unshift(child)
-
- // collect bindings
- const bindings = Object.keys(props).reduce((acc: any, prop) => {
- if (props[prop] instanceof Binding) {
- const binding = props[prop]
- delete props[prop]
- return [...acc, [prop, binding]]
- }
- return acc
- }, [])
-
- // collect signal handlers
- const onHandlers = Object.keys(props).reduce((acc: any, key) => {
- if (key.startsWith("on")) {
- const sig = kebabify(key).split("-").slice(1).join("-")
- const handler = props[key]
- delete props[key]
- return [...acc, [sig, handler]]
- }
- return acc
- }, [])
-
- // set children
- const mergedChildren = mergeBindings(children.flat(Infinity))
- if (mergedChildren instanceof Binding) {
- this._setChildren(mergedChildren.get())
- this.connect("destroy", mergedChildren.subscribe((v) => {
- this._setChildren(v)
- }))
- }
- else {
- if (mergedChildren.length > 0) {
- this._setChildren(mergedChildren)
- }
- }
-
- // setup signal handlers
- for (const [signal, callback] of onHandlers) {
- if (typeof callback === "function") {
- this.connect(signal, callback)
- }
- else {
- this.connect(signal, () => execAsync(callback)
- .then(print).catch(console.error))
- }
- }
-
- // setup bindings handlers
- for (const [prop, binding] of bindings) {
- if (prop === "child" || prop === "children") {
- this.connect("destroy", binding.subscribe((v: any) => {
- this._setChildren(v)
- }))
- }
- this.connect("destroy", binding.subscribe((v: any) => {
- setProp(this, prop, v)
- }))
- setProp(this, prop, binding.get())
- }
-
- Object.assign(this, props)
- setup?.(this)
+ construct(this, props)
}
}
@@ -266,15 +128,13 @@ export default function astalify<
return Widget
}
-export type BindableProps<T> = {
- [K in keyof T]: Binding<T[K]> | T[K];
-}
-
type SigHandler<
W extends InstanceType<typeof Gtk.Widget>,
Args extends Array<unknown>,
> = ((self: W, ...args: Args) => unknown) | string | string[]
+export type BindableChild = Gtk.Widget | Binding<Gtk.Widget>
+
export type ConstructProps<
Self extends InstanceType<typeof Gtk.Widget>,
Props extends Gtk.Widget.ConstructorProps,
@@ -284,23 +144,21 @@ export type ConstructProps<
[S in keyof Signals]: SigHandler<Self, Signals[S]>
}> & Partial<{
[Key in `on${string}`]: SigHandler<Self, any[]>
-}> & BindableProps<Partial<Props> & {
+}> & BindableProps<Partial<Props & {
className?: string
css?: string
cursor?: string
clickThrough?: boolean
-}> & {
- onDestroy?: (self: Self) => unknown
- onDraw?: (self: Self) => unknown
- onKeyPressEvent?: (self: Self, event: Gdk.Event) => unknown
- onKeyReleaseEvent?: (self: Self, event: Gdk.Event) => unknown
- onButtonPressEvent?: (self: Self, event: Gdk.Event) => unknown
- onButtonReleaseEvent?: (self: Self, event: Gdk.Event) => unknown
- onRealize?: (self: Self) => unknown
- setup?: (self: Self) => void
-}
-
-export type BindableChild = Gtk.Widget | Binding<Gtk.Widget>
+}>> & Partial<{
+ onDestroy: (self: Self) => unknown
+ onDraw: (self: Self) => unknown
+ onKeyPressEvent: (self: Self, event: Gdk.Event) => unknown
+ onKeyReleaseEvent: (self: Self, event: Gdk.Event) => unknown
+ onButtonPressEvent: (self: Self, event: Gdk.Event) => unknown
+ onButtonReleaseEvent: (self: Self, event: Gdk.Event) => unknown
+ onRealize: (self: Self) => unknown
+ setup: (self: Self) => void
+}>
type Cursor =
| "default"
diff --git a/lang/gjs/src/gtk3/index.ts b/lang/gjs/src/gtk3/index.ts
index ff641af..39a1ae7 100644
--- a/lang/gjs/src/gtk3/index.ts
+++ b/lang/gjs/src/gtk3/index.ts
@@ -7,3 +7,4 @@ export { Astal, Gtk, Gdk }
export { default as App } from "./app.js"
export { astalify, ConstructProps, BindableProps }
export * as Widget from "./widget.js"
+export { hook } from "../_astal"
diff --git a/lang/gjs/src/gtk3/jsx-runtime.ts b/lang/gjs/src/gtk3/jsx-runtime.ts
index f2fe9a4..ee720af 100644
--- a/lang/gjs/src/gtk3/jsx-runtime.ts
+++ b/lang/gjs/src/gtk3/jsx-runtime.ts
@@ -1,11 +1,8 @@
import Gtk from "gi://Gtk?version=3.0"
-import { mergeBindings, type BindableChild } from "./astalify.js"
+import { type BindableChild } from "./astalify.js"
+import { mergeBindings, jsx as _jsx } from "../_astal.js"
import * as Widget from "./widget.js"
-function isArrowFunction(func: any): func is (args: any) => any {
- return !Object.hasOwn(func, "prototype")
-}
-
export function Fragment({ children = [], child }: {
child?: BindableChild
children?: Array<BindableChild>
@@ -16,29 +13,9 @@ export function Fragment({ children = [], child }: {
export function jsx(
ctor: keyof typeof ctors | typeof Gtk.Widget,
- { children, ...props }: any,
+ props: any,
) {
- children ??= []
-
- if (!Array.isArray(children))
- children = [children]
-
- children = children.filter(Boolean)
-
- if (children.length === 1)
- props.child = children[0]
- else if (children.length > 1)
- props.children = children
-
- if (typeof ctor === "string") {
- return new ctors[ctor](props)
- }
-
- if (isArrowFunction(ctor))
- return ctor(props)
-
- // @ts-expect-error can be class or function
- return new ctor(props)
+ return _jsx(ctors, ctor as any, props)
}
const ctors = {
diff --git a/lang/gjs/src/gtk3/widget.ts b/lang/gjs/src/gtk3/widget.ts
index 9d1c409..16bcbbd 100644
--- a/lang/gjs/src/gtk3/widget.ts
+++ b/lang/gjs/src/gtk3/widget.ts
@@ -4,6 +4,12 @@ import Gtk from "gi://Gtk?version=3.0"
import GObject from "gi://GObject"
import astalify, { type ConstructProps, type BindableChild } 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() },
@@ -14,6 +20,7 @@ export type BoxProps = ConstructProps<Box, Astal.Box.ConstructorProps>
export class Box extends astalify(Astal.Box) {
static { GObject.registerClass({ GTypeName: "Box" }, this) }
constructor(props?: BoxProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) }
+ protected setChildren(children: any[]): void { this.set_children(filter(children)) }
}
// Button
@@ -35,6 +42,12 @@ export type CenterBoxProps = ConstructProps<CenterBox, Astal.CenterBox.Construct
export class CenterBox extends astalify(Astal.CenterBox) {
static { GObject.registerClass({ GTypeName: "CenterBox" }, this) }
constructor(props?: CenterBoxProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) }
+ protected setChildren(children: any[]): void {
+ const ch = filter(children)
+ this.startWidget = ch[0] || new Gtk.Box
+ this.centerWidget = ch[1] || new Gtk.Box
+ this.endWidget = ch[2] || new Gtk.Box
+ }
}
// CircularProgress
@@ -91,6 +104,7 @@ export type LabelProps = ConstructProps<Label, Astal.Label.ConstructorProps>
export class Label extends astalify(Astal.Label) {
static { GObject.registerClass({ GTypeName: "Label" }, this) }
constructor(props?: LabelProps) { super(props as any) }
+ protected setChildren(children: any[]): void { this.label = String(children) }
}
// LevelBar
@@ -119,6 +133,11 @@ export type OverlayProps = ConstructProps<Overlay, Astal.Overlay.ConstructorProp
export class Overlay extends astalify(Astal.Overlay) {
static { GObject.registerClass({ GTypeName: "Overlay" }, this) }
constructor(props?: OverlayProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) }
+ protected setChildren(children: any[]): void {
+ const [child, ...overlays] = filter(children)
+ this.set_child(child)
+ this.set_overlays(overlays)
+ }
}
// Revealer
@@ -149,6 +168,7 @@ export type StackProps = ConstructProps<Stack, Astal.Stack.ConstructorProps>
export class Stack extends astalify(Astal.Stack) {
static { GObject.registerClass({ GTypeName: "Stack" }, this) }
constructor(props?: StackProps, ...children: Array<BindableChild>) { super({ children, ...props } as any) }
+ protected setChildren(children: any[]): void { this.set_children(filter(children)) }
}
// Switch
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..51c75d2 100644
--- a/lang/gjs/src/gtk4/index.ts
+++ b/lang/gjs/src/gtk4/index.ts
@@ -1,9 +1,10 @@
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 * as Widget from "./widget.js"
+export { astalify, ConstructProps }
+export * as Widget from "./widget.js"
+export { hook } from "../_astal"
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..bd9091b 100644
--- a/lang/gjs/src/gtk4/widget.ts
+++ b/lang/gjs/src/gtk4/widget.ts
@@ -1 +1,166 @@
-// 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
+type ButtonSignals = {
+ onClicked: []
+}
+
+export type ButtonProps = ConstructProps<Gtk.Button, Gtk.Button.ConstructorProps, ButtonSignals>
+export const Button = astalify<Gtk.Button, Gtk.Button.ConstructorProps, ButtonSignals>(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)
diff --git a/lang/gjs/src/index.ts b/lang/gjs/src/index.ts
index 8fe8d01..f448af9 100644
--- a/lang/gjs/src/index.ts
+++ b/lang/gjs/src/index.ts
@@ -4,5 +4,5 @@ export * from "./process.js"
export * from "./time.js"
export * from "./file.js"
export * from "./gobject.js"
-export { bind, default as Binding } from "./binding.js"
-export { Variable } from "./variable.js"
+export { Binding, bind } from "./binding.js"
+export { Variable, derive } from "./variable.js"
diff --git a/lang/gjs/src/package.json b/lang/gjs/src/package.json
new file mode 100644
index 0000000..b792213
--- /dev/null
+++ b/lang/gjs/src/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "astal",
+ "type": "module",
+ "license": "LGPL-2.1",
+ "exports": {
+ ".": "./index.ts",
+ "./gtk3": "./gtk3/index.ts",
+ "./gtk4": "./gtk4/index.ts",
+ "./gtk3/app": "./gtk3/app.ts",
+ "./gtk4/app": "./gtk4/app.ts",
+ "./gtk3/widget": "./gtk3/widget.ts",
+ "./gtk4/widget": "./gtk4/widget.ts",
+ "./gtk3/jsx-runtime": "./gtk3/jsx-runtime.ts",
+ "./gtk4/jsx-runtime": "./gtk4/jsx-runtime.ts",
+ "./binding": "./binding.ts",
+ "./file": "./file.ts",
+ "./gobject": "./gobject.ts",
+ "./process": "./process.ts",
+ "./time": "./time.ts",
+ "./variable": "./variable.ts"
+ }
+}
diff --git a/lang/gjs/src/process.ts b/lang/gjs/src/process.ts
index c41adc1..6e3a4a9 100644
--- a/lang/gjs/src/process.ts
+++ b/lang/gjs/src/process.ts
@@ -50,18 +50,15 @@ export function execAsync(cmd: string | string[]): Promise<string> {
Astal.Process.exec_asyncv(cmd, (_, res) => {
try {
resolve(Astal.Process.exec_asyncv_finish(res))
- }
- catch (error) {
+ } catch (error) {
reject(error)
}
})
- }
- else {
+ } else {
Astal.Process.exec_async(cmd, (_, res) => {
try {
resolve(Astal.Process.exec_finish(res))
- }
- catch (error) {
+ } catch (error) {
reject(error)
}
})
diff --git a/lang/gjs/src/variable.ts b/lang/gjs/src/variable.ts
index 9b3d3d2..016d73a 100644
--- a/lang/gjs/src/variable.ts
+++ b/lang/gjs/src/variable.ts
@@ -60,13 +60,11 @@ class VariableWrapper<T> extends Function {
if (v instanceof Promise) {
v.then(v => this.set(v))
.catch(err => this.variable.emit("error", err))
- }
- else {
+ } else {
this.set(v)
}
})
- }
- else if (this.pollExec) {
+ } else if (this.pollExec) {
this._poll = interval(this.pollInterval, () => {
execAsync(this.pollExec!)
.then(v => this.set(this.pollTransform!(v, this.get())))
@@ -143,8 +141,7 @@ class VariableWrapper<T> extends Function {
if (typeof exec === "function") {
this.pollFn = exec
delete this.pollExec
- }
- else {
+ } else {
this.pollExec = exec
delete this.pollFn
}
@@ -188,8 +185,7 @@ class VariableWrapper<T> extends Function {
const id = o.connect(s, set)
this.onDropped(() => o.disconnect(id))
}
- }
- else {
+ } else {
if (typeof sigOrFn === "string") {
const id = objs.connect(sigOrFn, set)
this.onDropped(() => objs.disconnect(id))
@@ -227,4 +223,5 @@ export const Variable = new Proxy(VariableWrapper as any, {
new<T>(init: T): Variable<T>
}
+export const { derive } = Variable
export default Variable
diff --git a/lib/astal/gtk3/src/widget/circularprogress.vala b/lib/astal/gtk3/src/widget/circularprogress.vala
index df1635d..de7a5c7 100644
--- a/lib/astal/gtk3/src/widget/circularprogress.vala
+++ b/lib/astal/gtk3/src/widget/circularprogress.vala
@@ -41,7 +41,7 @@ public class Astal.CircularProgress : Gtk.Bin {
}
static construct {
- set_css_name("circular-progress");
+ set_css_name("circularprogress");
}
public override Gtk.SizeRequestMode get_request_mode() {
diff --git a/lib/astal/gtk4/src/application.vala b/lib/astal/gtk4/src/application.vala
index fadf705..fe5dd8d 100644
--- a/lib/astal/gtk4/src/application.vala
+++ b/lib/astal/gtk4/src/application.vala
@@ -146,13 +146,10 @@ public class Astal.Application : Gtk.Application, AstalIO.Application {
if (reset)
reset_css();
- try {
- if (FileUtils.test(style, FileTest.EXISTS))
- provider.load_from_path(style);
- else
- provider.load_from_string(style);
- } catch (Error err) {
- critical(err.message);
+ if (FileUtils.test(style, FileTest.EXISTS)) {
+ provider.load_from_path(style);
+ } else {
+ provider.load_from_string(style);
}
Gtk.StyleContext.add_provider_for_display(
diff --git a/lib/astal/gtk4/src/meson.build b/lib/astal/gtk4/src/meson.build
index 8aac969..7b9c1e0 100644
--- a/lib/astal/gtk4/src/meson.build
+++ b/lib/astal/gtk4/src/meson.build
@@ -25,6 +25,8 @@ deps = [
]
sources = [config] + files(
+ 'widget/box.vala',
+ 'widget/slider.vala',
'widget/window.vala',
'application.vala',
)
diff --git a/lib/astal/gtk4/src/widget/box.vala b/lib/astal/gtk4/src/widget/box.vala
new file mode 100644
index 0000000..28f2b00
--- /dev/null
+++ b/lib/astal/gtk4/src/widget/box.vala
@@ -0,0 +1,50 @@
+public class Astal.Box : Gtk.Box {
+ /**
+ * Corresponds to [[email protected] :orientation].
+ */
+ [CCode (notify = false)]
+ public bool vertical {
+ get { return orientation == Gtk.Orientation.VERTICAL; }
+ set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
+ }
+
+ construct {
+ notify["orientation"].connect(() => {
+ notify_property("vertical");
+ });
+ }
+
+ public List<weak Gtk.Widget> children {
+ set {
+ foreach (var child in children) {
+ remove(child);
+ }
+ foreach (var child in value) {
+ append(child);
+ }
+ }
+ owned get {
+ var list = new List<weak Gtk.Widget>();
+ var child = get_first_child();
+ while (child != null) {
+ list.append(child);
+ child = child.get_next_sibling();
+ }
+ return list;
+ }
+ }
+
+ public Gtk.Widget? child {
+ owned get {
+ foreach (var child in children) {
+ return child;
+ }
+ return null;
+ }
+ set {
+ var list = new List<weak Gtk.Widget>();
+ list.append(child);
+ this.children = children;
+ }
+ }
+}
diff --git a/lib/astal/gtk4/src/widget/slider.vala b/lib/astal/gtk4/src/widget/slider.vala
new file mode 100644
index 0000000..ca026a2
--- /dev/null
+++ b/lib/astal/gtk4/src/widget/slider.vala
@@ -0,0 +1,65 @@
+public class Astal.Slider : Gtk.Scale {
+ private Gtk.EventControllerLegacy controller;
+ private bool dragging;
+
+ construct {
+ if (adjustment == null)
+ adjustment = new Gtk.Adjustment(0,0,0,0,0,0);
+
+ if (max == 0 && min == 0) {
+ max = 1;
+ }
+
+ if (step == 0) {
+ step = 0.05;
+ }
+
+ controller = new Gtk.EventControllerLegacy();
+ add_controller(controller);
+ controller.event.connect((event) => {
+ var type = event.get_event_type();
+ if (type == Gdk.EventType.BUTTON_PRESS ||
+ type == Gdk.EventType.KEY_PRESS ||
+ type == Gdk.EventType.TOUCH_BEGIN) {
+ dragging = true;
+ }
+ if (type == Gdk.EventType.BUTTON_RELEASE ||
+ type == Gdk.EventType.KEY_RELEASE ||
+ type == Gdk.EventType.TOUCH_END) {
+ dragging = false;
+ }
+ });
+ }
+
+ /**
+ * Value of this slider. Defaults to `0`.
+ */
+ public double value {
+ get { return adjustment.value; }
+ set { if (!dragging) adjustment.value = value; }
+ }
+
+ /**
+ * Minimum possible value of this slider. Defaults to `0`.
+ */
+ public double min {
+ get { return adjustment.lower; }
+ set { adjustment.lower = value; }
+ }
+
+ /**
+ * Maximum possible value of this slider. Defaults to `1`.
+ */
+ public double max {
+ get { return adjustment.upper; }
+ set { adjustment.upper = value; }
+ }
+
+ /**
+ * Size of step increments. Defaults to `0.05`.
+ */
+ public double step {
+ get { return adjustment.step_increment; }
+ set { adjustment.step_increment = value; }
+ }
+}
diff --git a/nix/devshell.nix b/nix/devshell.nix
index 66c46e5..9217a8d 100644
--- a/nix/devshell.nix
+++ b/nix/devshell.nix
@@ -40,21 +40,34 @@
libdbusmenu-gtk3
wayland
blueprint-compiler
+ libadwaita
dart-sass
lua
python
gjs
];
+
+ lsp = with pkgs; [
+ nodejs
+ mesonlsp
+ vala-language-server
+ vtsls
+ vscode-langservers-extracted
+ ];
in {
default = pkgs.mkShell {
- inherit buildInputs;
+ packages = buildInputs ++ lsp;
};
astal = pkgs.mkShell {
- buildInputs =
+ packages =
buildInputs
+ ++ lsp
++ builtins.attrValues (
- builtins.removeAttrs self.packages.${pkgs.system} ["docs"]
+ builtins.removeAttrs self.packages.${pkgs.system} [
+ "docs"
+ "cava" # FIXME: temporary autoreconf
+ ]
);
};
}