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

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.
Files changed (46) hide show
  1. package/lib/esm/action-row.js +1 -1
  2. package/lib/esm/conformance/action-row.js +1 -1
  3. package/lib/esm/conformance/expander-row.js +1 -0
  4. package/lib/esm/conformance/index.js +1 -1
  5. package/lib/esm/conformance/label-markup.js +1 -0
  6. package/lib/esm/conformance/label.js +1 -1
  7. package/lib/esm/conformance/list.js +1 -1
  8. package/lib/esm/conformance/rows.js +1 -1
  9. package/lib/esm/conformance/split-button.js +1 -1
  10. package/lib/esm/default-translate.js +1 -0
  11. package/lib/esm/glib.js +1 -1
  12. package/lib/esm/gtk-platform.js +1 -1
  13. package/lib/esm/index.js +1 -1
  14. package/lib/esm/label-markup.js +1 -0
  15. package/lib/esm/label.js +1 -1
  16. package/lib/esm/list.js +1 -1
  17. package/lib/esm/option-list.js +1 -0
  18. package/lib/esm/pango-markup.js +1 -1
  19. package/lib/esm/rows.js +1 -1
  20. package/lib/esm/split-button.js +1 -1
  21. package/lib/types/action-row.d.ts +5 -1
  22. package/lib/types/conformance/action-row.d.ts +28 -0
  23. package/lib/types/conformance/expander-row.d.ts +40 -0
  24. package/lib/types/conformance/index.d.ts +10 -6
  25. package/lib/types/conformance/label-markup.d.ts +73 -0
  26. package/lib/types/conformance/list.d.ts +46 -39
  27. package/lib/types/conformance/rows.d.ts +62 -2
  28. package/lib/types/conformance/split-button.d.ts +7 -3
  29. package/lib/types/default-translate.d.ts +6 -0
  30. package/lib/types/default-translate.spec.d.ts +2 -0
  31. package/lib/types/engine.d.ts +40 -6
  32. package/lib/types/glib.d.ts +40 -0
  33. package/lib/types/glib.spec.d.ts +2 -0
  34. package/lib/types/gtk-platform.d.ts +9 -0
  35. package/lib/types/index.d.ts +13 -7
  36. package/lib/types/label-markup.d.ts +129 -0
  37. package/lib/types/label-markup.spec.d.ts +2 -0
  38. package/lib/types/label.d.ts +6 -0
  39. package/lib/types/list-view.d.ts +4 -4
  40. package/lib/types/list.d.ts +9 -23
  41. package/lib/types/option-list.d.ts +128 -0
  42. package/lib/types/option-list.spec.d.ts +2 -0
  43. package/lib/types/pango-markup.d.ts +47 -2
  44. package/lib/types/rows.d.ts +193 -65
  45. package/lib/types/split-button.d.ts +28 -6
  46. package/package.json +4 -4
@@ -32,13 +32,16 @@ export interface AdwListModelSource {
32
32
  get_item(position: number): unknown;
33
33
  }
34
34
  /**
35
- * What a `model` property ACCEPTS: the widened form of {@link AdwListModel}, plus a real
36
- * {@link AdwListModelSource}.
35
+ * What {@link normalizeComboOptions} ACCEPTS: the widened form of {@link AdwListModel}, plus a
36
+ * real {@link AdwListModelSource}.
37
37
  *
38
- * The model arm is new in ADR 0122, which made `Gtk.StringList` a `Gio.ListModel` instead of
39
- * an `Array` subclass — the one value that used to satisfy BOTH arms at once. It is not a
40
- * permanent widening: ADR 0122 § 6 narrows `Adw.ComboRow:model` to `Gio.ListModel` alone, and
41
- * the array arm goes with the widget that still reads a markup attribute.
38
+ * IT IS THE NORMALISER'S INPUT TYPE AND NOT A PROPERTY TYPE, which is the whole of what ADR
39
+ * 0122 § 6 changed about it. `Adw.ComboRow:model` and `Gtk.DropDown:model` are a
40
+ * `Gio.ListModel` and nothing else now — an array assigned to either is a `TypeError` naming
41
+ * the type. The union stays because the AUTHORED vocabulary is still both: a JSON markup
42
+ * attribute parses to an array, a `Gtk.StringList` handed to `GjsifyOptionList` is a model,
43
+ * and both arrive here on their way to becoming descriptors. The arms are the two forms a
44
+ * caller can WRITE DOWN, not the two values a property will take.
42
45
  */
43
46
  export type AdwListModelInput = ReadonlyArray<AdwComboOptionInput> | AdwListModelSource;
44
47
  /**
@@ -83,23 +86,6 @@ export declare function parseListModel(raw: string | null | undefined): AdwCombo
83
86
  * draw a placeholder has nothing to test.
84
87
  */
85
88
  export declare const ADW_COMBO_NO_SELECTION = -1;
86
- /**
87
- * Where a selection lands after the model under it was REPLACED.
88
- *
89
- * `GtkSingleSelection` autoselects, so a position the new model does not have falls back
90
- * to 0 — including the sentinel, which is how a widget recovers a selection when its model
91
- * grows. An EMPTY model has no 0 to fall back to and lands on
92
- * {@link ADW_COMBO_NO_SELECTION}.
93
- *
94
- * The test is membership and not `selected >= length`: the sentinel is BELOW the range, so
95
- * a one-sided comparison leaves a `-1` in place and a model that was emptied and refilled
96
- * comes back with nothing selected.
97
- *
98
- * Lifted out of `ComboState.setModel` so a second list widget cannot re-derive it. The
99
- * cross-renderer rule it encodes was already conformance data — `conformance/rows.ts`
100
- * carries it as "the ports' shared answer" — it just had no name of its own.
101
- */
102
- export declare function clampListSelection(selected: number, length: number): number;
103
89
  /**
104
90
  * One positional change — `Gio.ListModel::items-changed`'s own three parameters.
105
91
  *
@@ -0,0 +1,128 @@
1
+ import { GObjectObject, type GType } from './gobject.js';
2
+ import { type AdwComboOption, type AdwComboOptionInput } from './list.js';
3
+ import type { ComboWatchableModel } from './rows.js';
4
+ /**
5
+ * One selectable item, as the `GObject` a `GListModel` is allowed to hand out.
6
+ *
7
+ * Both properties are READ-ONLY, and that is not caution: an item is what a model contains, and
8
+ * a model reports a content change through `items-changed`. A writable `label` would let the
9
+ * text under a selection move with nothing emitted, which is the failure
10
+ * `GtkStringObject`'s read-only `string` exists to prevent.
11
+ *
12
+ * It deliberately has NO `string` property. `readListModel` unwraps an item that has one as a
13
+ * bare string — that is `Gtk.StringObject`'s shape — and an option that answered to it would
14
+ * lose its `value` on the way out, which is the whole reason this type exists.
15
+ */
16
+ export declare class GjsifyOptionObject extends GObjectObject {
17
+ constructor(props?: Record<string, unknown> | null);
18
+ /** An item holding `value` and `label` — the only way to put either in one. */
19
+ static new(value: string, label: string): GjsifyOptionObject;
20
+ /** An item for one authored entry, through the normaliser every surface already shares. */
21
+ static fromInput(entry: AdwComboOptionInput): GjsifyOptionObject;
22
+ /** The value this item is addressed by. */
23
+ get value(): string;
24
+ set value(_value: string);
25
+ /** The text this item draws as. */
26
+ get label(): string;
27
+ set label(_value: string);
28
+ /** The descriptor this item holds, as a COPY — nothing hands out the stored record. */
29
+ toOption(): AdwComboOption;
30
+ }
31
+ /** What `new GjsifyOptionList({ … })` takes. */
32
+ export interface GjsifyOptionListProps {
33
+ /** The options the list starts with, in any form {@link normalizeComboOptions} accepts. */
34
+ options?: readonly AdwComboOptionInput[] | null;
35
+ }
36
+ /**
37
+ * A `Gio.ListModel` of {@link GjsifyOptionObject}s — what a `model` property holds once the
38
+ * array door is closed.
39
+ *
40
+ * `new GjsifyOptionList()` is empty. The instance is a GObject with a real identity, so a
41
+ * `model` write that assigns the SAME list again is the no-op GTK makes it, and
42
+ * `row.model === list` answers true.
43
+ */
44
+ export declare class GjsifyOptionList extends GObjectObject {
45
+ /** Installed by the core from {@link ListModel}; declared here for the type alone. */
46
+ readonly get_item: (position: number) => GjsifyOptionObject | null;
47
+ readonly get_item_type: () => GType;
48
+ readonly get_n_items: () => number;
49
+ readonly items_changed: (position: number, removed: number, added: number) => void;
50
+ constructor(props?: GjsifyOptionListProps | null);
51
+ /** A list holding `options`, or an empty one for `null` — the markup door's constructor. */
52
+ static new(options: readonly AdwComboOptionInput[] | null): GjsifyOptionList;
53
+ /** The item type, which here is the specific one: this list holds nothing else. */
54
+ get item_type(): GType;
55
+ set item_type(_value: GType);
56
+ get itemType(): GType;
57
+ set itemType(_value: GType);
58
+ /** `n-items` — the `ParamSpec`'s reader, and `get_n_items`'s answer. */
59
+ get n_items(): number;
60
+ set n_items(_value: number);
61
+ /** `vfunc_get_item_type` — what the interface's `get_item_type()` calls through to. */
62
+ vfunc_get_item_type(): GType;
63
+ /** `vfunc_get_n_items`. */
64
+ vfunc_get_n_items(): number;
65
+ /** `vfunc_get_item` — `null` past the end, as `g_list_model_get_item` returns `NULL`. */
66
+ vfunc_get_item(position: number): GjsifyOptionObject | null;
67
+ /** The descriptors this list holds, as copies — the form every renderer draws from. */
68
+ toOptions(): AdwComboOption[];
69
+ /** One more option at the end. */
70
+ append(option: AdwComboOptionInput): void;
71
+ /** The option at `position`, which must be in range. */
72
+ remove(position: number): void;
73
+ /**
74
+ * `g_list_store_splice(position, n_removals, additions)` — the C signature, all three
75
+ * arguments, additions as ONE array, so a hundred-item replacement is one emission.
76
+ *
77
+ * Out of range is a CRITICAL and a no-op rather than an exception, which is the shape
78
+ * `Gtk.StringList` measured for the same call: this door is reachable from a markup
79
+ * attribute, and an author's slip must not take the widget down.
80
+ */
81
+ splice(position: number, nRemovals: number, additions: readonly AdwComboOptionInput[] | null): void;
82
+ /**
83
+ * The POSITION of the first option with this value, {@link INVALID_LIST_POSITION} when there
84
+ * is none — `gtk_string_list_find`'s meaning of the name, over the half a descriptor is
85
+ * addressed by.
86
+ */
87
+ find(value: string): number;
88
+ /**
89
+ * Replace the contents with `next` IN PLACE, as the ONE splice that turns one into the
90
+ * other — {@link listItemsChanged}'s minimal range.
91
+ *
92
+ * This is the markup door's update path and it keeps the list's IDENTITY, which is what
93
+ * makes it different from a `model` write. Re-parsing an attribute must not look to the
94
+ * widget like a new model: a model REPLACEMENT re-runs autoselect and resets the selection
95
+ * to 0 (ADR 0122 § 4, measured), while appending one option to the end of the list the
96
+ * widget already holds moves nothing. An author editing `model='[…]'` means the second.
97
+ */
98
+ reconcile(next: readonly AdwComboOptionInput[] | null): void;
99
+ /** The one mutation, so the emission rule is written once. See the file header. */
100
+ private spliceItems;
101
+ }
102
+ /**
103
+ * What a write to a `model` property reads as — the ONE door, for all four surfaces.
104
+ *
105
+ * ADR 0122 § 6 makes `model` an object property over `Gio.ListModel`, and § 5 leaves the JSON
106
+ * markup attribute open, so every `model` setter in this repository has the same three-way
107
+ * answer to give. It is written here once because it was about to be written four times:
108
+ *
109
+ * - a real model — anything with `get_n_items` and `get_item`, which is what
110
+ * {@link AdwListModelSource}'s docblock argues is the test — is handed through by IDENTITY,
111
+ * so `widget.model === model` holds as it is measured to in libadwaita;
112
+ * - `null` / `undefined` is the "no model" write, which notifies `model` alone;
113
+ * - a STRING is the markup attribute, parsed and reconciled into `authored` — the surface's
114
+ * OWN list, whose identity survives the edit so an author appending one option to
115
+ * `model='[…]'` gets one splice instead of a replacement that resets the selection;
116
+ * - anything else, an ARRAY above all, is a `TypeError`.
117
+ *
118
+ * THE REFUSAL IS THE PROPERTY'S, not a second opinion: the message comes out of
119
+ * {@link checkPropertyValue} against `model`'s own ParamSpec, which is where GJS's measured
120
+ * wording lives. Reaching it is already the failure case, and it always throws for a value that
121
+ * gets that far — the one thing it accepts is a registered implementor of `Gio.ListModel`, and
122
+ * the interface installs the two getters the duck test above just failed to find.
123
+ *
124
+ * The duck test and not `checkPropertyValue` alone, because a REAL `Gio.ListStore` reached
125
+ * through node-gi (ADR 0104) is no registered class of this subset and `implementsInterface`
126
+ * would refuse it — and handing one to `Adw.ComboRow:model` is the whole point of having it.
127
+ */
128
+ export declare function readModelWrite(value: unknown, authored: GjsifyOptionList): ComboWatchableModel | null;
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -17,6 +17,29 @@ export type PangoMarkupNode = {
17
17
  readonly attributes: Readonly<Record<string, string>>;
18
18
  readonly children: readonly PangoMarkupNode[];
19
19
  };
20
+ /**
21
+ * A piece of LABEL markup: {@link PangoMarkupNode}, plus the `<a href>` run pango knows
22
+ * nothing about. Every `PangoMarkupNode` IS a `LabelMarkupNode` — the link kind is the
23
+ * only addition — so a renderer written against this type draws both trees.
24
+ */
25
+ export type LabelMarkupNode = {
26
+ readonly kind: 'text';
27
+ readonly text: string;
28
+ } | {
29
+ readonly kind: 'element';
30
+ readonly tag: PangoMarkupTag;
31
+ readonly attributes: Readonly<Record<string, string>>;
32
+ readonly children: readonly LabelMarkupNode[];
33
+ } | {
34
+ readonly kind: 'link';
35
+ /** `<a>`'s required `href`, entities resolved. */
36
+ readonly href: string;
37
+ /** `<a title>`, which GTK shows as the label's tooltip; `null` when absent. */
38
+ readonly title: string | null;
39
+ /** `<a class>`, the CSS class GTK puts on the link's own node; `null` when absent. */
40
+ readonly cssClass: string | null;
41
+ readonly children: readonly LabelMarkupNode[];
42
+ };
20
43
  /**
21
44
  * `g_markup_escape_text` — the escape `create_search_row_subtitle` puts a page title
22
45
  * through before it becomes a markup subtitle. GLib emits NUMERIC references for the
@@ -50,10 +73,32 @@ export declare function escapeMarkup(text: string): string;
50
73
  * discarded).
51
74
  */
52
75
  export declare function parsePangoMarkup(text: string): PangoMarkupNode[] | null;
76
+ /**
77
+ * `gtk_label_set_markup`'s whole parse: pango's markup PLUS `<a href>`, which is
78
+ * `Gtk.Label`'s own extension and not a pango tag at all.
79
+ *
80
+ * Same `null`-never-throws contract as {@link parsePangoMarkup}, and the same failures,
81
+ * with `<a>`'s two added: an attribute other than `href` / `title` / `class`
82
+ * (`G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE`) and a missing `href`
83
+ * (`G_MARKUP_ERROR_INVALID_CONTENT`).
84
+ *
85
+ * USE THIS ONLY WHERE GTK ITSELF PARSES URI MARKUP — `Gtk.Label`, and the widgets that
86
+ * are a label (`Gtk.Inscription` is NOT one: it has no `::activate-link`). Everything
87
+ * else — the search pipeline's `stripMarkup`, `Adw.Banner`, `Adw.AlertDialog` — stays on
88
+ * {@link parsePangoMarkup}, where `<a>` is an unknown tag and the string falls back to
89
+ * text, because that is what those surfaces do in the C.
90
+ */
91
+ export declare function parseLabelMarkup(text: string): LabelMarkupNode[] | null;
53
92
  /**
54
93
  * The plain text of markup — {@link parsePangoMarkup} reduced to its text runs, and
55
94
  * `null` under exactly the same failures. This is what `make_comparable` compares, what
56
- * `<gtk-label>`/`<gtk-inscription>` show for `use-markup`, and what the C shows when
57
- * markup does not parse.
95
+ * `<gtk-inscription>` shows for `markup`, and what the C shows when markup does not parse.
58
96
  */
59
97
  export declare function stripMarkup(text: string): string | null;
98
+ /**
99
+ * The plain text of LABEL markup — {@link parseLabelMarkup} reduced the same way, so a
100
+ * `<a href="…">GTK</a>` shows as `GTK` and not as the raw string. This is what
101
+ * `gtk_label_get_text` returns: `parse_uri_markup` strips the `<a>` elements out before
102
+ * pango ever sees the string, and the text GTK keeps is the remainder.
103
+ */
104
+ export declare function stripLabelMarkup(text: string): string | null;
@@ -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';
@@ -6,21 +6,43 @@ export type { SpinStateChange, SpinStateListener, SpinStateRangeListener } from
6
6
  /** Subscriber for {@link ExpanderState} changes — receives the new expanded flag. */
7
7
  export type ExpanderStateListener = (expanded: boolean) => void;
8
8
  /**
9
- * The disclosure state of an expander row: a single boolean that toggles between
10
- * revealed and collapsed. Idempotent — setting the current value again is a no-op
11
- * and fires no notification. Both programmatic and interactive changes notify (as
12
- * `Adw.ExpanderRow` does).
9
+ * The `notify::*` an expander-row write emits, in emission order. A renderer that owns real
10
+ * GObject properties re-emits it verbatim; `expanded` leads `enable-expansion` because the
11
+ * C setter cross-writes `expanded` before it notifies its own property.
12
+ */
13
+ export type ExpanderNotify = 'expanded' | 'enable-expansion';
14
+ /**
15
+ * The state of an expander row: whether the disclosure is revealed, and whether it may be.
16
+ *
17
+ * The two are coupled the way `adw-expander-row.c` couples them. `setExpanded` coerces
18
+ * `expanded && enableExpansion` before its equal-return (C:264), so opening a disabled row is
19
+ * a silent no-op rather than a refusal. `setEnableExpansion` stores, drives `expanded` to the
20
+ * new value (C:310) and only then notifies its own property — enabling EXPANDS, disabling
21
+ * COLLAPSES. Idempotent — setting a current value again fires nothing. Both programmatic and
22
+ * interactive changes notify (as `Adw.ExpanderRow` does).
13
23
  */
14
24
  export declare class ExpanderState {
15
25
  private _expanded;
26
+ private _enableExpansion;
16
27
  private readonly _listeners;
17
28
  /** Subscribe to expanded-state changes. Returns an unsubscribe function. */
18
29
  subscribe(listener: ExpanderStateListener): () => void;
19
30
  private _emit;
20
31
  /** Whether the disclosure is revealed. */
21
32
  get expanded(): boolean;
22
- /** Set the expanded flag. Notifies only on an actual change. Returns whether it changed. */
33
+ /** Whether the row can be expanded at all. */
34
+ get enableExpansion(): boolean;
35
+ /**
36
+ * Set the expanded flag, held `false` while expansion is disabled. Notifies only on an
37
+ * actual change. Returns whether it changed.
38
+ */
23
39
  setExpanded(value: boolean): boolean;
40
+ /**
41
+ * Set `enable-expansion` and drive `expanded` to the same value. Returns the properties
42
+ * the write notifies, in order: empty for an equal write, `['enable-expansion']` when
43
+ * `expanded` already held the value, `['expanded', 'enable-expansion']` otherwise.
44
+ */
45
+ setEnableExpansion(value: boolean): readonly ExpanderNotify[];
24
46
  /** Flip expanded↔collapsed (the disclosure tap). Returns whether it changed. */
25
47
  toggle(): boolean;
26
48
  /** Reveal the disclosure. Returns whether it changed. */
@@ -43,6 +65,29 @@ export interface ComboStateChange {
43
65
  export type ComboStateListener = (change: ComboStateChange) => void;
44
66
  /** Subscriber for {@link ComboState} MODEL changes — one `items-changed` per replacement. */
45
67
  export type ComboItemsListener = (change: AdwListItemsChanged) => void;
68
+ /**
69
+ * A GObject property of `Adw.ComboRow` / `Gtk.DropDown` whose `notify::` a renderer owes its
70
+ * widget — the three ADR 0122 § 5's table is written in terms of.
71
+ */
72
+ export type ComboNotify = 'selected' | 'selected-item' | 'model';
73
+ /** Subscriber for the `notify::*` a {@link ComboState} transition owes, in emission ORDER. */
74
+ export type ComboNotifyListener = (property: ComboNotify) => void;
75
+ /**
76
+ * What {@link ComboState} needs off a model to WATCH it, beyond reading it.
77
+ *
78
+ * Optional, and duck-typed for the reason `AdwListModelSource` is: a real `Gio.ListModel` has
79
+ * `connect`, and so does every model this repository builds, but the reader in `list.ts` accepts
80
+ * anything with the two getters and a state machine that DEMANDED a signal surface would refuse
81
+ * models it can otherwise read perfectly.
82
+ *
83
+ * EXPORTED because it is the published type of a `model` property now (ADR 0122 § 4). Left
84
+ * un-exported it still reached the `.d.ts`, so a consumer could satisfy it and not NAME it —
85
+ * which is the shape that makes a caller declare its own copy.
86
+ */
87
+ export interface ComboWatchableModel extends AdwListModelSource {
88
+ connect?: (signal: string, handler: (...args: unknown[]) => void) => number;
89
+ disconnect?: (id: number) => void;
90
+ }
46
91
  /**
47
92
  * The selection state of a combo row: a portable list model (`AdwListModel`) plus
48
93
  * the selected index, with a two-way index↔value mapping and empty/out-of-range guards.
@@ -60,12 +105,39 @@ export type ComboItemsListener = (change: AdwListItemsChanged) => void;
60
105
  * assignment.
61
106
  */
62
107
  export declare class ComboState {
108
+ /** The model's IDENTITY — the object `model` hands back, never a copy (ADR 0122 § 4). */
109
+ private _model;
110
+ /** The descriptors the held model holds, re-read whenever it says it changed. */
63
111
  private _options;
64
112
  private _selected;
113
+ /** Detaches this state from the held model's `items-changed`. */
114
+ private _unwatch;
65
115
  private readonly _listeners;
66
116
  private readonly _itemListeners;
117
+ private readonly _notifyListeners;
67
118
  /** Subscribe to selection changes. Returns an unsubscribe function. */
68
119
  subscribe(listener: ComboStateListener): () => void;
120
+ /**
121
+ * Subscribe to the `notify::*` each transition owes, delivered one at a time IN ORDER.
122
+ *
123
+ * Separate from {@link subscribe} because the two answer different questions and the
124
+ * ordering is the whole content of this one. It is the seam for a renderer that owns real
125
+ * GObject properties — none does today: both ports render DOM or NativeScript views and
126
+ * use {@link subscribe}, so this channel is measured in core alone (its table says so).
127
+ * Re-emitting it verbatim is what a host with real properties has to do, and the reason
128
+ * the order is recorded here rather than re-derived there.
129
+ *
130
+ * The ORDER is not derived, it is measured, and it is not the same on both paths
131
+ * (`~/.cache/gjsify-android/combo-probe2.js`, GJS 1.88.01 / libadwaita 1.9.4):
132
+ *
133
+ * - a `model` write notifies `selected`, `selected-item`, `model` — in that order, and
134
+ * ALL THREE even when `selected` ends on the value it already had;
135
+ * - a `selected` write notifies `selected`, then `selected-item`;
136
+ * - an `items-changed` on the HELD model notifies `selected-item` BEFORE `selected`.
137
+ *
138
+ * The third is the inverse of the second, which is why no single rule is written here.
139
+ */
140
+ subscribeNotify(listener: ComboNotifyListener): () => void;
69
141
  /**
70
142
  * Subscribe to MODEL changes — `Gio.ListModel::items-changed`, one splice per
71
143
  * {@link setModel}. Returns an unsubscribe function.
@@ -78,61 +150,112 @@ export declare class ComboState {
78
150
  */
79
151
  subscribeItems(listener: ComboItemsListener): () => void;
80
152
  private _emit;
153
+ /** Deliver one `notify::`, snapshotted for the reason `_emit` states. */
154
+ private _notify;
155
+ /** Deliver one `items-changed`, snapshotted for the reason `_emit` states. */
156
+ private _emitItems;
81
157
  private _setIndex;
82
158
  /**
83
- * The list model — `Adw.ComboRow:model` / `Gtk.DropDown:model`, in portable form.
159
+ * The list model — `Adw.ComboRow:model` / `Gtk.DropDown:model`, by IDENTITY.
84
160
  *
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:
161
+ * The OBJECT that was assigned, never a copy and never a snapshot of its items: ADR 0122
162
+ * § 4 makes `model` an object-kind property over a real `Gio.ListModel`, and `row.model
163
+ * === model` is measured to hold in libadwaita. The copy this getter used to hand back
164
+ * belonged to the Array era, where the model WAS its items and the splice was computed by
165
+ * comparing two arrays — which made `const m = row.model; m.push(x); row.model = m` a
166
+ * silent no-op the copy existed to prevent. A live model has no such round trip: items go
167
+ * in through the model's own API, and it says so itself with `items-changed`.
91
168
  *
92
- * ```ts
93
- * const m = row.model; m.push(item); row.model = m; // → no splice, DOM unchanged
94
- * ```
169
+ * The normalised `{ value, label }` descriptors moved to {@link options}, which is what a
170
+ * renderer drawing the list wants and is still a copy.
171
+ */
172
+ get model(): ComboWatchableModel | null;
173
+ /**
174
+ * The held model's items as the normalised `{ value, label }` descriptors (ADR 0046's item
175
+ * vocabulary), re-read whenever the model says it changed.
95
176
  *
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.
177
+ * A COPY, items included, for the reason the cache exists: `_options` is what every label
178
+ * lookup on this class reads, so a caller that retained it and mutated an item would move
179
+ * the row's inline text without the model knowing. The copy is what keeps the model the
180
+ * single source — mutate the MODEL and the descriptors follow, which is the whole point of
181
+ * holding one.
101
182
  *
102
183
  * Cost, MEASURED rather than assumed (Node 24.19, `{value,label}` records): linear, about
103
184
  * 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.
111
- */
112
- get model(): AdwComboOption[];
113
- /**
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.
128
- */
129
- setModel(model: AdwComboOption[]): void;
130
- /**
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".
185
+ * row actually holds. What the shape rules out is a per-item read: the call sites that had
186
+ * one — `_applyItemsChanged` in each browser selector, `_filter` in `<gtk-drop-down>` which
187
+ * runs per row per keystroke — hoist it out of their loop, so a read is O(n) once per
188
+ * operation instead of O(n²). Nothing reads it per frame. A model large enough for the
189
+ * linear term to matter is a `Gtk.ListView` and belongs to `ListController`'s
190
+ * virtualisation, which ADR 0046 § 7 keeps out of this class on purpose.
191
+ */
192
+ get options(): AdwComboOption[];
193
+ /**
194
+ * The selected ITEM — `Adw.ComboRow:selected-item` / `Gtk.DropDown:selected-item`.
195
+ *
196
+ * The model's OWN item at {@link selectedIndex}, read through `get_item`, so
197
+ * `row.selectedItem === model.get_item(row.selectedIndex)` the way it is measured to hold
198
+ * in libadwaita. Not a descriptor: the item is whatever the model holds — a
199
+ * `GjsifyOptionObject`, a `Gtk.StringObject`, an application object — and handing back the
200
+ * normalised `{ value, label }` instead would make the identity comparison every
201
+ * `selected-item` consumer writes fail. {@link selectedValue} / {@link selectedLabel} are
202
+ * the descriptor side of the same question.
203
+ *
204
+ * `null` for the sentinel, for no model, and for an item the model declines to hand over.
205
+ */
206
+ get selectedItem(): unknown;
207
+ /**
208
+ * Replace the model — the `model` property write of ADR 0122 § 5.
209
+ *
210
+ * Assigning the model the state ALREADY holds does nothing at all: no splice, no selection
211
+ * emit, no `notify::`. Identity is the test, because identity is now what the property
212
+ * holds — two distinct models with equal contents are two different values of `model`, and
213
+ * GTK treats them that way.
214
+ *
215
+ * Otherwise the state detaches from the old model's `items-changed`, attaches to the new
216
+ * one's, re-reads the descriptors, and RE-RUNS AUTOSELECT: the selection lands on 0, or on
217
+ * {@link ADW_COMBO_NO_SELECTION} when the new model is empty. Not a clamp of the old index
218
+ * — measured, a replacement resets the selection to 0 even when the old index exists in the
219
+ * new model (`combo-probe2.js`: selected 2, new model of 3 → selected 0).
220
+ *
221
+ * The `notify::` set is measured and is NOT conditional on anything having changed: a
222
+ * non-null model notifies `selected`, `selected-item`, `model` in that order, even when the
223
+ * selection ends on the value it already had; `model = null` notifies `model` ALONE, and
224
+ * the selection goes to the sentinel silently.
225
+ *
226
+ * The splice ({@link subscribeItems}) is emitted first and only when the descriptors
227
+ * actually differ, because that channel is a renderer's instruction to move item views and
228
+ * an empty instruction is noise. It is NOT `Gio.ListModel::items-changed` — no model emits
229
+ * one when a WIDGET swaps which model it points at — it is this class telling its renderer
230
+ * what the swap did to the drawn list.
231
+ */
232
+ setModel(model: ComboWatchableModel | null): void;
233
+ /**
234
+ * The held model moved under us — `Gio.ListModel::items-changed`, forwarded to the
235
+ * renderer and then applied to the selection the way `GtkSingleSelection` applies it.
236
+ *
237
+ * Four cases, all MEASURED (`combo-probe2.js`, GJS 1.88.01 / GTK 4.22.5), and the notify
238
+ * ORDER here is the inverse of the `selected`-write path — `selected-item` comes FIRST,
239
+ * because the item under the selection changed before the index did:
240
+ *
241
+ * - nothing selected → autoselect takes 0 as soon as the model has one, notifying
242
+ * `selected-item` then `selected`;
243
+ * - the change sits entirely AFTER the selection → the index still addresses the same
244
+ * item, no notify at all;
245
+ * - entirely BEFORE it → the item did not change, only its position did, so the index
246
+ * shifts by `added - removed` and ONLY `selected` notifies;
247
+ * - the change COVERS the selection → the selected item is gone, so the index clamps to
248
+ * `min(position, n_items - 1)` (the sentinel for an emptied model) and `selected-item`
249
+ * notifies whether or not the index moved — which is what makes a replace-in-place
250
+ * (`removed: 1, added: 1` at the selection) notify `selected-item` alone.
251
+ */
252
+ private _onItemsChanged;
253
+ /**
254
+ * The selected option index, or {@link ADW_COMBO_NO_SELECTION}.
255
+ *
256
+ * Always either the sentinel or a position the model HAS: every door that writes it
257
+ * ({@link setSelectedIndex}, {@link select}, autoselect) refuses an index the model does
258
+ * not address, so this is the one place "nothing selected" is spelled.
136
259
  */
137
260
  get selectedIndex(): number;
138
261
  /**
@@ -142,7 +265,16 @@ export declare class ComboState {
142
265
  * nothing.
143
266
  */
144
267
  get presentsChooser(): boolean;
145
- /** Programmatic index set — notifies `interactive: false`. Returns whether it changed. */
268
+ /**
269
+ * Programmatic index set — notifies `interactive: false`. Returns whether it changed.
270
+ *
271
+ * An index the model does not have is IGNORED, silently: no change, no notify, no error.
272
+ * That is measured on both widgets (`combo-probe2.js`: `selected = 5` on a model of 3, and
273
+ * `selected = GTK_INVALID_LIST_POSITION` on a non-empty model, each leave `selected` where
274
+ * it was and emit nothing). `Adw.ComboRow` forwards the position to
275
+ * `gtk_single_selection_set_selected`, which validates it — so this is not a renderer
276
+ * policy and no renderer states its own answer any more.
277
+ */
146
278
  setSelectedIndex(index: number): boolean;
147
279
  /** The selected option's `value`, or `''` when out of range. */
148
280
  get selectedValue(): string;
@@ -151,17 +283,13 @@ export declare class ComboState {
151
283
  /** Programmatic value set — moves the index to the first option with this `value`. No-op for an unknown value. */
152
284
  setSelectedValue(value: string): boolean;
153
285
  /**
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.
286
+ * Whether `index` addresses an item the held model has.
287
+ *
288
+ * The ONE bounds predicate: {@link setSelectedIndex} and {@link select} both refuse what it
289
+ * refuses, so a programmatic set and a user pick agree about what is out of range. They
290
+ * once did not — `refs/gtk` is empty, so what `gtk_single_selection_set_selected` does with
291
+ * `GTK_INVALID_LIST_POSITION` was not verifiable in this tree and each renderer stated its
292
+ * own answer. It is now measured (see {@link setSelectedIndex}) and there is one.
165
293
  */
166
294
  hasIndex(index: number): boolean;
167
295
  /**