@gjsify/adwaita-web 0.51.0 → 0.52.0
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.
- package/dist/adwaita-web.css +255 -27
- package/dist/adwaita-web.css.map +1 -1
- package/lib/types/blueprint-layout.spec.d.ts +1 -0
- package/lib/types/blueprint-markup.spec.d.ts +4 -0
- package/lib/types/blueprint-tree.spec.d.ts +1 -0
- package/lib/types/elements/adw-bottom-sheet.d.ts +31 -13
- package/lib/types/elements/adw-carousel.d.ts +5 -0
- package/lib/types/elements/adw-header-bar.d.ts +15 -0
- package/lib/types/elements/adw-inline-view-switcher.d.ts +1 -0
- package/lib/types/elements/adw-navigation-view.d.ts +1 -0
- package/lib/types/elements/adw-sidebar.d.ts +10 -0
- package/lib/types/elements/adw-tab-view.d.ts +1 -0
- package/lib/types/elements/adw-toggle-group.d.ts +11 -0
- package/lib/types/elements/gtk-action-bar.d.ts +19 -0
- package/lib/types/elements/gtk-box.d.ts +17 -0
- package/lib/types/elements/gtk-button.d.ts +2 -0
- package/lib/types/elements/gtk-label.d.ts +122 -0
- package/lib/types/gio-menu.spec.d.ts +1 -0
- package/lib/types/gtk-box.spec.d.ts +1 -0
- package/lib/types/gtk-label.spec.d.ts +1 -0
- package/lib/types/gtk-value-doors.spec.d.ts +1 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/namespace/gio.d.ts +1 -0
- package/lib/types/namespace/gtk.d.ts +5 -0
- package/lib/types/shared-tree-builder.d.ts +64 -0
- package/lib/types/source-view/asm6502.d.ts +1 -20
- package/lib/types/source-view/index.d.ts +3 -2
- package/lib/types/source-view/theme.d.ts +1 -1
- package/lib/types/string-list-slot.d.ts +4 -0
- package/lib/types/styles.generated.d.ts +1 -1
- package/lib/types/tags.spec.d.ts +1 -0
- package/lib/types/value-lists.spec.d.ts +1 -0
- package/package.json +8 -6
- package/scss/_action_bar.scss +51 -0
- package/scss/_banner.scss +8 -1
- package/scss/_bottom_sheet.scss +57 -0
- package/scss/_box.scss +23 -0
- package/scss/_button.scss +29 -7
- package/scss/_button_content.scss +6 -1
- package/scss/_button_row.scss +5 -2
- package/scss/_carousel.scss +6 -0
- package/scss/_labels.scss +126 -12
- package/scss/_navigation_split_view.scss +18 -0
- package/scss/_overlay_split_view.scss +5 -0
- package/scss/_reset.scss +29 -4
- package/scss/_split_button.scss +5 -2
- package/scss/_status_page.scss +31 -17
- package/scss/_theme.scss +11 -3
- package/scss/_toolbar_view.scss +10 -0
- package/scss/_variables.scss +1 -0
- package/scss/_widget.scss +55 -0
- package/scss/_window_title.scss +9 -0
- package/scss/adwaita-skin.scss +8 -2
- package/src/adw-banner.spec.ts +16 -0
- package/src/adw-button-row.spec.ts +8 -0
- package/src/adw-header-bar.spec.ts +17 -0
- package/src/blueprint-layout.spec.ts +223 -0
- package/src/blueprint-markup.spec.ts +155 -0
- package/src/blueprint-tree.spec.ts +174 -0
- package/src/bottom-sheet.spec.ts +166 -1
- package/src/chrome.spec.ts +5 -2
- package/src/elements/adw-bottom-sheet.ts +166 -59
- package/src/elements/adw-carousel.ts +10 -5
- package/src/elements/adw-clamp.ts +12 -0
- package/src/elements/adw-combo-row.ts +13 -4
- package/src/elements/adw-header-bar.ts +46 -3
- package/src/elements/adw-inline-view-switcher.ts +11 -0
- package/src/elements/adw-navigation-view.ts +10 -0
- package/src/elements/adw-sidebar.ts +22 -0
- package/src/elements/adw-spin-row.ts +44 -5
- package/src/elements/adw-status-page.ts +12 -6
- package/src/elements/adw-tab-view.ts +20 -3
- package/src/elements/adw-toggle-group.ts +45 -4
- package/src/elements/adw-toolbar-view.ts +9 -0
- package/src/elements/gtk-action-bar.ts +90 -0
- package/src/elements/gtk-box.ts +87 -0
- package/src/elements/gtk-button.ts +60 -13
- package/src/elements/gtk-drop-down.ts +10 -1
- package/src/elements/gtk-label.ts +415 -0
- package/src/gio-menu.spec.ts +125 -0
- package/src/globals.d.ts +7 -0
- package/src/gtk-box.spec.ts +168 -0
- package/src/gtk-button.spec.ts +123 -1
- package/src/gtk-label.spec.ts +595 -0
- package/src/gtk-value-doors.spec.ts +104 -0
- package/src/index.ts +14 -1
- package/src/keyboard-operable.spec.ts +2 -1
- package/src/namespace/gio.ts +19 -0
- package/src/namespace/gtk.ts +14 -0
- package/src/shared-tree-builder.ts +268 -0
- package/src/shared-trees.spec.ts +296 -32
- package/src/source-view/adw-source-view.ts +3 -3
- package/src/source-view/asm6502.ts +11 -191
- package/src/source-view/index.ts +17 -11
- package/src/source-view/theme.ts +16 -16
- package/src/split-button.spec.ts +7 -0
- package/src/string-list-slot.ts +21 -0
- package/src/styles.generated.ts +1 -1
- package/src/tags.spec.ts +51 -0
- package/src/test.browser.mts +18 -0
- package/src/value-lists.spec.ts +93 -0
- package/lib/types/source-view/hex.d.ts +0 -21
- package/src/source-view/hex.ts +0 -37
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
// <gtk-label> — a run of text, and the node libadwaita's typography classes are written
|
|
2
|
+
// for (`.title-1` … `.caption`, `.dimmed`, `_labels.scss`).
|
|
3
|
+
//
|
|
4
|
+
// NAMED FOR THE LIBRARY THAT OWNS THE GTYPE (ADR 0034 clause 1): libadwaita ships no label
|
|
5
|
+
// type, so there is no `adw-` spelling. `_labels.scss` used to argue there should be no
|
|
6
|
+
// element at all — a `<span>` with a class does everything a hand-written page needs. What
|
|
7
|
+
// it cannot do is be the target of an authored tree: a Blueprint `Gtk.Label { label: …; }`
|
|
8
|
+
// mounted through `mountSharedTree` arrives as a TAG with a `label` attribute, and without
|
|
9
|
+
// an element that reads it the text never reached the screen.
|
|
10
|
+
//
|
|
11
|
+
// THE TEXT IS WRITTEN AS TEXT. `use-markup` is honoured by REDUCING the markup to its
|
|
12
|
+
// plain text (`labelDisplayText` in `@gjsify/adwaita-core`, the reduction the NativeScript
|
|
13
|
+
// label shares), never by handing it to `innerHTML`, which would run whatever the string
|
|
14
|
+
// carries. GTK draws the bold; this port shows the word. `use-underline` strips the
|
|
15
|
+
// mnemonic marker and binds no key.
|
|
16
|
+
//
|
|
17
|
+
// `xalign` IS WHERE THE TEXT SITS IN THE LABEL'S BOX, `justify` how its LINES align with
|
|
18
|
+
// each other — two properties, as in GTK. `xalign` is a continuum and CSS alignment has
|
|
19
|
+
// three keywords, so the box splits its free space between two spacers in the ratio
|
|
20
|
+
// `xalign : 1 − xalign` (`_labels.scss`), which is the C's `xalign * (width − text width)`
|
|
21
|
+
// to the pixel, and mirrors in RTL as `gtk_label` does. `justify` maps through Pango's own
|
|
22
|
+
// switch: LEFT and RIGHT are START and END of the text direction, FILL is start-aligned
|
|
23
|
+
// lines with inter-word justification.
|
|
24
|
+
//
|
|
25
|
+
// `ELLIPSIZE`, `WRAP-MODE`, `LINES`, `WIDTH-CHARS`, `MAX-WIDTH-CHARS` AND `YALIGN` are
|
|
26
|
+
// Pango's text-layout knobs, and each reaches a REAL CSS mechanism rather than being
|
|
27
|
+
// declared unreachable: `wrap-mode` is `word-break`/`overflow-wrap`, `width-chars`/
|
|
28
|
+
// `max-width-chars` are `min-width`/`max-width` in `ch` (`@gjsify/adwaita-core`'s
|
|
29
|
+
// `labelWidthCharsExtent`). `ellipsize` and `lines` are the SAME `-webkit-line-clamp` box
|
|
30
|
+
// (`.adw-label-clamp` in `_labels.scss`) WHENEVER `ellipsize` is active, `wrap` or not —
|
|
31
|
+
// MEASURED (a real `Gtk.Label`, allocated, gjs 1.88.1/gtk 4.22.5, `@gjsify/adwaita-core`'s
|
|
32
|
+
// `label.ts` header carries the numbers): `wrap` gates NOTHING once `ellipsize` is set,
|
|
33
|
+
// and an unset `lines` still caps at Pango's own default of ONE line, never "unlimited" —
|
|
34
|
+
// `labelEffectiveLines` is that whole derivation, shared with the NativeScript port. A
|
|
35
|
+
// single-line, non-ellipsized, non-wrapping label is the one case with no box at all: a
|
|
36
|
+
// bare text node, see `_renderSpan`'s doc comment for why it still needs the measurement
|
|
37
|
+
// pass the wrap-only case always needed. `yalign` is a DECLARED divergence past its three
|
|
38
|
+
// exact values (0, 0.5, 1): CSS `align-items` has three keywords and no ratio the way
|
|
39
|
+
// `flex-grow` gives `xalign` one — `_labels.scss`'s header says why a second spacer pair
|
|
40
|
+
// is not the fix; `labelYalignAlignItems` (`@gjsify/adwaita-core`) is the nearest-of-three
|
|
41
|
+
// zone map. `ellipsize` carries its own declared divergence at `start`/`middle`:
|
|
42
|
+
// `labelEllipsizeOverflowValue` (`@gjsify/adwaita-core`) draws every non-`none` mode as an
|
|
43
|
+
// end-ellipsis, the one truncating value CSS `text-overflow` and NativeScript's
|
|
44
|
+
// `textOverflow` both have.
|
|
45
|
+
//
|
|
46
|
+
// NOT HERE, and declared in `check-adwaita-element-properties.mjs`: `natural-wrap-mode`
|
|
47
|
+
// (a NATURAL-SIZE-REQUEST hint over a size-negotiation protocol this renderer does not
|
|
48
|
+
// run — a browser lays out once, it does not ask a widget for a preferred width first),
|
|
49
|
+
// `single-line-mode` (height pinned to one line's ascent+descent regardless of content —
|
|
50
|
+
// no CSS box does that without measuring the font, which a `<gtk-label>` never needs to:
|
|
51
|
+
// its own height already IS one line's whenever `wrap` is off) and the mnemonic machinery.
|
|
52
|
+
// `selectable` is only the ability to select; there is no caret or context menu.
|
|
53
|
+
//
|
|
54
|
+
// Reference: refs/gtk/gtk/gtklabel.c (properties, the `label` CSS name, the justify
|
|
55
|
+
// switch, get_default_widths, gtk_label_ensure_layout's width/height conditions)
|
|
56
|
+
// Reference: refs/libadwaita/src/stylesheet/widgets/_labels.scss (`label {}`)
|
|
57
|
+
// Copyright (c) The GTK Team, GNOME contributors. LGPLv2.1+.
|
|
58
|
+
|
|
59
|
+
import {
|
|
60
|
+
labelDisplayText,
|
|
61
|
+
labelEffectiveLines,
|
|
62
|
+
labelEllipsizeOverflowValue,
|
|
63
|
+
labelWidthCharsExtent,
|
|
64
|
+
labelYalignAlignItems,
|
|
65
|
+
normalizeLabelEllipsize,
|
|
66
|
+
normalizeLabelJustify,
|
|
67
|
+
normalizeLabelLines,
|
|
68
|
+
normalizeLabelMaxWidthChars,
|
|
69
|
+
normalizeLabelWidthChars,
|
|
70
|
+
normalizeLabelWrapMode,
|
|
71
|
+
normalizeLabelXalign,
|
|
72
|
+
normalizeLabelYalign,
|
|
73
|
+
type LabelEllipsizeMode,
|
|
74
|
+
type LabelJustification,
|
|
75
|
+
type LabelWrapMode,
|
|
76
|
+
} from '@gjsify/adwaita-core';
|
|
77
|
+
|
|
78
|
+
/** The attributes that carry a property — also the `notify::` roster. */
|
|
79
|
+
const PROPERTY_ATTRIBUTES = [
|
|
80
|
+
'label',
|
|
81
|
+
'use-markup',
|
|
82
|
+
'use-underline',
|
|
83
|
+
'justify',
|
|
84
|
+
'xalign',
|
|
85
|
+
'yalign',
|
|
86
|
+
'wrap',
|
|
87
|
+
'wrap-mode',
|
|
88
|
+
'ellipsize',
|
|
89
|
+
'lines',
|
|
90
|
+
'width-chars',
|
|
91
|
+
'max-width-chars',
|
|
92
|
+
'selectable',
|
|
93
|
+
] as const;
|
|
94
|
+
|
|
95
|
+
/** `Gtk.Justification` as a CSS `text-align`, through Pango's switch in `gtklabel.c`. */
|
|
96
|
+
const JUSTIFY_TEXT_ALIGN: Record<LabelJustification, string> = {
|
|
97
|
+
left: 'start',
|
|
98
|
+
right: 'end',
|
|
99
|
+
center: 'center',
|
|
100
|
+
fill: 'justify',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export class GtkLabel extends HTMLElement {
|
|
104
|
+
/**
|
|
105
|
+
* The rendered text's own box — present while {@link wrap} is set (so it can be pinned
|
|
106
|
+
* to its measured wrap width, see `_remeasure`) OR while {@link ellipsize} is not
|
|
107
|
+
* `none` (so `overflow`/`text-overflow` have a box of their own to clip, rather than
|
|
108
|
+
* the label's flex container, which also holds the `xalign` spacer pseudo-elements).
|
|
109
|
+
* `null` the rest of the time so a single-line, non-ellipsized label keeps the plain
|
|
110
|
+
* text node `childElementCount` asserts on (`gtk-label.spec.ts`'s XSS note: "the DOM
|
|
111
|
+
* gets one text node").
|
|
112
|
+
*/
|
|
113
|
+
private _wrapSpan: HTMLSpanElement | null = null;
|
|
114
|
+
private _resizes: ResizeObserver | null = null;
|
|
115
|
+
|
|
116
|
+
static get observedAttributes() {
|
|
117
|
+
return [...PROPERTY_ATTRIBUTES];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** `Gtk.Label:label` — the text as written, before markup and the mnemonic come out. */
|
|
121
|
+
get label(): string {
|
|
122
|
+
return this.getAttribute('label') ?? '';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
set label(value: string) {
|
|
126
|
+
this.setAttribute('label', value ?? '');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** `Gtk.Label:use-markup` — whether {@link label} is Pango markup (shown as its text). */
|
|
130
|
+
get useMarkup(): boolean {
|
|
131
|
+
return this.hasAttribute('use-markup');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
set useMarkup(value: boolean) {
|
|
135
|
+
this.toggleAttribute('use-markup', !!value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** `Gtk.Label:use-underline` — whether an `_` marks a mnemonic (stripped, not bound). */
|
|
139
|
+
get useUnderline(): boolean {
|
|
140
|
+
return this.hasAttribute('use-underline');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
set useUnderline(value: boolean) {
|
|
144
|
+
this.toggleAttribute('use-underline', !!value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** `Gtk.Label:justify` — how the lines align with each other. Defaults to `left`. */
|
|
148
|
+
get justify(): LabelJustification {
|
|
149
|
+
return normalizeLabelJustify(this.getAttribute('justify'));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
set justify(value: LabelJustification) {
|
|
153
|
+
this.setAttribute('justify', value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** `Gtk.Label:xalign` — where the text sits in the label's box, 0…1. Defaults to 0.5. */
|
|
157
|
+
get xalign(): number {
|
|
158
|
+
return normalizeLabelXalign(this.getAttribute('xalign'));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
set xalign(value: number) {
|
|
162
|
+
this.setAttribute('xalign', String(value));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* `Gtk.Label:yalign` — where the text sits vertically in the label's box, 0…1.
|
|
167
|
+
* Defaults to 0.5. Exact at 0, 0.5 and 1; a DECLARED divergence between them — see
|
|
168
|
+
* `_labels.scss`'s header for why `align-items` has no continuum the way `xalign`'s
|
|
169
|
+
* flex-grow ratio does.
|
|
170
|
+
*/
|
|
171
|
+
get yalign(): number {
|
|
172
|
+
return normalizeLabelYalign(this.getAttribute('yalign'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
set yalign(value: number) {
|
|
176
|
+
this.setAttribute('yalign', String(value));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** `Gtk.Label:wrap` — whether the text breaks into lines rather than overflowing. */
|
|
180
|
+
get wrap(): boolean {
|
|
181
|
+
return this.hasAttribute('wrap');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
set wrap(value: boolean) {
|
|
185
|
+
this.toggleAttribute('wrap', !!value);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `Gtk.Label:wrap-mode` — where a wrapping line may break. Defaults to `word`. Only
|
|
190
|
+
* affects the formatting while the layout can actually span more than one line —
|
|
191
|
+
* {@link wrap}, or {@link ellipsize} being active (`ellipsize`, not just `wrap`, is
|
|
192
|
+
* what gives GTK's own Pango layout a width to break within: `label.ts`'s header in
|
|
193
|
+
* `@gjsify/adwaita-core`).
|
|
194
|
+
*/
|
|
195
|
+
get wrapMode(): LabelWrapMode {
|
|
196
|
+
return normalizeLabelWrapMode(this.getAttribute('wrap-mode'));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
set wrapMode(value: LabelWrapMode) {
|
|
200
|
+
this.setAttribute('wrap-mode', value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `Gtk.Label:ellipsize` — where the string is trimmed when it does not fit. Defaults
|
|
205
|
+
* to `none`. `start` and `middle` are HELD faithfully — read `ellipsize` back and it
|
|
206
|
+
* is exactly what was set — but DRAWN as an end-ellipsis: `labelEllipsizeOverflowValue`
|
|
207
|
+
* (`@gjsify/adwaita-core`) says why.
|
|
208
|
+
*/
|
|
209
|
+
get ellipsize(): LabelEllipsizeMode {
|
|
210
|
+
return normalizeLabelEllipsize(this.getAttribute('ellipsize'));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
set ellipsize(value: LabelEllipsizeMode) {
|
|
214
|
+
this.setAttribute('ellipsize', value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* `Gtk.Label:lines` — the line count an ellipsized label is held to. Defaults to -1
|
|
219
|
+
* ("unset"), which still caps at ONE line the moment {@link ellipsize} is active —
|
|
220
|
+
* Pango's own default, not "unlimited". Has no effect at all while ellipsize is
|
|
221
|
+
* `none`, `wrap` or not — `labelEffectiveLines` (`@gjsify/adwaita-core`) is that
|
|
222
|
+
* MEASURED guard, and its own doc comment carries the numbers.
|
|
223
|
+
*/
|
|
224
|
+
get lines(): number {
|
|
225
|
+
return normalizeLabelLines(this.getAttribute('lines'));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
set lines(value: number) {
|
|
229
|
+
this.setAttribute('lines', String(value));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* `Gtk.Label:width-chars` — the label's minimum width, in characters. Defaults to -1
|
|
234
|
+
* (calculated automatically, i.e. unset here).
|
|
235
|
+
*/
|
|
236
|
+
get widthChars(): number {
|
|
237
|
+
return normalizeLabelWidthChars(this.getAttribute('width-chars'));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
set widthChars(value: number) {
|
|
241
|
+
this.setAttribute('width-chars', String(value));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* `Gtk.Label:max-width-chars` — the label's natural (maximum) width, in characters.
|
|
246
|
+
* Defaults to -1 (calculated automatically, i.e. unset here).
|
|
247
|
+
*/
|
|
248
|
+
get maxWidthChars(): number {
|
|
249
|
+
return normalizeLabelMaxWidthChars(this.getAttribute('max-width-chars'));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
set maxWidthChars(value: number) {
|
|
253
|
+
this.setAttribute('max-width-chars', String(value));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** `Gtk.Label:selectable` — whether the text can be selected. GTK's default is not. */
|
|
257
|
+
get selectable(): boolean {
|
|
258
|
+
return this.hasAttribute('selectable');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
set selectable(value: boolean) {
|
|
262
|
+
this.toggleAttribute('selectable', !!value);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** `gtk_label_get_text` — the text as SHOWN, markup reduced and the marker removed. */
|
|
266
|
+
getText(): string {
|
|
267
|
+
return labelDisplayText(this.label, this.useMarkup, this.useUnderline);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
connectedCallback() {
|
|
271
|
+
this._render();
|
|
272
|
+
this._resizes = new ResizeObserver(() => this._remeasure());
|
|
273
|
+
this._resizes.observe(this);
|
|
274
|
+
void document.fonts?.ready.then(() => this._remeasure());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
disconnectedCallback() {
|
|
278
|
+
this._resizes?.disconnect();
|
|
279
|
+
this._resizes = null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private _remeasure(): void {
|
|
283
|
+
if (!this._wrapSpan || !this.isConnected) return;
|
|
284
|
+
this._wrapSpan.style.width = '';
|
|
285
|
+
const width = measuredWrapWidth(this._wrapSpan);
|
|
286
|
+
this._wrapSpan.style.width = width !== null ? `${width}px` : '';
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
attributeChangedCallback(name: string, old: string | null, value: string | null) {
|
|
290
|
+
this._render();
|
|
291
|
+
const next = this._normalized(name, value);
|
|
292
|
+
if (next === this._normalized(name, old)) return;
|
|
293
|
+
this.dispatchEvent(new CustomEvent(`notify::${name}`, { bubbles: true, detail: { [name]: next } }));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private _normalized(name: string, raw: string | null): string | number | boolean {
|
|
297
|
+
if (name === 'label') return raw ?? '';
|
|
298
|
+
if (name === 'justify') return normalizeLabelJustify(raw);
|
|
299
|
+
if (name === 'xalign') return normalizeLabelXalign(raw);
|
|
300
|
+
if (name === 'yalign') return normalizeLabelYalign(raw);
|
|
301
|
+
if (name === 'wrap-mode') return normalizeLabelWrapMode(raw);
|
|
302
|
+
if (name === 'ellipsize') return normalizeLabelEllipsize(raw);
|
|
303
|
+
if (name === 'lines') return normalizeLabelLines(raw);
|
|
304
|
+
if (name === 'width-chars') return normalizeLabelWidthChars(raw);
|
|
305
|
+
if (name === 'max-width-chars') return normalizeLabelMaxWidthChars(raw);
|
|
306
|
+
return raw !== null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private _render(): void {
|
|
310
|
+
// `textContent`, the whole of the XSS answer: whatever the label holds, the DOM
|
|
311
|
+
// gets one text node (wrapped in {@link _wrapSpan} while {@link wrap} or
|
|
312
|
+
// {@link ellipsize} needs a box of its own — see `_renderSpan`).
|
|
313
|
+
const text = this.getText();
|
|
314
|
+
const wrap = this.wrap;
|
|
315
|
+
const ellipsize = this.ellipsize;
|
|
316
|
+
const clamped = ellipsize !== 'none';
|
|
317
|
+
if (wrap || clamped) this._renderSpan(text, clamped);
|
|
318
|
+
else this._renderSingleLine(text);
|
|
319
|
+
|
|
320
|
+
this.style.setProperty('--gtk-label-xalign', String(this.xalign));
|
|
321
|
+
this.style.textAlign = JUSTIFY_TEXT_ALIGN[this.justify];
|
|
322
|
+
// `yalign`'s three-zone snap — `_labels.scss`'s header explains why there is no
|
|
323
|
+
// continuum here the way `xalign`'s ratio spacers give the main axis one.
|
|
324
|
+
this.style.setProperty('--gtk-label-align-items', labelYalignAlignItems(this.yalign));
|
|
325
|
+
|
|
326
|
+
// The ONE truncating value CSS/NativeScript can both draw — see
|
|
327
|
+
// `labelEllipsizeOverflowValue`'s comment. Left unset (falling through to the
|
|
328
|
+
// `clip` `_labels.scss`'s non-clamping rules default to) while `ellipsize` is
|
|
329
|
+
// `none`, so a label that never asked to be truncated never gets a stray custom
|
|
330
|
+
// property a later `getComputedStyle` read would have to explain.
|
|
331
|
+
const overflow = labelEllipsizeOverflowValue(ellipsize);
|
|
332
|
+
if (overflow === 'ellipsis') this.style.setProperty('--gtk-label-ellipsize-overflow', overflow);
|
|
333
|
+
else this.style.removeProperty('--gtk-label-ellipsize-overflow');
|
|
334
|
+
|
|
335
|
+
// MEASURED (`label.ts`'s header, `@gjsify/adwaita-core`): `wrap` plays no part in
|
|
336
|
+
// whether `lines` has an effect — only `ellipsize` does, and once it is active an
|
|
337
|
+
// UNSET `lines` still caps at Pango's own default of ONE line, never "unlimited".
|
|
338
|
+
// So this custom property is set whenever `ellipsize` is active, `wrap` or not;
|
|
339
|
+
// `_labels.scss`'s `.adw-label-clamp` rule (toggled by `_renderSpan`, not an
|
|
340
|
+
// attribute selector, since the RAW `ellipsize` attribute can hold a value this
|
|
341
|
+
// method's own `normalizeLabelEllipsize` already rejected) is the one place that
|
|
342
|
+
// reads it.
|
|
343
|
+
const effectiveLines = labelEffectiveLines(this.lines, ellipsize);
|
|
344
|
+
if (effectiveLines !== null) this.style.setProperty('--gtk-label-lines', String(effectiveLines));
|
|
345
|
+
else this.style.removeProperty('--gtk-label-lines');
|
|
346
|
+
|
|
347
|
+
// `width-chars` / `max-width-chars`, in `ch` — GTK's MINIMUM and NATURAL widths
|
|
348
|
+
// (`get_default_widths`, gtklabel.c) — on the flex item itself, so wrap/ellipsize
|
|
349
|
+
// resolve against that box rather than the ambient container.
|
|
350
|
+
const extent = labelWidthCharsExtent(this.widthChars, this.maxWidthChars);
|
|
351
|
+
if (extent.minCh !== null) this.style.minWidth = `${extent.minCh}ch`;
|
|
352
|
+
else this.style.removeProperty('min-width');
|
|
353
|
+
if (extent.maxCh !== null) this.style.maxWidth = `${extent.maxCh}ch`;
|
|
354
|
+
else this.style.removeProperty('max-width');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private _renderSingleLine(text: string): void {
|
|
358
|
+
this._wrapSpan?.remove();
|
|
359
|
+
this._wrapSpan = null;
|
|
360
|
+
if (this.textContent !== text) this.textContent = text;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* A WRAPPED OR ELLIPSIZED label centres its BLOCK by `xalign` (`gtklabel.c`,
|
|
365
|
+
* `gtk_label_get_layout_location`), not just each line — `_labels.scss`'s two spacer
|
|
366
|
+
* pseudo-elements split the label's FREE space by `xalign`, and a flex item only has
|
|
367
|
+
* free space to give them once it FITS. `flex-basis: auto` sizes the plain text node to
|
|
368
|
+
* its max-content (single-line) width; once that overflows, `flex-shrink` collapses the
|
|
369
|
+
* zero-basis spacers to nothing and stretches the text item to the full container width
|
|
370
|
+
* instead — which is where the offset was lost (the defect this method fixes).
|
|
371
|
+
*
|
|
372
|
+
* NO PURE-CSS FIX EXISTS: `width: fit-content` clamps to that SAME full-container width
|
|
373
|
+
* the moment its content no longer fits one line — CSS shrink-to-fit is
|
|
374
|
+
* `min(max(min-content, available), max-content)`, and `available` IS the container
|
|
375
|
+
* here, not the narrower width GTK's Pango layout actually wrapped to. So this measures
|
|
376
|
+
* instead, in two passes: first let {@link _wrapSpan} wrap at the FULL container width
|
|
377
|
+
* (`width: ''`, the same collapse above, which is exactly the width GTK's own
|
|
378
|
+
* allocation wraps against), then pin the span to its widest resulting line via
|
|
379
|
+
* `Range.getClientRects()`, which gives the pseudo spacers real free space to split.
|
|
380
|
+
* Approximate on purpose, and cheap: one synchronous reflow per render. The pinned
|
|
381
|
+
* width goes stale whenever the available width or the font changes, and a page does
|
|
382
|
+
* both (a rotated phone, an opened sidebar, a web font arriving late), so
|
|
383
|
+
* {@link connectedCallback} re-measures on each resize and once the fonts have loaded.
|
|
384
|
+
*
|
|
385
|
+
* AN ELLIPSIZED SPAN NEEDS THIS TOO, MEASURED THE HARD WAY: an earlier version of this
|
|
386
|
+
* method skipped it for `ellipsize` without `wrap`, reasoning that `white-space: nowrap`
|
|
387
|
+
* text has one width whichever way it is measured. That reasoning stopped applying the
|
|
388
|
+
* moment `ellipsize` alone started wrapping onto MULTIPLE lines to match GTK
|
|
389
|
+
* (`label.ts`'s header) — `white-space: normal` multi-word text has a real
|
|
390
|
+
* min-content/max-content gap, the exact shrink-to-fit collapse the wrapped case above
|
|
391
|
+
* was already written for. One code path now, for both.
|
|
392
|
+
*/
|
|
393
|
+
private _renderSpan(text: string, clamped: boolean): void {
|
|
394
|
+
if (!this._wrapSpan) {
|
|
395
|
+
this.textContent = '';
|
|
396
|
+
this._wrapSpan = document.createElement('span');
|
|
397
|
+
this._wrapSpan.className = 'adw-label-text';
|
|
398
|
+
this.appendChild(this._wrapSpan);
|
|
399
|
+
}
|
|
400
|
+
this._wrapSpan.classList.toggle('adw-label-clamp', clamped);
|
|
401
|
+
if (this._wrapSpan.textContent !== text) this._wrapSpan.textContent = text;
|
|
402
|
+
this._remeasure();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The widest line {@link span}'s text wrapped to, at its CURRENT width — see `_renderSpan`. */
|
|
407
|
+
function measuredWrapWidth(span: HTMLSpanElement): number | null {
|
|
408
|
+
const range = document.createRange();
|
|
409
|
+
range.selectNodeContents(span);
|
|
410
|
+
let max = 0;
|
|
411
|
+
for (const rect of range.getClientRects()) max = Math.max(max, rect.width);
|
|
412
|
+
return max > 0 ? Math.ceil(max) : null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
customElements.define('gtk-label', GtkLabel);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// `Gio.Menu` as an authoring door — held against the value it has to produce.
|
|
2
|
+
//
|
|
3
|
+
// THE CLAIM UNDER TEST is not "the class has methods": it is that a menu BUILT the GJS way
|
|
4
|
+
// and the same menu WRITTEN as an array reduce to one model, so a `menuModel` property
|
|
5
|
+
// cannot tell them apart. Every case below therefore asserts against
|
|
6
|
+
// `normalizeMenuModel(<the array a reader would have written>)` rather than against a
|
|
7
|
+
// hand-typed expectation — a hand-typed one would agree with whatever this file produces.
|
|
8
|
+
//
|
|
9
|
+
// The class itself lives in `@gjsify/adwaita-core`; what this file drives is the port's
|
|
10
|
+
// own `Gio` DOOR onto it, so a barrel that stops resolving fails here rather than in a
|
|
11
|
+
// consumer.
|
|
12
|
+
//
|
|
13
|
+
// Compared as JSON rather than with `toEqual`: `@gjsify/unit`'s `toEqual` is `==`, so two
|
|
14
|
+
// different objects pass it whatever they hold.
|
|
15
|
+
|
|
16
|
+
import { describe, expect, it } from '@gjsify/unit';
|
|
17
|
+
import { normalizeMenuModel } from '@gjsify/adwaita-core';
|
|
18
|
+
|
|
19
|
+
import { Menu, MenuItem } from './namespace/gio.js';
|
|
20
|
+
|
|
21
|
+
/** What a `menuModel` property does with a value, on both spellings. */
|
|
22
|
+
const model = (value: readonly unknown[]) => JSON.stringify(normalizeMenuModel(value as never));
|
|
23
|
+
|
|
24
|
+
export const GioMenuTest = async () => {
|
|
25
|
+
await describe('Gio.Menu is the array a menuModel already takes', async () => {
|
|
26
|
+
await it('passes Array.isArray, which is the gate normalizeMenuModel opens on', () => {
|
|
27
|
+
expect(Array.isArray(new Menu())).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await it('starts empty, as g_menu_new() does', () => {
|
|
31
|
+
expect(new Menu().length).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await describe('Gio.Menu.append', async () => {
|
|
36
|
+
await it('builds the gallery menu the array spelling builds', () => {
|
|
37
|
+
const menu = new Menu();
|
|
38
|
+
menu.append('Save as…', 'app.save-as');
|
|
39
|
+
menu.append('Export', 'app.export');
|
|
40
|
+
menu.append('Print', 'app.print');
|
|
41
|
+
|
|
42
|
+
expect(model(menu)).toBe(
|
|
43
|
+
model([
|
|
44
|
+
{ label: 'Save as…', action: 'app.save-as' },
|
|
45
|
+
{ label: 'Export', action: 'app.export' },
|
|
46
|
+
{ label: 'Print', action: 'app.print' },
|
|
47
|
+
]),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
await it('takes a label alone, the way the bare-string shorthand does', () => {
|
|
52
|
+
const menu = new Menu();
|
|
53
|
+
menu.append('Export');
|
|
54
|
+
|
|
55
|
+
expect(model(menu)).toBe(model(['Export']));
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await describe('Gio.Menu.append_item and Gio.MenuItem', async () => {
|
|
60
|
+
await it('appends an item built separately', () => {
|
|
61
|
+
const menu = new Menu();
|
|
62
|
+
menu.append_item(new MenuItem('Save as…', 'app.save-as'));
|
|
63
|
+
|
|
64
|
+
expect(model(menu)).toBe(model([{ label: 'Save as…', action: 'app.save-as' }]));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
await it('sets both halves after construction', () => {
|
|
68
|
+
const item = new MenuItem();
|
|
69
|
+
item.set_label('Print');
|
|
70
|
+
item.set_detailed_action('app.print');
|
|
71
|
+
|
|
72
|
+
expect(model([item])).toBe(model([{ label: 'Print', action: 'app.print' }]));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
await it('REMOVES the attribute on null rather than carrying an undefined one', () => {
|
|
76
|
+
const item = new MenuItem('Print', 'app.print');
|
|
77
|
+
item.set_detailed_action(null);
|
|
78
|
+
|
|
79
|
+
expect(Object.hasOwn(item, 'action')).toBe(false);
|
|
80
|
+
expect(model([item])).toBe(model(['Print']));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await it('drops an item carrying neither half — g_menu_item_new(NULL, NULL)', () => {
|
|
84
|
+
expect(model([new MenuItem()])).toBe(model([]));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await describe('Gio.Menu.append_section', async () => {
|
|
89
|
+
await it('produces the section LINK, not an item with a stray array', () => {
|
|
90
|
+
const section = new Menu();
|
|
91
|
+
section.append('Save as…', 'app.save-as');
|
|
92
|
+
|
|
93
|
+
const menu = new Menu();
|
|
94
|
+
menu.append_section(null, section);
|
|
95
|
+
|
|
96
|
+
expect(model(menu)).toBe(model([{ section: [{ label: 'Save as…', action: 'app.save-as' }] }]));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await it('carries the heading when C would have one', () => {
|
|
100
|
+
const menu = new Menu();
|
|
101
|
+
menu.append_section('Document', ['Export']);
|
|
102
|
+
|
|
103
|
+
expect(model(menu)).toBe(model([{ label: 'Document', section: ['Export'] }]));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await it('writes NO label attribute when there is none', () => {
|
|
107
|
+
const menu = new Menu();
|
|
108
|
+
menu.append_section(null, ['Export']);
|
|
109
|
+
|
|
110
|
+
expect(Object.hasOwn(menu[0] as object, 'label')).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await describe('Gio.Menu.append_submenu', async () => {
|
|
115
|
+
await it('produces the submenu LINK', () => {
|
|
116
|
+
const submenu = new Menu();
|
|
117
|
+
submenu.append('Print', 'app.print');
|
|
118
|
+
|
|
119
|
+
const menu = new Menu();
|
|
120
|
+
menu.append_submenu('More', submenu);
|
|
121
|
+
|
|
122
|
+
expect(model(menu)).toBe(model([{ label: 'More', submenu: [{ label: 'Print', action: 'app.print' }] }]));
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
};
|
package/src/globals.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Type-check-only ambient declarations. Nothing here is imported at runtime.
|
|
2
|
+
//
|
|
3
|
+
// `tsconfig.json` sets `"types": []`, so an ambient package's declarations reach this project
|
|
4
|
+
// only by an explicit reference. The `*.blp` / `*.blp?shared-tree` module patterns come from
|
|
5
|
+
// the build plugin that serves them, which is the same route every `.blp`-importing showcase
|
|
6
|
+
// already takes (`showcases/gtk/adw-blueprint-layout/src/globals.d.ts`).
|
|
7
|
+
/// <reference types="@gjsify/vite-plugin-blueprint/types" />
|