v1.0.0-rc.3
Pre-releaseThis release finishes replacing codegen's name-based heuristics with explicit lists and real field analysis: a widget's single child is expressed one way only, and a record is constructible when its bytes can actually be copied rather than when its name looks right. @gtkx/testing gains a typed as option, jest-dom's matcher vocabulary and public event-controller helpers, so a test asserts on widgets instead of casting them and calling getters. @gtkx/config loses its implicit-default mode: a project with no gtkx.config.ts is an error rather than a silent no-op.
Breaking changes
Widgets that hold one child no longer take it as a prop
content is gone from AdwApplicationWindow, AdwWindow, AdwToolbarView, AdwNavigationSplitView, AdwOverlaySplitView, AdwBottomSheet and AdwFlap. child, along with its onNotifyChild, is gone from every widget whose child a behavior adopts: GtkButton, GtkFrame, GtkScrolledWindow, GtkPopover, GtkWindow, GtkOverlay, AdwBin, AdwStatusPage, AdwDialog and the rest. Pass the widget as a child.
// Before
<AdwNavigationSplitView
sidebar={<AdwNavigationPage title="Lists"><Sidebar /></AdwNavigationPage>}
content={<AdwNavigationPage title="Items"><ContentPane /></AdwNavigationPage>}
/>
// After
<AdwNavigationSplitView sidebar={<AdwNavigationPage title="Lists"><Sidebar /></AdwNavigationPage>}>
<AdwNavigationPage title="Items">
<ContentPane />
</AdwNavigationPage>
</AdwNavigationSplitView>Which properties an element omits is an explicit per-type list now, not something codegen infers from a set_child method, so rc.2's half-measure of rejecting an element while still accepting a widget instance is gone. Sibling props such as sidebar are unchanged, and with the heuristic dropped every remaining writable object property accepts a ReactElement as well as an instance: GtkColumnViewCell.child, which no behavior claims, went from Gtk.Widget | null to Gtk.Widget | ReactElement | null.
A record is constructible only when its bytes can be copied
A record with no copy or free function of its own is duplicated by copying its bytes, which aliases anything those bytes point at, so it is now constructible only when its fields are transitively scalar. Otherwise its constructor throws Cannot construct <Name>: opaque boxed type with no known layout and its …ConstructorProps is empty. Gtk.RecentData, GLib.OptionEntry, Gio.ActionEntry, GObject.EnumValue, GObject.Parameter, Pango.Analysis and GdkPixbuf.PixbufModule are among them: take an instance from the API that produces it, whose fields stay readable and writable. A record carrying its own copy and free pair keeps its property constructor.
The same analysis gates field accessors. A field embedding another record by value is exposed only when that record is itself copyable, so Pango.Item.analysis, the attr of every Pango.Attr*, GObject.Parameter.value, GObject.CClosure.closure and HarfBuzz.segment_properties_t.language are gone from the accessors and from the constructor props. A field holding a pointer to a record is unaffected.
A missing gtkx.config.ts is now an error
loadConfig validates before it looks for a file, so it rejects with a gtkx.config.ts: error naming the missing applicationId instead of handing back an empty config. gtkx codegen, the preflight gtkx dev and gtkx build run, and @gtkx/mcp all fail there now, where codegen --force used to emit default Gtk-4.0 bindings and the rest used to do nothing silently. LoadedConfig.configFile is a string to match.
DropDown no longer takes a component prop
DropDown renders a Gtk.DropDown and nothing else. Use ComboRow from @gtkx/components/adw to present the same choice as a preferences row.
// Before
import { DropDown } from "@gtkx/components";
import { AdwComboRow } from "@gtkx/jsx/adw";
<DropDown component={AdwComboRow} title="Theme" items={themes} selectedId={theme} onSelectionChanged={setTheme} />
// After
import { ComboRow } from "@gtkx/components/adw";
<ComboRow title="Theme" items={themes} selectedId={theme} onSelectionChanged={setTheme} />ComboRow<T, S> takes the same collection props and adds the row's own, so title, subtitle, useSubtitle and enableSearch work on it. DropDownProps loses its third type parameter, and WidgetProps, which described the swap, is gone from @gtkx/components. ChildProps is unchanged.
@gtkx/testing reads widget state more strictly
toHaveTextContent no longer falls back to the accessible name, matching jest-dom, which reads textContent and nothing else. It reads the widget's own label, text or title, and otherwise its descendants' text joined by a space, so a widget named only by accessibleLabel or a tooltip has no text content at all: assert toHaveAccessibleName instead. The text is trimmed and its whitespace collapsed before comparing, which { normalizeWhitespace: false } reduces to turning non-breaking spaces into regular ones.
toHaveDisplayValue throws widget does not expose a display value rather than comparing against null, so a negated assertion on an unrelated widget no longer passes for the wrong reason.
The checked state is tri-state. A Gtk.CheckButton marked inconsistent reads as mixed, so toBeChecked() fails for it, the new toBePartiallyChecked() passes, and it answers a checked filter for neither true nor false.
New features
defineBehavior types an element behavior against the class it applies to
A behavior written as a bare object literal gets never for every hook's object parameter, so the first member access reports Property 'x' does not exist on type 'never', naming a type you never wrote. defineBehavior from @gtkx/react/config takes the GObject class as a type argument and infers the rest, and reports a wrong type argument where it is written rather than at the first member access. Bare object literals keep working.
import { defineBehavior, defineElements } from "@gtkx/react/config";
export default defineElements({
GtkWidget: {
behaviors: [
defineBehavior<Gtk.Widget>({
update: (widget, prev, next) => {
if (typeof next.cursorName === "string") widget.setCursorFromName(next.cursorName);
return ["cursorName"];
},
}),
],
},
});omitProps in gtkx.config.ts
elements.config[type].omitProps lists properties to leave out of that element's generated props, for a property a behavior already writes from children. It is the same list the built-in Adwaita and GTK entries now use.
Class and interface structs are bound, and functions that mention one are no longer dropped
A vtable such as GtkButtonClass or GtkBuildableIface exists as a type now, which is enough for the functions taking one to be bound. Constructing one throws unless its fields are transitively scalar, as they are for GObject.TypeClass and Gtk.OrientableIface, and a field gets an accessor wherever it can be marshalled, so Gtk.BuildableIface exposes gIface and GObject.EnumClass its values while a function-pointer field gets none.
The recovered surface includes Soup's websocket handshake functions, whose supportedExtensions is a GObject.TypeClass[], along with GObject.enumGetValue* and flagsGetValue*. Those last are emitted but not callable yet: nothing in the bound API produces the EnumClass or FlagsClass they take.
Typed queries and jest-dom's matchers in @gtkx/testing
Every query takes as, naming a widget class. It filters candidates by instanceof and narrows the return type, so the cast that used to follow a query disappears, on screen, on a render result and through within.
// Before
const scale = (await screen.findByRole(Gtk.AccessibleRole.SLIDER)) as Gtk.Scale;
// After
const scale = await screen.findByRole(Gtk.AccessibleRole.SLIDER, { as: Gtk.Scale });The matcher set closes most of the gap with jest-dom, adding toBeDisabled, toBeEnabled, toBeVisible, toBeRooted, toBeEmpty, toBeInvalid, toBeValid, toBeRequired, toBePartiallyChecked, toHaveFocus, toHaveRole, toHaveSelection, toHaveAccessibleDescription, toHaveAccessibleErrorMessage, toContainElement, toHaveClass and toHaveObjectProperty. toHaveObjectProperty works on any GObject.Object and replaces asserting on a getter's return value, and toHaveValue now also takes a string, delegating to the display-value matcher.
// Before
expect(scale.getAdjustment().getUpper()).toBe(100);
expect(button.getSensitive()).toBe(true);
// After
expect(scale.getAdjustment()).toHaveObjectProperty("upper", 100);
expect(button).toBeEnabled();queryController, queryAllControllers, getAllControllers and the new getController are exported too, replacing the hand-written loop a test needed to reach a Gtk.EventController.
const drag = getController(handle, Gtk.GestureDrag);Bug fixes
- Fixed a transfer-none field write releasing the value it displaced. Writing a boxed or fundamental field gives GTKX no claim on the pointer already in the slot, but the write freed or unreffed it anyway, and the copy it stored was never handed back to be owned. Reachable from
Pango.GlyphItem.item,GdkPixbuf.PixbufModule.infoand, worst,GObject.ValueArray.values, where an array ofGValuewas freed as though it were one. - Fixed writing an inline fundamental field. The codec has no size for one, so it stored a pointer into the embedded struct and destroyed the bytes it displaced; it reports an error instead. The
Pango.Attr*.attrfields that reached it are no longer generated at all. - Fixed an inline
GValuefield being copied byte for byte, which aliased the source's contents without taking a reference. It is deep-copied throughg_value_copynow, and an inlineGClosurefield, which has no copy-into-place operation at all, reports an error rather than duplicating a reference count. - Fixed a record constructor describing an inline field differently from the matching setter, so it stored a pointer where the embedded bytes belong.
- Fixed a newly created floating fundamental being marshalled as an owned reference.
GParamSpecinstances are born floating and GIR annotates the constructorstransfer-ownership="full"all the same, so aGObject.paramSpec*result adopted a claim nobody had taken and was over-released. They are sunk through the type's own ref function now, which is whatregisterClass'spropertiesoption depends on. - Fixed
getTypereading a handle that does not reference a GObject; it returns 0 now instead of dereferencing a non-GObject pointer. - Fixed a record constructor or field setter throwing
Expected an Object for Boxed field write type, got Objectwhen handed a wrapper instance. A field write converts throughtoNativenow, the counterpart of thefromNativeits getter already used, sonew Gsk.ColorStop({ offset, color })andnew Graphene.Rect({ origin, size })work again. - Fixed
gtkx codegenreporting bindings up to date over a storenpm installhad pruned. The self-link and manifest that make a store resolvable were checked for gi and not for jsx, so a pruned jsx store read as fresh and surfaced later as a missing@gtkx/jsx. Both are checked now. - Fixed a blockable signal being dropped for the whole of a React commit rather than only while GTKX itself was writing.
onChanged,onToggled,onNotify*,onSelectionChangedand their siblings were suppressed fromprepareForCommituntil afterresetAfterCommit, so an emission the framework did not cause, one raised from auseLayoutEffector one aimed at anothercreateRoottree, was swallowed along with the ones it should have blocked. Each framework write is wrapped individually now, and only a handler firing inside one is skipped. - Fixed
render's harness window putting a title bar into the accessibility tree. It set aGtk.HeaderBartitlebar, whose window handle, center box and title label competed with the widgets under test, sogetByRole(Gtk.AccessibleRole.LABEL)on a single rendered label found two. The window is undecorated now, and the tree holds only what the test rendered. - Fixed a widget with an
accessibleLabelledByrelation reporting its own text as its accessible name. The relation comes first now, which is the precedence ARIA defines, changing whatgetByRole(role, { name })matches and whattoHaveAccessibleNamesees. - Fixed
toBeSelectedthrowing for widgets that plainly have a selected state.Gtk.FlowBoxChildis handled next toGtk.ListBoxRow, and theSELECTEDflag is read for theLIST_ITEM,GRID_CELL,OPTIONandTREE_ITEMroles as well asROW. - Fixed
getByDisplayValueandtoHaveDisplayValuefinding nothing on a drop-down: a widget with theCOMBO_BOXrole reports the text of its visible, non-popover face now. - Fixed
getByPlaceholderTextandtoHavePlaceholderTextmissing an editable that carriesplaceholder-textas a property but has no getter method of its own, such asGtkPasswordEntry. createPortalreportsReactPortalagain rather thanReactNode, and no longer assignstypeandpropsonto the portal it returns.@types/reactand@types/react-reconcilereach declare their ownReactPortaland only React's carries thetypeandpropsit inherits fromReactElement; rc.2 bridged the two by fabricating those fields at runtime, where they named a host type that does not exist and nothing read them. Rendering is unchanged.