Skip to content

v1.0.0-rc.2

Pre-release
Pre-release

Choose a tag to compare

@eugeniodepalo eugeniodepalo released this 30 Jul 12:18
· 176 commits to main since this release
02dc3b5

This release moves declarative placement out of @gtkx/components and onto the generated JSX elements themselves, so menus, grid and fixed placement, overlays, size groups and constraint layouts are expressed with the real GTK types. Element customization changes shape too: the declarative elementProps grammar in gtkx.config.ts is replaced by typed behavior hooks written in TypeScript. GSettings values are now typed and marshalled from each key's GVariant type string, and signal delivery during a React commit no longer swallows handlers it should never have swallowed. The collection views were rebuilt around one subscription per bound cell and a lazily built model, so scrolling re-renders only the cells whose own item changed and a collection of millions of rows no longer materializes an object per row.

Breaking changes

Menu, Grid, Fixed, Overlay and ConstraintLayout were removed from @gtkx/components

All five components, their sub-components and their prop types are gone. Placement is now expressed by wrapping each child in the generated GtkGridLayoutChild, GtkFixedLayoutChild or GtkOverlayLayoutChild element. Because those set properties on the real Gtk.LayoutChild, changing a cell repositions the widget in place instead of removing and re-attaching it.

// Before
import { Grid } from "@gtkx/components";

<Grid>
    <Grid.Child component={GtkLabel} column={0} row={0} />
    <Grid.Child component={GtkEntry} column={1} row={0} columnSpan={2} />
</Grid>

// After
import { GtkGrid, GtkGridLayoutChild } from "@gtkx/jsx/gtk";

<GtkGrid>
    <GtkGridLayoutChild column={0} row={0}>
        <GtkLabel />
    </GtkGridLayoutChild>
    <GtkGridLayoutChild column={1} row={0} columnSpan={2}>
        <GtkEntry />
    </GtkGridLayoutChild>
</GtkGrid>

Menu becomes <GMenu items={items}> from @gtkx/jsx/gio, taking the same array, with the entry type now exported from @gtkx/react as MenuItem. Overlay.Child becomes a GtkOverlayLayoutChild passed in GtkOverlay's new overlays prop. Fixed's x/y shorthand has no equivalent: GtkFixedLayoutChild exposes only the Gsk.Transform. An unwrapped GtkGrid or GtkFixed child is attached with default placement.

ConstraintLayout, its Constraint, Guide and Vfl sub-components and the ConstraintLayoutProps, ConstraintProps, ConstraintGuideProps and ConstraintVflProps types are gone as well. Constraints are now GtkConstraint and GtkConstraintGuide elements passed in GtkConstraintLayout's constraints, guides and vfl props. The participants are Gtk.ConstraintTarget objects rather than names, so capture each widget in state and render the constraints once it resolves; an omitted source still means the widget that owns the layout, and omitting both source and sourceAttribute still makes the relation a constant.

// Before
import { ConstraintLayout } from "@gtkx/components";

<GtkBox layoutManager={(
    <ConstraintLayout>
        <ConstraintLayout.Constraint
            target="button"
            targetAttribute={Gtk.ConstraintAttribute.START}
            sourceAttribute={Gtk.ConstraintAttribute.START}
            constant={8}
        />
    </ConstraintLayout>
)}>
    <GtkButton name="button" label="Constrained" />
</GtkBox>

// After
import { GtkConstraint, GtkConstraintLayout } from "@gtkx/jsx/gtk";

const [button, setButton] = useState<Gtk.Button | null>(null);

<GtkBox layoutManager={(
    <GtkConstraintLayout constraints={button && (
        <GtkConstraint
            target={button}
            targetAttribute={Gtk.ConstraintAttribute.START}
            sourceAttribute={Gtk.ConstraintAttribute.START}
            constant={8}
        />
    )}
    />
)}>
    <GtkButton ref={setButton} label="Constrained" />
</GtkBox>

A vfl block keeps its lines, hspacing and vspacing fields and gains views, the map from the names used in the description to targets. Blocks are compared field by field and views by identity, so memoize that map or every render tears the parsed constraints down and rebuilds them. GtkConstraintGuide's properties are ordinary, so it updates in place; every GtkConstraint property is construct-only, so a constraint that has to change needs a key that changes with it.

Element customization moved from elementProps to elements and @gtkx/react/config

The elementProps option and the Arg/Call/ArgRef prop-mapping grammar are removed. Custom elements are now authored as behavior objects with defineElements from the new @gtkx/react/config entrypoint, pointed at by elements.behaviors in gtkx.config.ts. Behaviors carry no type information, so a prop a behavior introduces has to be declared by hand.

Besides update, a behavior can hook attach, detach, reorder and resolve for children, createContext for private per-element state, mount, unmount and flush for commit-time work, and create for a type whose constructor does more than set properties (create is used for its own type only, never inherited by subtypes).

// gtkx.config.ts
export default defineConfig({
    applicationId: "com.example.app",
    elements: { behaviors: "./src/elements.ts" },
});

// src/elements.ts
import { defineElements } from "@gtkx/react/config";

export default defineElements({
    GtkWidget: {
        behaviors: [
            {
                update: (widget: Gtk.Widget, prev, next) => {
                    if (typeof next.cursorName === "string") widget.setCursorFromName(next.cursorName);
                    return ["cursorName"];
                },
            },
        ],
    },
});

declare module "@gtkx/jsx/gtk" {
    interface GtkWidgetProps {
        cursorName?: string | null | undefined;
    }
}

Signals are no longer blocked during a commit unless you declare them

Previously every handler was suppressed while React applied a commit, apart from a fixed allowlist of eleven lifecycle signals. That is now inverted: only signals classified as user-event signals for the emitting type are suppressed, resolved through the type's ancestry from a built-in table covering the state-echo signals (GObject::notify, GtkEditable::changed, GtkToggleButton::toggled, GtkRange::value-changed, and more). Handlers that used to be dropped silently, including onClicked, onActivate, onRowActivated and every app-defined custom signal, now fire. To suppress one, add it under its emitting type in userEventSignals in gtkx.config.ts; entries there are merged into the built-in table rather than replacing it.

GSettings value types come from the schema's type strings, and useBindSetting takes an options object

SchemaRef<K> is replaced by SettingsSchema<K>, whose generic parameter is the record of GVariant type strings rather than a hand-written map of value types, so every key's value type is derived instead of declared. That is what lets tuples, dictionaries, maybe types and nested arrays unpack into plain JavaScript values instead of a raw GLib.Variant. Two kinds change at runtime: enum keys are now a number read through getEnum rather than a nick string, and flags keys a bitfield number rather than string[].

// Before
const SCHEMA: SchemaRef<{ enabled: boolean; "wrap-mode": "none" | "word"; "window-size": GLib.Variant }> = {
    id: "com.example.app",
    path: null,
    keys: { enabled: "b", "wrap-mode": "enum", "window-size": "(ii)" },
};
useBindSetting(SCHEMA, "enabled", switchRef, "active");

// After
const SCHEMA: SettingsSchema<{ enabled: "b"; "wrap-mode": "enum"; "window-size": "(ii)" }> = {
    id: "com.example.app",
    path: null,
    keys: { enabled: "b", "wrap-mode": "enum", "window-size": "(ii)" },
};
useBindSetting({ schema: SCHEMA, key: "enabled", object: switchRef, property: "active" });

useSetting(schema, key) keeps its positional signature. SettingsSchemaKeys and SettingValue are exported alongside SettingsSchema from @gtkx/react.

Text buffer content uses GtkTextChildAnchor for both widgets and paintables

The synthetic <GtkTextAnchor> and <GtkTextPaintable> elements are gone, and so is the TextPaintableProps type that @gtkx/react exported for the latter. Both kinds of embedded object are now the real GtkTextChildAnchor element from @gtkx/jsx/gtk: give it a child widget, or give it a paintable prop. Passing both throws, as mixing a text prop with content children on <GtkTextBuffer> does.

// Before
<GtkTextBuffer>
    {"The buffer can have images in it: "}
    <GtkTextPaintable paintable={texture} />
</GtkTextBuffer>

// After
<GtkTextBuffer>
    {"The buffer can have images in it: "}
    <GtkTextChildAnchor paintable={texture} />
</GtkTextBuffer>

Because the paintable is inserted while the buffer is built, an enclosing <GtkTextTag> covers it and its character counts towards the offsets of the text after it. An anchor with a custom replacement character still has to be built with Gtk.TextChildAnchor.newWithReplacement and inserted through buffer.insertChildAnchor.

Changing a construct-only prop throws instead of being ignored

GTK accepts a construct-only property only while the object is being built, so a later change never reached the widget. That was silent before; it now throws, naming the prop and the element and telling you to give the element a key that changes with the prop so React builds a new one. GtkConstraint is the type this matters most for, since every one of its properties is construct-only.

Renamed types

In @gtkx/components: ItemNode is now Item, SectionNode is Section, RenderItemProps is RenderItemArgs, DropDownItemRenderer is ItemRenderer, and ColumnDef is Column. Prop names and runtime shapes are unchanged, so these are mechanical, but one behavior change rides along: GridView is pinned to a flat collection, so nested Item.children no longer build a tree there (ListView and ColumnView still auto-detect).

The split between a view's widget props and its declarative props is gone with them. ColumnViewDeclarativeProps, DropDownDeclarativeProps, GridViewDeclarativeProps, ListViewDeclarativeProps, ColumnDefDeclarativeProps and ColumnViewSortProps are removed, and ColumnViewProps, DropDownProps, GridViewProps and ListViewProps now name the merged type that used to be the two halves together, so a component typed with one of them keeps working while one typed with a *DeclarativeProps has to move to the merged name. CollectionItemSizeProps, ControlledSelectionProps and ControlledExpansionProps are no longer exported; spell those props out or take them from the view's own props type. ChildProps loses its second Placement parameter, which existed only for the removed placement components. RenderHeaderArgs and HeaderRenderer are new.

In @gtkx/react: ObjectProp<T extends GObject.Object> is now RefProp<T extends object>, ElementProp is gone, and createPortal returns ReactNode rather than ReactPortal.

Generated bindings changed, so regenerate and expect a few compile errors

Gtk.EVENT_STOP and Gtk.EVENT_CONTINUE are removed: use Gdk.EVENT_STOP and Gdk.EVENT_PROPAGATE. Boolean-typed GIR constants now emit real booleans, so those and GLib.SOURCE_CONTINUE/SOURCE_REMOVE hold true/false instead of the strings "true" and "false". A namespace function that GIR marks as moved onto a type is no longer exported at namespace level, so Gio.fileNewForPath becomes Gio.File.newForPath and Pango.fontDescriptionFromString becomes Pango.FontDescription.fromString. Construct-only properties are emitted read-only, since g_object_set_property refuses them anyway: pass them through the constructor or as a JSX prop. GLib's 64-bit constants are now bigint, and abstract GTypes such as GtkWidget are no longer JSX elements.

Re-run gtkx codegen (or delete node_modules/.gtkx) after upgrading. The corrected offsets, nullability and promisification metadata only land in regenerated bindings.

React 19.2 is now the minimum

@gtkx/react and @gtkx/components raised their react peer from ^19 to ^19.2, and @gtkx/testing, which declared no peers before, now declares react and @types/react at ^19.2.

New features

Toasts in @gtkx/components/adw

A new ./adw entrypoint exports ToastProvider, useToast and useToastOverlay. ToastProvider takes an overlayRef that you also give to your AdwToastOverlay; useToast() returns { show, dismiss } and useToastOverlay() returns { dismissAll }, with ToastOptions being Adw.ToastConstructorProps plus onButtonClicked and onDismissed. It needs "Adw-1" in libraries.

More container slots on the generated elements

Beyond GMenu's items and GtkOverlay's overlays, GtkSizeGroup takes widgets and GtkConstraintLayout takes constraints, guides and vfl (typed by the new VflConstraints export). Element-valued props are detected structurally rather than from a registered list of names, and the check recurses into arrays, so a slot accepts a single element, an array or a conditional expression. SizeGroup survives in @gtkx/components, reimplemented on these props.

useSignal reconnects when immediate changes

The hook calls through useEffectEvent, so every emission runs the handler from the latest render and the connection is keyed only on object, signal, after and immediate. Toggling immediate now reconnects and re-fires; it was previously ignored.

createRoot accepts any GObject as its container

It was previously restricted to a RootElement. Error reporting is unchanged: an uncaught render error is still rethrown, and one an error boundary caught is still logged.

prettyWidget depth limiting

PrettyWidgetOptions gained maxDepth, which flows through prettyWidget, logWidget, RenderResult.debug and screen.debug, replacing deeper children with a hidden-count summary line.

registerClass installs GObject properties

registerClass takes a properties option, a record from property name to the GObject.ParamSpec describing it. The class gets real installed properties: g_object_get_property and g_object_set_property work, notify is emitted, and they can be driven from GTK expressions such as Gtk.PropertyExpression, which is what lets a custom class be sorted natively by GtkNumericSorter.

Accessors are generated on the prototype for the dashed, underscored and camelCased spellings unless the class already defines one, so a class can supply its own getter or setter and the property vfuncs read and write through it. Declare the field with declare rather than initializing it: a class field would shadow the prototype accessor.

class Swatch extends GObject.Object {
    declare red: number;
}

registerClass(Swatch, {
    typeName: "AppSwatch",
    properties: { red: paramSpecInt("red", null, null, 0, 255, 0, ParamFlags.READWRITE) },
});

Performance

Collection views re-render one cell at a time

ListView, ColumnView, GridView and DropDown now put one React fiber per live GtkListItem, each subscribed to its own binding, so a cell re-renders only when the item bound to it changes. Every live cell used to re-render whenever anything in the collection changed, which made the cost of scrolling grow with the size of the view rather than staying flat. In the gtk-demo Characters demo that is 1,236 renders per bind down to 1, and roughly 330 ms down to 7 ms of work per page scrolled.

The model behind those views is also built lazily and keyed by item id, so a large collection no longer materializes one GObject per row up front, and a filter that shrinks the list and grows it again reuses the objects it already made instead of rebuilding them. The gtk-demo Colors demo renders its full 16,777,216 colors, and a keystroke in the Words demo search costs about 307 ms instead of 2,851 ms.

Sorting a collection of that size is still bounded by the per-item cost of the sorter you give it, so the Colors demo keeps a JavaScript Gtk.CustomSorter.

Bug fixes

  • Fixed a class registered with registerClass writing its interface implementations into the vtable it shares with its parent, which patched every instance of the parent type process-wide. Implementations now go in through g_type_add_interface_static, so each type gets its own vtable.
  • Fixed registerClass reading every prototype member while scanning for vfunc overrides, which invoked any getter the class defined against the bare prototype at registration time.
  • Fixed writing an unchanged text prop resetting the caret to the start of a GtkEditable, GtkEntryBuffer or GtkTextBuffer: GTK's setter replaces the contents even when they are identical, so the live text is compared before writing.
  • Fixed a callback being freed while it was still executing, which crashed the process when a handler disconnected itself (as once does) or destroyed the object it was attached to.
  • Fixed transfer-none string and container returns from a JavaScript callback being freed on the callback's next invocation, so GTK no longer reads freed memory from list factories, sorters, filters and vfunc implementations.
  • Fixed record and array marshalling throughout: field offsets for bit-fields, anonymous nested unions and the graphene SIMD types; fields embedding another record by value (such as Graphene.Rect's origin and size) being dereferenced as pointers; array arguments reading their element count from the wrong slot; and arrays passed into JavaScript callbacks being decoded without their length parameter.
  • Fixed a type derived from a fundamental, non-GObject base being marshalled as a GObject when a constructor returned it, so Gtk.PropertyExpression.new, Gsk.ColorNode.new and their siblings reference-counted the result through g_object_ref/g_object_unref and printed G_IS_OBJECT assertion failures. GIR marks only the root of such a hierarchy (GtkExpression, GskRenderNode) as fundamental, so the derived type now inherits the ref pair from it.
  • Fixed calls into C functions whose callback parameter has no user-data slot (for example Gtk.CustomLayout.new) pushing an extra argument, which shifted every following argument.
  • Fixed pure out-parameters of JavaScript callbacks and vfuncs being seeded from uninitialized caller memory, handing the callback an arbitrary pointer or a nonsense number.
  • Fixed two leaks: one JavaScript object per GObject re-wrap, and every string written into a plain struct field.
  • Fixed stale signal handler ids being disconnected after the emitter was disposed, which printed GLib-GObject-CRITICAL warnings and could disconnect a live handler that GLib had since given the same id.
  • Fixed <GtkTextChildAnchor> building its anchor with g_object_new, which left the object without the internal segment GTK's own constructor allocates: the anchor in the buffer was a different one from the element's, and calling getDeleted or getWidgets on the ref crashed the process. The element's own anchor is now the one inserted into the buffer.
  • Fixed any state update under a <GtkTextBuffer> wiping and rebuilding the whole buffer, which reset the cursor and selection, destroyed marks and tags, and recreated embedded child widgets; text changes are now applied at the child's offset.
  • Fixed a widget staying permanently insensitive after its actionName prop was cleared, which left reused buttons greyed out for the rest of the session.
  • Fixed an AdwDialog staying attached to its old window when its parent window changed.
  • Fixed async methods whose finish function does not follow the _finish naming convention (such as Gio.IOStream.spliceAsync) not being promisified, so awaiting them now works.
  • Fixed clearing the transform prop of a fixed layout child throwing "No native handle associated with".
  • Fixed a throwing cleanup step in @gtkx/testing abandoning the rest of the queue, which leaked widgets and handlers into the next test; every step now runs and failures are reported as an AggregateError.
  • Fixed saved screenshots being written to a shared, predictable /tmp path, where they could collide or be read by another user.