Read the announcement: GTKX 1.3: Introducing @gtkx/animated.
New Features
@gtkx/animatedbrings React Spring to GTK —useSpring,useSprings,useTrail,useTransition,useChainanduseSpringValue, theSpring,TrailandTransitioncomponents, and theanimated(Component)wrapper with itsanimated.GtkLabelshorthand drive real GObject properties. The wrapper writes each frame straight onto the widget through its ref, so a running spring does not re-render the component, while a prop with no native setter, such as theaccessible*props, falls back to a React render per frame. Frames come from the GTK frame clock, anduseReducedMotionreads GTK'sgtk-enable-animationsandgtk-interface-reduced-motionsettings.- A
styleprop on every widget — Every element that renders aGtk.Widgettakes astyleobject of CSS declarations, the way React DOM spells them. GTK4 has no inline styles, so the object compiles to a single rule in aGtk.CssProviderthat belongs to that widget alone, registered one step aboveSTYLE_PROVIDER_PRIORITY_APPLICATIONsostyleoutranks anything a class incssClassessets. Changing the object rewrites that one rule, so the cost does not grow with the number of distinct values a component cycles through; setting the prop toundefinedornullremoves it again. A key starting with&nests a block under a selector derived from it, so"&:hover"styles the hover state, and a bare number getspxwhere CSS expects a length. The prop is typed as a curated list of the paint and typography properties GTK4 actually understands rather than the whole web set, sostyle={{ display: "flex" }}fails to compile instead of becoming a runtime warning — layout stays in the widget's own props.StyleandStylePropertiesare new public types. - Springs drive
style—@gtkx/animatedwritesstyleper frame the way it writes a GObject property, through the widget'srefand without re-rendering the component, which is what makes a color, aborder-radiusor abox-shadowanimatable at all, none of which GTK exposes as a property. A spring can be the whole prop,style={level.to(declarations)}, or sit on a single declaration,style={{ color: styles.color }}, so the object a spring hook returns can be handed tostyleas it is, the way React Spring is written for the DOM. Onlystyleis read that way: a spring nested inside any other object-valued prop is left alone. - Cairo ships as
@gtkx/cairo— Contexts, surfaces, patterns, regions, matrices, font faces, font options, scaled fonts and devices are real classes.Surface,PatternandFontFaceare abstract, and instances arrive as the most specific class the package models, sosurface instanceof ImageSurfaceandctx.getSource() instanceof LinearPatternnarrow, while kinds with no dedicated subclass — a solid or surface pattern, a group target — come back as the base class. registerClassdeclares signals — Asignalsoption creates GObject signals on the new type, each naming itsparamTypesand optionalreturnTypeas numeric GTypes or as classes carrying one, itsflags, and anaccumulatoroffirst-winsortrue-handled.connect,emit,on,once,offanduseSignaltake the declared names, withprogress_changedandprogress-changedreaching the same signal, andemitconverts each argument into aGValueof the declared type.SignalSpecandSignalGTypeare new public types.GValueparameters take a plain JavaScript value — Every parameter the callee only reads is typedGObject.Value | JsValueand accepts the JavaScript value itself, with the GType inferred: string togchararray, boolean togboolean, a whole in-range number togint, any other number togdouble,biginttogint64orguint64, an array of strings toGStrv, a wrapper to the GType it carries, andnullto a NULLgpointer. Arguments of an emitted signal infer the same way, and passing aGObject.Valuebuilt by hand still works wherever inference would guess something else. A signal handler still receives a realGObject.Value.- GType parameters take a class — Every parameter that takes a GType accepts the class registered under it alongside the numeric
bigint, soGio.ListStore.new(Gtk.Label)andGObject.typeName(Gtk.Label)work, as do signal arguments declared as GTypes. A class that never went throughregisterClassis rejected rather than resolving to its parent's type. Only the input direction widens: return values, out parameters and handler arguments still hand back the numeric GType. - Abstract registered types —
abstract: trueregisters the type withG_TYPE_FLAG_ABSTRACT, the way the flag marks a C type. Registered subclasses instantiate as usual and inherit its vfunc overrides, while constructing the class itself throws, from JavaScript and from a native caller alike. classInitandcssNameonregisterClass—registerClasstakes aclassInithook and acssName, and every generated*ClassGTypeStruct wrapper is now paired with its class.GObject.ObjectClass.peekandGtk.WidgetClass.peekhand back any type's class struct outside aclassInithook, soGObject.ObjectClass.peek(Gtk.Label).findProperty("label")works, backed by the newpeekTypeClassandregisterClassStructruntime exports.- Property overrides with
paramSpecOverride—paramSpecOverride(name, source)redeclares a property a parent class or an implemented interface already carries, the wayg_param_spec_overridedoes in C, giving the subclass its own storage andnotifyemission while the value type, flags and default stay the ones the overridden spec declares. The source is a wrapper class, an interface, or a raw GType, and the call throws when it declares no property under that name.newParamSpecOverrideis the matching@gtkx/runtimeexport. ParamSpecintrospection — EveryGObject.ParamSpeccarries readonlyname,nick,blurb,flags,valueTypeandownerTypegetters, whether it comes from aparamSpec*constructor, anotifyhandler, orfindPropertyandlistProperties.flagsis theParamFlagsbitfield the spec was created with,valueTypethe GType of the values the property holds, andownerTypethe GType the spec is installed on,TYPE_INVALIDuntil it is installed on one.getParamSpecFlags,getParamSpecValueTypeandgetParamSpecOwnerTypeback them from@gtkx/runtime.fromVarianttakes a variant alone and unpacks recursively —fromVariantno longer requires a type string: it accepts aGLib.Varianton its own, and an options object withrecursive: trueunwraps every nested variant into the value it holds, all the way down.toVariantacceptsUint8Arrayornumber[]for a byte array either way.FromVariantOptions,RecursiveFromVariantOptions,RecursiveVariantValue,VariantInputandByteArrayare new exported types.- Bare
@gtkx/jsximports — The generated JSX store gains an index module, soimport { GtkLabel } from "@gtkx/jsx"reaches every namespace's components without the per-namespace subpath. The undeclared-library check in the CLI resolves and diagnoses the bare import, with its own message when the store has no index module. v2ValueReturns,v2FinishResultsandv2InoutReturnsfuture flags — Thefutureconfig block takes three new flags, all off by default and unconditional in 2.0.v2ValueReturnsmakes the bindings whose return or caller-allocated out parameter is aGValue—Gtk.DropTarget.getValue,Gtk.ConstantExpression.getValue,Gdk.Clipboard.readValueAsync,Gtk.Builder.valueFromStringType,Gtk.TreeModel.getValueand a handful more — hand back what the value holds, typedunknown.v2FinishResultsdrops the always-true leading success boolean from promisified async methods, soGio.File.loadContentsAsyncresolves to[number[], string | null], or[Uint8Array, string | null]withv2ByteArraysalso enabled, and a call left with a single out parameter, such asreplaceContentsAsync, resolves to that value directly.v2InoutReturnsstops repeating a caller-allocated inout record in a method's result, because the callee mutates the instance you passed and the returned entry was always the same object you already hold, soGsk.Path.getNext(point)becomesbooleanrather than[boolean, PathPoint]andPango.Matrix.transformRectangle(rect)becomesvoid; primitive inout parameters, which cannot be mutated in place, stay in the result either way. Flip them and runtsc: every site that needs attention is a type error.- New
@gtkx/runtimeexports — TheGValuemarshalling setfromValue,toValueHandle,tryToValueHandle,ValueMarshalErrorandJsValueis public, alongsidecoerceObjectProperty,registerWrapperClassResolverwith itsWrapperClassResolvertype,matchRegexandmatchAllRegex, which keep a regex subject's bytes alive alongside itsMatchInfo, andtrimFinish.
Breaking Changes
on<SignalName>methods become signal default handlers —registerClassinstalls every method matching/^on[A-Z]/as the class-closure default handler for the kebab-cased signal it names, GJS-style, on every call, walking the whole prototype chain up to the generated wrapper class. This retroactively promotes helpers written before the feature existed: anonShow,onDestroy,onMaporonNotifymethod on a subclass now runs on every emission of that inherited signal. Audit every registered class foron-prefixed methods and rename any that were not meant to be handlers; a name matching no real signal is left alone.- Cairo moves into
@gtkx/cairo, and@gtkx/gi/cairois deprecated —@gtkx/gi/cairostill resolves for all of 1.x and re-exports@gtkx/cairo, but a project that does not declare@gtkx/cairogets the copy the code generator links into the generated store plus a per-run notice, because 2.0 stops doing that; install@gtkx/cairoand point imports at it. Constructors andcreate*factories check the cairo status and throw instead of handing back a broken object —new Context(finishedSurface),new ImageSurface(...),ImageSurface.createandcreateFromPng,Surface.createSimilar,createSimilarImageandcreateForRectangle,new RecordingSurface(...)and thePattern.create*factories — so code that inspectedstatus()afterwards must move totry/catch.Surface.createSimilarImageis typed as returningImageSurfacerather thanSurface, and the*ConstructorPropstypes survive as deprecatedRecord<string, never>stubs removed in 2.0. - Unsafe C symbols are removed from the generated store — C symbols that double-free, thread-hop, or move raw pointers are marked non-introspectable, so the members vanish from the generated store and calling code stops compiling. Safe siblings remain for most of them — the typed
setAttributeString,setAttributeUint32and friends,addActionon the action group,asciiStrup,asciiStrdown,strstrLen,strrstrandinternString,logSetHandler,logRemoveHandlerandlogWriterDefault, andGLib.Thread.self()andjoin— so move each call to those. The raw object data accessors have no replacement: keep per-instance data in JavaScript instead.GObject.Object.prototype.getData,setData,setDataFull,stealData,getQdata,setQdata,setQdataFull,stealQdata, inherited by every generated classGObject.ParamSpec.prototype.getQdata,setQdata,setQdataFull,stealQdataGLib.Thread.new,tryNewandexit, plus the module-levelGLib.threadNew,threadTryNewandthreadExitGLib.refStringNew,refStringNewLen,refStringNewIntern,refStringAcquire,refStringRelease,refStringLengthGLib.stpcpy,strcanon,strchomp,strchug,strdelimit,strdown,strup,strreverseGLib.asciiDtostr,GLib.asciiFormatd,GLib.logSetWriterFuncGio.ActionMap.prototype.addActionEntries, and with itGio.SimpleActionGroup,Gio.ApplicationandGtk.ApplicationGio.FileInfo.prototype.setAttributeandGio.File.prototype.setAttribute
- Missing required arguments and invalid flag bits throw — A non-nullable argument that is omitted or passed as
undefinedthrows instead of being marshalled as garbage, soGLib.Uri.escapeString("a b"),label.setText()andGtk.acceleratorParse()all raise, while nullable arguments may still be omitted, skipped asundefined, or followed by extra trailing arguments. A flags argument is validated against the registered bits, soGtk.acceleratorGetLabel(key, 1 << 15)or any value carrying an undefined bit throws where it previously passed through; zero and the union of every defined bit stay valid. Fix the call sites the exceptions point at. - Number props are fitted to the property instead of refused — A number headed for a whole-number property (
gint, an enum, a flags field) is truncated toward zero and clamped to the ParamSpec range on the React reconciler path, in@gtkx/animated, and in the construct-property path of generated wrapper classes, so<GtkBox marginTop={12.7} />andnew Gtk.Label({ marginStart: 12.6, opacity: 1.5 })succeed as 12, 12 and 1 where they previously threw. Imperative writes truncate the same way —label.marginStart = 12.7andlabel.setProperty("margin-start", 12.7)land 12 — while a non-finite number or one outside the property's range is still refused. Code that relied on the old error to catch a bad computation must check the value itself. @gtkx/testinginstalls the React act environment itself —registerTestRuntimeHookssetsIS_REACT_ACT_ENVIRONMENTin abeforeAlland restores the previous value in anafterAll, and it runs on import, so every project using@gtkx/testinggets it. Test setups that set the global themselves can delete that boilerplate, since the hook saves and restores whatever it found. Projects that never set it are in an act environment for the first time, so imperative mutations made outsideact()— calling a selection-model method,activateAction,setText, or a store action directly — now warn and must be wrapped inact(() => …).registerClassvalidates the GType name —typeName, or the class name it falls back to, must match/^[A-Za-z_][A-Za-z0-9\-_+]{2,}$/, a letter or underscore followed by at least two more characters drawn from letters, digits,-,_and+, and anything else raises aTypeErrorinstead of reachingg_type_register_staticand producing a broken type behind ag_critical. A two-character class name, or one carrying a$or a dot, now fails at registration; pass an explicittypeName.registerClassrejectsGio.AsyncInitablewithoutvfuncInitAsync— ListingGio.AsyncInitableinimplementswithout avfuncInitAsyncanywhere on the class chain throws aTypeError, because the defaultinit_asyncslot runsvfuncIniton a worker thread and crashes the process. The guard checks only interfaces the class actually adopts, so it is skipped when the parent type already implementsGio.AsyncInitable. Add avfuncInitAsyncoverride, or drop the interface from the list.- Plain-struct ownership transfers are dropped from the store — Callables that take or hand over ownership of a plain struct, one that is neither boxed nor carries a ref/unref pair, are refused because neither side can free it correctly, which removes
GLib.PathBuf.prototype.freeandfreeToPath,GLib.OptionContext.prototype.free,GLib.StringChunk.prototype.free,GObject.ParamSpecPool.prototype.free, andGObject.enumCompleteTypeInfoandflagsCompleteTypeInfo. Use the non-consuming sibling where one exists —PathBuf.clearToPath()forfreeToPath(),PathBuf.clear()andStringChunk.clear()forfree()— and otherwise drop the call and let the wrapper's own lifetime handle it. - Callables with detached callback closure or destroy slots are dropped — A callable whose callback parameter has a
closureordestroyindex that is not immediately adjacent to it cannot be marshalled safely and is now refused, which removesGLib.Tree.newFull,GLib.Tree.prototype.traverseandJavaScriptCore.Value.prototype.objectDefinePropertyAccessor. Adjacent triples such asGtk.CustomFilter.newandGLib.AsyncQueue.newFullstill marshal; move to the sibling members that stay —GLib.Tree.prototype.foreachanddestroy,GLib.Node.prototype.traverse, andJavaScriptCore.Value.prototype.objectDefinePropertyData. - UCS-4 conversions return arrays of characters — The return type of
GLib.utf8ToUcs4,utf8ToUcs4Fastandutf16ToUcs4is corrected from the scalargunicharthe GIR describes to the array of characters the C functions produce, soGLib.utf8ToUcs4("a💩b", -1n)yields[["a", "💩", "b"], 6n, 3n]where the binding previously handed back a single character.GLib.ucs4ToUtf8accepts the array back, throwing on a surrogate, an out-of-range codepoint, a fractional element, or a multi-character string. Update any code that read the old scalar result. Gio.File.replaceContentsBytesAsyncis promisified — An override table supplies the finish function the GIR omits, so the binding changes from a void method taking a trailingAsyncReadyCallbacktoreplaceContentsBytesAsync(contents, etag, makeBackup, flags, cancellable?): Promise<[boolean, string | null]>, orPromise<string | null>withfuture.v2FinishResultsenabled. Drop the callback argument and await the call.- Record fields holding callbacks are typed
bigint— A record field whose GIR type is a callback renders asbigint, a raw function pointer, in both the field accessor and the record's constructor props, matching what actually crosses the boundary. Reads and writes of such fields,GObject.TypeInfo.classInitandGLib.SourceFuncs.prepareamong them, stop type-checking against a function and need a pointer value instead. newObjectWithPropertiesreturns the wrapper — The signature changes from(gtype, props, wrapper) => ExternalObject<Handle>to<T extends object>(gtype, props, wrapper: T) => T, because construction may find that the object already reached JavaScript and was wrapped — aGtk.Windowobserved throughgetToplevels()firingitems-changed— in which case the existing wrapper is returned so both references stay one object. Direct callers expecting a handle must callgetHandleon the result.v2ByteArraysalso governs whatfromVariantunpacks anayto — A byte-array variant unpacks throughgetDataAsBytes().unrefToArray()rather than element by element, so withfuture.v2ByteArraysenabledfromVariant("ay", v)returns aUint8Arrayinstead of anumber[], at both the type and the runtime level. Projects already on the flag must update any.push,.concat,Array.isArrayorJSON.stringifyuse on that result; projects without it see no change.toVariant's value parameter widens toVariantInput<S>, acceptingUint8Array | number[], and throws when a byte array is packed from anything else.- Deprecated in 1.3, removed in 2.0 —
Gdk.RGBA.create(css)gives way tonew RGBA()plusparse(...)with its return value checked, so an unparseable string is rejected rather than becoming transparent black, or tonew RGBA({ red, green, blue, alpha });Graphene.Point.create(x, y)becomesnew Point({ x, y }),Graphene.Rect.create(x, y, w, h)becomesnew Rect()plusinit(...), andGraphene.Size.create(w, h)becomesnew Size({ width, height }).GObject.buildValue(gtype, populate)is deprecated too: pass the JavaScript value itself where aValueis expected, or build one withnew Value()andinitwhen the GType is one inference cannot name. All still work in 1.x and surface a deprecation in the editor.
Bug Fixes
- A
Gdk.Surfaceis destroyed before its last reference drops, so collecting one no longer crashes the app. - Structs, boxed types and fundamentals no longer leak or double-free when a binding transfers ownership across the boundary.
GLib.Regexmatch results no longer point at a freed subject string.- An object that reaches JavaScript mid-construction no longer ends up with a second, conflicting wrapper.
- The same
Gsk.RenderNodeorGtk.Expressionalways comes back as the same JavaScript object. - A vfunc slot handed a callback, such as
Gio.AsyncInitable'sinit_async, receives a real callable instead of a raw pointer. - An error thrown from a vfunc or a callback reaches the C caller as a real
GErrorinstead of being swallowed. GValues and properties holding aGByteArrayread and write as bytes instead of failing.- Caller-allocated array results such as
Graphene.Matrix.toFloatandGdk.TextureDownloaderare returned instead of dropped. - Tree-list selection keeps reporting the right row ids after expanding a branch shifts rows.
prefers-color-scheme,prefers-contrastandprefers-reduced-motionblocks in CSS templates apply instead of being silently ignored.- Selecting many rows at once no longer costs an FFI call per row.