@gjsify/adwaita-core 0.64.12-next.281 → 0.64.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { AdwComboOption, AdwListItemsChanged } from './list.js';
1
+ import type { AdwComboOption, AdwListItemsChanged, AdwListModelSource } from './list.js';
2
2
  export { ADW_COMBO_NO_SELECTION, normalizeComboOptions } from './list.js';
3
3
  export type { AdwComboOption, AdwComboOptionInput } from './list.js';
4
4
  export { SpinState } from './adjustment.js';
@@ -43,6 +43,29 @@ export interface ComboStateChange {
43
43
  export type ComboStateListener = (change: ComboStateChange) => void;
44
44
  /** Subscriber for {@link ComboState} MODEL changes — one `items-changed` per replacement. */
45
45
  export type ComboItemsListener = (change: AdwListItemsChanged) => void;
46
+ /**
47
+ * A GObject property of `Adw.ComboRow` / `Gtk.DropDown` whose `notify::` a renderer owes its
48
+ * widget — the three ADR 0122 § 5's table is written in terms of.
49
+ */
50
+ export type ComboNotify = 'selected' | 'selected-item' | 'model';
51
+ /** Subscriber for the `notify::*` a {@link ComboState} transition owes, in emission ORDER. */
52
+ export type ComboNotifyListener = (property: ComboNotify) => void;
53
+ /**
54
+ * What {@link ComboState} needs off a model to WATCH it, beyond reading it.
55
+ *
56
+ * Optional, and duck-typed for the reason `AdwListModelSource` is: a real `Gio.ListModel` has
57
+ * `connect`, and so does every model this repository builds, but the reader in `list.ts` accepts
58
+ * anything with the two getters and a state machine that DEMANDED a signal surface would refuse
59
+ * models it can otherwise read perfectly.
60
+ *
61
+ * EXPORTED because it is the published type of a `model` property now (ADR 0122 § 4). Left
62
+ * un-exported it still reached the `.d.ts`, so a consumer could satisfy it and not NAME it —
63
+ * which is the shape that makes a caller declare its own copy.
64
+ */
65
+ export interface ComboWatchableModel extends AdwListModelSource {
66
+ connect?: (signal: string, handler: (...args: unknown[]) => void) => number;
67
+ disconnect?: (id: number) => void;
68
+ }
46
69
  /**
47
70
  * The selection state of a combo row: a portable list model (`AdwListModel`) plus
48
71
  * the selected index, with a two-way index↔value mapping and empty/out-of-range guards.
@@ -60,12 +83,39 @@ export type ComboItemsListener = (change: AdwListItemsChanged) => void;
60
83
  * assignment.
61
84
  */
62
85
  export declare class ComboState {
86
+ /** The model's IDENTITY — the object `model` hands back, never a copy (ADR 0122 § 4). */
87
+ private _model;
88
+ /** The descriptors the held model holds, re-read whenever it says it changed. */
63
89
  private _options;
64
90
  private _selected;
91
+ /** Detaches this state from the held model's `items-changed`. */
92
+ private _unwatch;
65
93
  private readonly _listeners;
66
94
  private readonly _itemListeners;
95
+ private readonly _notifyListeners;
67
96
  /** Subscribe to selection changes. Returns an unsubscribe function. */
68
97
  subscribe(listener: ComboStateListener): () => void;
98
+ /**
99
+ * Subscribe to the `notify::*` each transition owes, delivered one at a time IN ORDER.
100
+ *
101
+ * Separate from {@link subscribe} because the two answer different questions and the
102
+ * ordering is the whole content of this one. It is the seam for a renderer that owns real
103
+ * GObject properties — none does today: both ports render DOM or NativeScript views and
104
+ * use {@link subscribe}, so this channel is measured in core alone (its table says so).
105
+ * Re-emitting it verbatim is what a host with real properties has to do, and the reason
106
+ * the order is recorded here rather than re-derived there.
107
+ *
108
+ * The ORDER is not derived, it is measured, and it is not the same on both paths
109
+ * (`~/.cache/gjsify-android/combo-probe2.js`, GJS 1.88.01 / libadwaita 1.9.4):
110
+ *
111
+ * - a `model` write notifies `selected`, `selected-item`, `model` — in that order, and
112
+ * ALL THREE even when `selected` ends on the value it already had;
113
+ * - a `selected` write notifies `selected`, then `selected-item`;
114
+ * - an `items-changed` on the HELD model notifies `selected-item` BEFORE `selected`.
115
+ *
116
+ * The third is the inverse of the second, which is why no single rule is written here.
117
+ */
118
+ subscribeNotify(listener: ComboNotifyListener): () => void;
69
119
  /**
70
120
  * Subscribe to MODEL changes — `Gio.ListModel::items-changed`, one splice per
71
121
  * {@link setModel}. Returns an unsubscribe function.
@@ -78,61 +128,112 @@ export declare class ComboState {
78
128
  */
79
129
  subscribeItems(listener: ComboItemsListener): () => void;
80
130
  private _emit;
131
+ /** Deliver one `notify::`, snapshotted for the reason `_emit` states. */
132
+ private _notify;
133
+ /** Deliver one `items-changed`, snapshotted for the reason `_emit` states. */
134
+ private _emitItems;
81
135
  private _setIndex;
82
136
  /**
83
- * The list model — `Adw.ComboRow:model` / `Gtk.DropDown:model`, in portable form.
137
+ * The list model — `Adw.ComboRow:model` / `Gtk.DropDown:model`, by IDENTITY.
84
138
  *
85
- * A COPY, items included, and that is load-bearing rather than defensive. The splice
86
- * (ADR 0046) decides what changed by COMPARING the assigned model against the stored
87
- * one, so anything that leaks a reference into `_options` makes the comparison read the
88
- * new value on both sides and answer "nothing changed". Handing back `_options` itself
89
- * did exactly that, and it turned a pattern that WORKED under the old full rebuild into
90
- * a silent no-op:
139
+ * The OBJECT that was assigned, never a copy and never a snapshot of its items: ADR 0122
140
+ * § 4 makes `model` an object-kind property over a real `Gio.ListModel`, and `row.model
141
+ * === model` is measured to hold in libadwaita. The copy this getter used to hand back
142
+ * belonged to the Array era, where the model WAS its items and the splice was computed by
143
+ * comparing two arrays — which made `const m = row.model; m.push(x); row.model = m` a
144
+ * silent no-op the copy existed to prevent. A live model has no such round trip: items go
145
+ * in through the model's own API, and it says so itself with `items-changed`.
91
146
  *
92
- * ```ts
93
- * const m = row.model; m.push(item); row.model = m; // → no splice, DOM unchanged
94
- * ```
147
+ * The normalised `{ value, label }` descriptors moved to {@link options}, which is what a
148
+ * renderer drawing the list wants and is still a copy.
149
+ */
150
+ get model(): ComboWatchableModel | null;
151
+ /**
152
+ * The held model's items as the normalised `{ value, label }` descriptors (ADR 0046's item
153
+ * vocabulary), re-read whenever the model says it changed.
95
154
  *
96
- * Fresh items and not just a fresh array, because the same failure sits one level down:
97
- * `m[0].label = 'x'` through a shared descriptor mutates the stored one too, and the
98
- * comparison is by BOTH halves. A frozen item would have made the mutation loud; a copy
99
- * makes the round trip CORRECT, which is the difference between documenting the hazard
100
- * and removing it.
155
+ * A COPY, items included, for the reason the cache exists: `_options` is what every label
156
+ * lookup on this class reads, so a caller that retained it and mutated an item would move
157
+ * the row's inline text without the model knowing. The copy is what keeps the model the
158
+ * single source — mutate the MODEL and the descriptors follow, which is the whole point of
159
+ * holding one.
101
160
  *
102
161
  * Cost, MEASURED rather than assumed (Node 24.19, `{value,label}` records): linear, about
103
162
  * four nanoseconds per item — so a read is well under a microsecond at every size a combo
104
- * row actually holds, and the assignment beside it already costs more. What the shape
105
- * rules out is a per-item read: the three call sites that had one — `_applyItemsChanged`
106
- * in each browser selector and `_filter` in `<gtk-drop-down>`, which runs per row per
107
- * keystroke — hoist it out of their loop, so a read is O(n) once per operation instead of
108
- * O(n²). Nothing reads it per frame. A model large enough for the linear term to matter is
109
- * a `Gtk.ListView` and belongs to `ListController`'s virtualisation, which ADR 0046 § 7
110
- * keeps out of this class on purpose.
163
+ * row actually holds. What the shape rules out is a per-item read: the call sites that had
164
+ * one — `_applyItemsChanged` in each browser selector, `_filter` in `<gtk-drop-down>` which
165
+ * runs per row per keystroke — hoist it out of their loop, so a read is O(n) once per
166
+ * operation instead of O(n²). Nothing reads it per frame. A model large enough for the
167
+ * linear term to matter is a `Gtk.ListView` and belongs to `ListController`'s
168
+ * virtualisation, which ADR 0046 § 7 keeps out of this class on purpose.
169
+ */
170
+ get options(): AdwComboOption[];
171
+ /**
172
+ * The selected ITEM — `Adw.ComboRow:selected-item` / `Gtk.DropDown:selected-item`.
173
+ *
174
+ * The model's OWN item at {@link selectedIndex}, read through `get_item`, so
175
+ * `row.selectedItem === model.get_item(row.selectedIndex)` the way it is measured to hold
176
+ * in libadwaita. Not a descriptor: the item is whatever the model holds — a
177
+ * `GjsifyOptionObject`, a `Gtk.StringObject`, an application object — and handing back the
178
+ * normalised `{ value, label }` instead would make the identity comparison every
179
+ * `selected-item` consumer writes fail. {@link selectedValue} / {@link selectedLabel} are
180
+ * the descriptor side of the same question.
181
+ *
182
+ * `null` for the sentinel, for no model, and for an item the model declines to hand over.
183
+ */
184
+ get selectedItem(): unknown;
185
+ /**
186
+ * Replace the model — the `model` property write of ADR 0122 § 5.
187
+ *
188
+ * Assigning the model the state ALREADY holds does nothing at all: no splice, no selection
189
+ * emit, no `notify::`. Identity is the test, because identity is now what the property
190
+ * holds — two distinct models with equal contents are two different values of `model`, and
191
+ * GTK treats them that way.
192
+ *
193
+ * Otherwise the state detaches from the old model's `items-changed`, attaches to the new
194
+ * one's, re-reads the descriptors, and RE-RUNS AUTOSELECT: the selection lands on 0, or on
195
+ * {@link ADW_COMBO_NO_SELECTION} when the new model is empty. Not a clamp of the old index
196
+ * — measured, a replacement resets the selection to 0 even when the old index exists in the
197
+ * new model (`combo-probe2.js`: selected 2, new model of 3 → selected 0).
198
+ *
199
+ * The `notify::` set is measured and is NOT conditional on anything having changed: a
200
+ * non-null model notifies `selected`, `selected-item`, `model` in that order, even when the
201
+ * selection ends on the value it already had; `model = null` notifies `model` ALONE, and
202
+ * the selection goes to the sentinel silently.
203
+ *
204
+ * The splice ({@link subscribeItems}) is emitted first and only when the descriptors
205
+ * actually differ, because that channel is a renderer's instruction to move item views and
206
+ * an empty instruction is noise. It is NOT `Gio.ListModel::items-changed` — no model emits
207
+ * one when a WIDGET swaps which model it points at — it is this class telling its renderer
208
+ * what the swap did to the drawn list.
111
209
  */
112
- get model(): AdwComboOption[];
210
+ setModel(model: ComboWatchableModel | null): void;
113
211
  /**
114
- * Replace the model: emit the splice that turns the old one into the new
115
- * ({@link subscribeItems}), then notify the SELECTION (`interactive: false`) so the
116
- * renderer re-syncs the inline value label — which may change even at an unchanged
117
- * index, because the label is read out of the model.
118
- *
119
- * Replacing the model re-runs autoselect ({@link clampListSelection}), so an index the
120
- * new one does not have falls back to 0 — including the sentinel, which is how a row
121
- * recovers a selection when its model grows. An EMPTY model has no 0 to fall back to
122
- * and lands on {@link ADW_COMBO_NO_SELECTION}.
123
- *
124
- * The selection notify is UNCONDITIONAL while the splice is not: an assignment of an
125
- * equal model emits no `items-changed` (GTK emits none either) but still re-runs the
126
- * label sync, because that is the behaviour every renderer has depended on since this
127
- * class existed and nothing measured says it is wrong.
212
+ * The held model moved under us — `Gio.ListModel::items-changed`, forwarded to the
213
+ * renderer and then applied to the selection the way `GtkSingleSelection` applies it.
214
+ *
215
+ * Four cases, all MEASURED (`combo-probe2.js`, GJS 1.88.01 / GTK 4.22.5), and the notify
216
+ * ORDER here is the inverse of the `selected`-write path — `selected-item` comes FIRST,
217
+ * because the item under the selection changed before the index did:
218
+ *
219
+ * - nothing selected → autoselect takes 0 as soon as the model has one, notifying
220
+ * `selected-item` then `selected`;
221
+ * - the change sits entirely AFTER the selection → the index still addresses the same
222
+ * item, no notify at all;
223
+ * - entirely BEFORE it → the item did not change, only its position did, so the index
224
+ * shifts by `added - removed` and ONLY `selected` notifies;
225
+ * - the change COVERS the selection → the selected item is gone, so the index clamps to
226
+ * `min(position, n_items - 1)` (the sentinel for an emptied model) and `selected-item`
227
+ * notifies whether or not the index moved — which is what makes a replace-in-place
228
+ * (`removed: 1, added: 1` at the selection) notify `selected-item` alone.
128
229
  */
129
- setModel(model: AdwComboOption[]): void;
230
+ private _onItemsChanged;
130
231
  /**
131
- * The selected option index, or {@link ADW_COMBO_NO_SELECTION}. May also sit PAST the
132
- * options length: {@link setSelectedIndex} is deliberately permissive, mirroring a
133
- * `guint` property that takes any position. {@link selectedValue} reads `''` in both
134
- * cases, which is why the sentinel is what distinguishes "nothing selected" from "an
135
- * empty label".
232
+ * The selected option index, or {@link ADW_COMBO_NO_SELECTION}.
233
+ *
234
+ * Always either the sentinel or a position the model HAS: every door that writes it
235
+ * ({@link setSelectedIndex}, {@link select}, autoselect) refuses an index the model does
236
+ * not address, so this is the one place "nothing selected" is spelled.
136
237
  */
137
238
  get selectedIndex(): number;
138
239
  /**
@@ -142,7 +243,16 @@ export declare class ComboState {
142
243
  * nothing.
143
244
  */
144
245
  get presentsChooser(): boolean;
145
- /** Programmatic index set — notifies `interactive: false`. Returns whether it changed. */
246
+ /**
247
+ * Programmatic index set — notifies `interactive: false`. Returns whether it changed.
248
+ *
249
+ * An index the model does not have is IGNORED, silently: no change, no notify, no error.
250
+ * That is measured on both widgets (`combo-probe2.js`: `selected = 5` on a model of 3, and
251
+ * `selected = GTK_INVALID_LIST_POSITION` on a non-empty model, each leave `selected` where
252
+ * it was and emit nothing). `Adw.ComboRow` forwards the position to
253
+ * `gtk_single_selection_set_selected`, which validates it — so this is not a renderer
254
+ * policy and no renderer states its own answer any more.
255
+ */
146
256
  setSelectedIndex(index: number): boolean;
147
257
  /** The selected option's `value`, or `''` when out of range. */
148
258
  get selectedValue(): string;
@@ -151,17 +261,13 @@ export declare class ComboState {
151
261
  /** Programmatic value set — moves the index to the first option with this `value`. No-op for an unknown value. */
152
262
  setSelectedValue(value: string): boolean;
153
263
  /**
154
- * Whether `index` addresses an option that exists.
155
- *
156
- * The bounds arithmetic lives here; the POLICY for a programmatic out-of-range set does
157
- * not, because this tree cannot settle it: `adw_combo_row_set_selected` forwards the
158
- * position straight to `gtk_single_selection_set_selected` and documents
159
- * `GTK_INVALID_LIST_POSITION` as a legal argument, but `refs/gtk` is EMPTY so what
160
- * GtkSingleSelection does with it is not verifiable here. Each renderer therefore states
161
- * its own answer against this one predicate: {@link setSelectedIndex} keeps the permissive
162
- * model (an index past the end is how "nothing selected" is spelled and
163
- * {@link selectedValue} reads `''`), while `<gtk-drop-down>` rejects the set, as its
164
- * published `selected` docs promise.
264
+ * Whether `index` addresses an item the held model has.
265
+ *
266
+ * The ONE bounds predicate: {@link setSelectedIndex} and {@link select} both refuse what it
267
+ * refuses, so a programmatic set and a user pick agree about what is out of range. They
268
+ * once did not — `refs/gtk` is empty, so what `gtk_single_selection_set_selected` does with
269
+ * `GTK_INVALID_LIST_POSITION` was not verifiable in this tree and each renderer stated its
270
+ * own answer. It is now measured (see {@link setSelectedIndex}) and there is one.
165
271
  */
166
272
  hasIndex(index: number): boolean;
167
273
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/adwaita-core",
3
- "version": "0.64.12-next.281",
3
+ "version": "0.64.12",
4
4
  "description": "Headless Adwaita widget behavior (breakpoint condition grammar + apply/unapply state machine, color-scheme observable, avatar derivation) shared by the web and NativeScript Adwaita renderers — pure TS, no platform imports",
5
5
  "type": "module",
6
6
  "module": "lib/esm/index.js",
@@ -67,9 +67,9 @@
67
67
  "headless"
68
68
  ],
69
69
  "devDependencies": {
70
- "@gjsify/cli": "0.64.12-next.281",
70
+ "@gjsify/cli": "^0.64.12",
71
71
  "@gjsify/node-gi": "file:../../../packages/node-gi/node-gi",
72
- "@gjsify/unit": "0.64.12-next.281",
72
+ "@gjsify/unit": "^0.64.12",
73
73
  "@types/node": "^25.9.2",
74
74
  "typescript": "^6.0.3"
75
75
  },
@@ -92,6 +92,5 @@
92
92
  "bugs": {
93
93
  "url": "https://github.com/gjsify/gjsify/issues"
94
94
  },
95
- "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-core#readme",
96
- "gitHead": "c4d2e266c0d9849860b9828c14ebfe37b6d493b8"
95
+ "homepage": "https://github.com/gjsify/gjsify/tree/main/packages/web/adwaita-core#readme"
97
96
  }