@gjsify/adwaita-web 0.62.0 → 0.63.1-next.235

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 (43) hide show
  1. package/README.md +31 -0
  2. package/dist/adwaita-web.css +4 -0
  3. package/dist/adwaita-web.css.map +1 -1
  4. package/lib/types/canvas2d/canvas2d-bridge.d.ts +26 -5
  5. package/lib/types/elements/adw-application-window.d.ts +26 -7
  6. package/lib/types/elements/adw-bottom-sheet.d.ts +3 -0
  7. package/lib/types/elements/adw-data-grid.d.ts +4 -0
  8. package/lib/types/elements/adw-window-features.d.ts +39 -0
  9. package/lib/types/elements/adw-window.d.ts +11 -24
  10. package/lib/types/elements/gtk-application-window.d.ts +12 -4
  11. package/lib/types/elements/gtk-window.d.ts +44 -0
  12. package/lib/types/gtk-window.spec.d.ts +6 -0
  13. package/lib/types/pango-markup.d.ts +10 -0
  14. package/lib/types/pango-markup.spec.d.ts +1 -0
  15. package/lib/types/source-view/copy-glyph.d.ts +6 -0
  16. package/lib/types/source-view/copy-glyph.spec.d.ts +1 -0
  17. package/lib/types/styles.generated.d.ts +1 -1
  18. package/package.json +13 -12
  19. package/scss/_window.scss +7 -2
  20. package/src/adw-application-window.spec.ts +8 -0
  21. package/src/adw-data-grid.spec.ts +11 -0
  22. package/src/adw-window.spec.ts +8 -0
  23. package/src/application.spec.ts +34 -1
  24. package/src/application.ts +16 -0
  25. package/src/bottom-sheet.spec.ts +9 -0
  26. package/src/canvas2d/canvas2d-bridge.ts +24 -8
  27. package/src/elements/adw-alert-dialog.ts +8 -5
  28. package/src/elements/adw-application-window.ts +90 -20
  29. package/src/elements/adw-banner.ts +14 -9
  30. package/src/elements/adw-bottom-sheet.ts +9 -0
  31. package/src/elements/adw-data-grid.ts +18 -0
  32. package/src/elements/adw-window-features.ts +128 -0
  33. package/src/elements/adw-window.ts +45 -108
  34. package/src/elements/gtk-application-window.ts +17 -6
  35. package/src/elements/gtk-window.ts +170 -18
  36. package/src/gtk-window.spec.ts +133 -0
  37. package/src/pango-markup.spec.ts +257 -0
  38. package/src/pango-markup.ts +266 -0
  39. package/src/source-view/adw-source-view.ts +2 -8
  40. package/src/source-view/copy-glyph.spec.ts +46 -0
  41. package/src/source-view/copy-glyph.ts +52 -0
  42. package/src/styles.generated.ts +1 -1
  43. package/src/test.browser.mts +4 -0
@@ -0,0 +1,128 @@
1
+ // What `Adw.Window` and `Adw.ApplicationWindow` add on top of their Gtk parent: the content
2
+ // area, the breakpoints and the open-dialog bookkeeping.
3
+ //
4
+ // A HELPER, NOT A BASE CLASS. Upstream `AdwWindow` extends `GtkWindow` and `AdwApplicationWindow`
5
+ // extends `GtkApplicationWindow` (Adw-1.gir), so the two cannot share an Adw base without one of
6
+ // them losing its Gtk parent. A custom element has one prototype chain, so the shared part is
7
+ // composed in: each class owns one of these and forwards to it.
8
+ //
9
+ // Reference: refs/libadwaita/src/adw-window.c
10
+ // Copyright (c) 2020-2023 Alice Mikhaylenko / Purism SPC (libadwaita). LGPLv2.1+
11
+ // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
12
+
13
+ import { AdwBreakpoint } from '@gjsify/adwaita-core';
14
+ import { addBreakpoints } from '../breakpoints.js';
15
+
16
+ /** The default minimum size of an AdwWindow, from adw_window_init (C:344). */
17
+ const DEFAULT_WIDTH = 360;
18
+ const DEFAULT_HEIGHT = 200;
19
+
20
+ export class AdwWindowFeatures {
21
+ private _contentEl: HTMLDivElement | null = null;
22
+ private _breakpoints: AdwBreakpoint[] = [];
23
+ private _disposeBreakpoints: (() => void) | null = null;
24
+ private _openDialogs: HTMLElement[] = [];
25
+
26
+ constructor(private readonly _host: HTMLElement) {}
27
+
28
+ /** The content area, or `null` before the window is first connected. */
29
+ get contentArea(): HTMLDivElement | null {
30
+ return this._contentEl;
31
+ }
32
+
33
+ /** The content widget — the first child of the content area. */
34
+ get content(): HTMLElement | null {
35
+ return (this._contentEl?.firstElementChild as HTMLElement | null) ?? null;
36
+ }
37
+
38
+ set content(value: HTMLElement | null) {
39
+ if (value) this._contentEl?.appendChild(value);
40
+ }
41
+
42
+ get breakpoints(): readonly AdwBreakpoint[] {
43
+ return this._breakpoints;
44
+ }
45
+
46
+ get currentBreakpoint(): AdwBreakpoint | null {
47
+ return this._breakpoints.find((bp) => bp.applied) ?? null;
48
+ }
49
+
50
+ get dialogs(): readonly HTMLElement[] {
51
+ // A dialog taken out of the document never raises `notify::open` on the way
52
+ // out, so a dialog that was REMOVED while open would otherwise stay listed
53
+ // for the rest of the window's life.
54
+ this._openDialogs = this._openDialogs.filter((dialog) => dialog.isConnected);
55
+ return this._openDialogs;
56
+ }
57
+
58
+ get visibleDialog(): HTMLElement | null {
59
+ const dialogs = this.dialogs;
60
+ return dialogs.length > 0 ? (dialogs[dialogs.length - 1] as HTMLElement) : null;
61
+ }
62
+
63
+ /**
64
+ * Take the content area the Gtk parent built, once. Tracks open dialogs presented inside
65
+ * the window: an AdwDialog raises `notify::open`, which bubbles to the host.
66
+ */
67
+ adopt(contentEl: HTMLDivElement): void {
68
+ if (this._contentEl !== null) return;
69
+ this._contentEl = contentEl;
70
+ contentEl.classList.add('adw-window-content');
71
+ this._host.addEventListener('notify::open', (e) => {
72
+ const dialog = e.target as HTMLElement;
73
+ if (dialog === this._host) return;
74
+ if (dialog.hasAttribute('open')) {
75
+ if (!this._openDialogs.includes(dialog)) this._openDialogs.push(dialog);
76
+ } else {
77
+ this._openDialogs = this._openDialogs.filter((d) => d !== dialog);
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Add a breakpoint (adw_window_add_breakpoint). The breakpoint evaluates against the
84
+ * window's own box and sets a `data-breakpoint` attribute when applied.
85
+ */
86
+ addBreakpoint(condition: string): AdwBreakpoint {
87
+ const host = this._host;
88
+ const bp = new AdwBreakpoint(condition, {
89
+ onApply: () => (host.dataset.breakpoint = condition),
90
+ onUnapply: () => {
91
+ if (host.dataset.breakpoint === condition) delete host.dataset.breakpoint;
92
+ },
93
+ });
94
+ this._breakpoints.push(bp);
95
+ // Re-evaluated against a box that may have changed since the last add.
96
+ this.bind();
97
+ return bp;
98
+ }
99
+
100
+ /**
101
+ * Bind the breakpoints to THIS box.
102
+ *
103
+ * NOT once per lifetime: `release` drops the disposer, so a window moved between
104
+ * parents would come back deaf — and a window is exactly the thing a client-side route
105
+ * change re-parents. Re-binding is cheap and idempotent, and it re-measures the new box.
106
+ */
107
+ bind(): void {
108
+ this._disposeBreakpoints?.();
109
+ this._disposeBreakpoints = null;
110
+ if (this._contentEl !== null && this._breakpoints.length > 0) {
111
+ this._disposeBreakpoints = addBreakpoints(this._host, this._breakpoints);
112
+ }
113
+ }
114
+
115
+ release(): void {
116
+ this._disposeBreakpoints?.();
117
+ this._disposeBreakpoints = null;
118
+ }
119
+
120
+ /** The window's size request — GTK's, not CSS's, so an absent attribute means the default. */
121
+ applySizeRequest(): void {
122
+ const host = this._host;
123
+ const width = host.getAttribute('default-width') ?? host.getAttribute('width');
124
+ const height = host.getAttribute('default-height') ?? host.getAttribute('height');
125
+ host.style.width = width ? `${width}px` : `${DEFAULT_WIDTH}px`;
126
+ host.style.height = height ? `${height}px` : `${DEFAULT_HEIGHT}px`;
127
+ }
128
+ }
@@ -1,5 +1,6 @@
1
- // <adw-window> — the web counterpart of Adw.Window: a freeform window with no
2
- // titlebar area. Adw.Window styles nothing itself — you pair it with
1
+ // <adw-window> — the web counterpart of Adw.Window: a Gtk.Window (so <gtk-window>'s state
2
+ // members — is-active, focus-widget, close-request, present(), maximized — are inherited)
3
+ // with no titlebar area. Adw.Window styles nothing itself — you pair it with
3
4
  // Adw.ToolbarView and an Adw.HeaderBar (or a Gtk.HeaderBar) to get the chrome —
4
5
  // so this element owns the FRAME and the window's own state, and the chrome is
5
6
  // whatever you put in it.
@@ -38,159 +39,95 @@
38
39
  // Copyright (c) 2020-2023 Alice Mikhaylenko / Purism SPC (libadwaita). LGPLv2.1+
39
40
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
40
41
 
41
- import { AdwBreakpoint } from '@gjsify/adwaita-core';
42
- import { addBreakpoints } from '../breakpoints.js';
43
- import { bindSlottedChildren } from '../slotted-children.js';
42
+ import type { AdwBreakpoint } from '@gjsify/adwaita-core';
43
+ import type { AdwSlot } from '../slotted-children.js';
44
+ import { AdwWindowFeatures } from './adw-window-features.js';
45
+ import { GtkWindow } from './gtk-window.js';
44
46
 
45
- /** The default minimum size of an AdwWindow, from adw_window_init (C:344). */
46
- const DEFAULT_WIDTH = 360;
47
- const DEFAULT_HEIGHT = 200;
48
-
49
- export class AdwWindow extends HTMLElement {
50
- private _initialized = false;
51
- private _contentEl!: HTMLDivElement;
52
- private _breakpoints: AdwBreakpoint[] = [];
53
- private _disposeBreakpoints: (() => void) | null = null;
54
- private _openDialogs: HTMLElement[] = [];
47
+ export class AdwWindow extends GtkWindow {
48
+ private readonly _features = new AdwWindowFeatures(this);
55
49
 
56
50
  static get observedAttributes() {
57
- return ['width', 'height', 'default-width', 'default-height'];
51
+ return [...GtkWindow.observedAttributes, 'width', 'height'];
58
52
  }
59
53
 
60
54
  /** The content area — the Adw.Window:content property. */
61
55
  get contentArea(): HTMLDivElement {
62
- return this._contentEl;
56
+ return this._content;
63
57
  }
64
58
 
65
59
  /** The content widget — the first child of the content area. */
66
60
  get content(): HTMLElement | null {
67
- return this._contentEl.firstElementChild as HTMLElement | null;
61
+ return this._features.content;
68
62
  }
69
63
 
70
64
  set content(value: HTMLElement | null) {
71
- if (value) {
72
- this._contentEl.appendChild(value);
73
- }
65
+ this._features.content = value;
74
66
  }
75
67
 
76
68
  /** The breakpoints added to this window. */
77
69
  get breakpoints(): readonly AdwBreakpoint[] {
78
- return this._breakpoints;
70
+ return this._features.breakpoints;
79
71
  }
80
72
 
81
73
  /** The currently applied breakpoint, or null when none is. */
82
74
  get currentBreakpoint(): AdwBreakpoint | null {
83
- return this._breakpoints.find((bp) => bp.applied) ?? null;
75
+ return this._features.currentBreakpoint;
84
76
  }
85
77
 
86
78
  /** The open dialogs presented inside this window (Adw.Window:dialogs). */
87
79
  get dialogs(): readonly HTMLElement[] {
88
- // A dialog taken out of the document never raises `notify::open` on the way
89
- // out, so a dialog that was REMOVED while open would otherwise stay listed
90
- // for the rest of the window's life.
91
- this._openDialogs = this._openDialogs.filter((dialog) => dialog.isConnected);
92
- return this._openDialogs;
80
+ return this._features.dialogs;
93
81
  }
94
82
 
95
83
  /** The currently visible dialog, or null when none is (Adw.Window:visible-dialog). */
96
84
  get visibleDialog(): HTMLElement | null {
97
- const dialogs = this.dialogs;
98
- return dialogs.length > 0 ? (dialogs[dialogs.length - 1] as HTMLElement) : null;
85
+ return this._features.visibleDialog;
99
86
  }
100
87
 
101
- /**
102
- * Add a breakpoint to this window (adw_window_add_breakpoint).
103
- * The breakpoint evaluates against the window's own box and sets a
104
- * `data-breakpoint` attribute when applied.
105
- */
88
+ /** Add a breakpoint to this window (adw_window_add_breakpoint). */
106
89
  addBreakpoint(condition: string): AdwBreakpoint {
107
- const bp = new AdwBreakpoint(condition, {
108
- onApply: () => (this.dataset.breakpoint = condition),
109
- onUnapply: () => {
110
- if (this.dataset.breakpoint === condition) delete this.dataset.breakpoint;
111
- },
112
- });
113
- this._breakpoints.push(bp);
114
- // Re-evaluated against a box that may have changed since the last add.
115
- this._bindBreakpoints();
116
- return bp;
90
+ return this._features.addBreakpoint(condition);
117
91
  }
118
92
 
119
93
  connectedCallback() {
120
- this._buildOnce();
121
- // EVERY connect, not only the first — see `_bindBreakpoints`.
122
- this._bindBreakpoints();
94
+ super.connectedCallback();
95
+ this._features.adopt(this._content);
96
+ // EVERY connect, not only the first — see `AdwWindowFeatures.bind`.
97
+ this._features.bind();
123
98
  }
124
99
 
125
100
  disconnectedCallback() {
126
- this._disposeBreakpoints?.();
127
- this._disposeBreakpoints = null;
101
+ this._features.release();
102
+ super.disconnectedCallback();
128
103
  }
129
104
 
130
105
  attributeChangedCallback(name: string) {
131
- // The size request is read from the ATTRIBUTE, so it is applied on every
132
- // change — including one that arrived before the element was connected, which
133
- // is the ordinary case for markup already sitting in a document.
134
- if (name === 'width' || name === 'height' || name === 'default-width' || name === 'default-height') {
135
- this._applySizeRequest();
106
+ // The size request is read from the ATTRIBUTE, so it is applied on every change —
107
+ // including one that arrived before the element was connected.
108
+ if (name === 'width' || name === 'height') {
109
+ this.renderFrameSize();
110
+ return;
136
111
  }
112
+ super.attributeChangedCallback(name);
137
113
  }
138
114
 
139
- private _buildOnce() {
140
- if (this._initialized) return;
141
- this._initialized = true;
142
-
143
- this._applySizeRequest();
144
-
145
- this._contentEl = document.createElement('div');
146
- this._contentEl.className = 'adw-window-content';
147
-
148
- // Children are the content — the Adw.Window buildable default, and `content` is
149
- // ALSO its name, because a `.blp` writes `content:` on the window and an authored
150
- // tree needs the name to route it: `refuseUnknownSlots` in
151
- // `src/shared-tree-builder.ts` throws on a slot a defined element does not
152
- // declare, and `showcases/gtk/effect-adw-services/src/window.blp` is rooted at
153
- // one. The content area becomes the host's own child, so a child appended later
154
- // lands in it too (`bindSlottedChildren` is LIVE, see `src/slotted-children.ts`).
155
- bindSlottedChildren(this, [{ name: 'content', into: this._contentEl }, { into: this._contentEl }]).install(
156
- this._contentEl,
157
- );
158
-
159
- // Track open dialogs presented inside this window: an AdwDialog raises
160
- // `notify::open`, which bubbles to here.
161
- this.addEventListener('notify::open', (e) => {
162
- const dialog = e.target as HTMLElement;
163
- if (dialog === this) return;
164
- if (dialog.hasAttribute('open')) {
165
- if (!this._openDialogs.includes(dialog)) this._openDialogs.push(dialog);
166
- } else {
167
- this._openDialogs = this._openDialogs.filter((d) => d !== dialog);
168
- }
169
- });
170
- }
171
-
172
- /**
173
- * Bind the breakpoints to THIS box.
174
- *
175
- * NOT once per lifetime: `disconnectedCallback` releases the disposer, so a
176
- * window moved between parents would come back deaf — and a window is exactly
177
- * the thing a client-side route change re-parents. Re-binding is cheap and
178
- * idempotent, and it re-measures the new box on the way.
179
- */
180
- private _bindBreakpoints() {
181
- this._disposeBreakpoints?.();
182
- this._disposeBreakpoints = null;
183
- if (this._initialized && this._breakpoints.length > 0) {
184
- this._disposeBreakpoints = addBreakpoints(this, this._breakpoints);
185
- }
115
+ /** `Adw.Window` has no titlebar of its own: you pair it with a header bar. */
116
+ protected override hasTitlebar(): boolean {
117
+ return false;
118
+ }
119
+
120
+ protected override strips(): Node[] {
121
+ return [this._content];
122
+ }
123
+
124
+ protected override extraSlots(): AdwSlot[] {
125
+ // `content` is the content area's name too: a `.blp` writes `content:` on the window.
126
+ return [{ name: 'content', into: this._content }];
186
127
  }
187
128
 
188
- /** The window's size request — GTK's, not CSS's, so an absent attribute means the default. */
189
- private _applySizeRequest() {
190
- const width = this.getAttribute('default-width') ?? this.getAttribute('width');
191
- const height = this.getAttribute('default-height') ?? this.getAttribute('height');
192
- this.style.width = width ? `${width}px` : `${DEFAULT_WIDTH}px`;
193
- this.style.height = height ? `${height}px` : `${DEFAULT_HEIGHT}px`;
129
+ protected override renderFrameSize(): void {
130
+ this._features.applySizeRequest();
194
131
  }
195
132
  }
196
133
 
@@ -16,21 +16,32 @@
16
16
  //
17
17
  // THE `win.` / `app.` PREFIXES. `GtkApplicationWindow` exports window actions under `win.` and
18
18
  // application-wide ones under `app.` (:48-56), and any `GtkActionable` inside the window can
19
- // activate either. A page has no `GActionMap`, so nothing is exported here; what this element
20
- // adds over `<gtk-window>` is the strip and the property that shows it.
19
+ // activate either. `defineWindowApplication` installs the `Gio.ActionMap` half on the prototype
20
+ // (`add_action`, `lookup_action`, `remove_action`, the `win.` registry); the `Gio.ActionGroup`
21
+ // half stays off because a window's `activate_action` is the widget's. Plain `<gtk-window>` has
22
+ // no ActionMap. What this element adds over it is that map, the strip and the property that shows it.
21
23
  //
22
24
  // Reference: refs/gtk/gtk/gtkapplicationwindow.c
23
25
  // Reference: refs/libadwaita/src/stylesheet/widgets/_window.scss (GtkWindow)
24
26
  // Copyright (c) The GTK Team. LGPLv2.1+.
25
27
  // Modifications: Implemented as a Web Component for @gjsify/adwaita-web.
26
28
 
29
+ import type { ActionLike } from '@gjsify/adwaita-core';
30
+ import type { GtkApplication } from '../application.js';
27
31
  import type { AdwSlot } from '../slotted-children.js';
28
32
  import { GtkWindow } from './gtk-window.js';
29
33
  import { defineWindowApplication } from '../application.js';
30
34
 
31
35
  export class GtkApplicationWindow extends GtkWindow {
32
36
  /** `Gtk.Window:application`: setting it is `add_window` (ADR 0098), set by `defineWindowApplication`. */
33
- declare application: object | null;
37
+ declare application: GtkApplication | null;
38
+
39
+ /** `Gio.ActionMap.add_action`: installed by `installActionMap`; the action is reachable as `win.<name>`. */
40
+ declare add_action: (action: ActionLike) => void;
41
+ /** `Gio.ActionMap.lookup_action`: the window's own action, or `null`. */
42
+ declare lookup_action: (name: string) => ActionLike | null;
43
+ /** `Gio.ActionMap.remove_action`: removing an unknown name does nothing. */
44
+ declare remove_action: (name: string) => void;
34
45
  private _menubar: HTMLDivElement | null = null;
35
46
  private _menubarObserver: MutationObserver | null = null;
36
47
 
@@ -51,7 +62,7 @@ export class GtkApplicationWindow extends GtkWindow {
51
62
  * The menubar strip, created once and reused: `strips()` and `extraSlots()` both ask for
52
63
  * it, so whichever runs first builds it and the other finds it.
53
64
  */
54
- private menubarSection(): HTMLDivElement {
65
+ protected menubarSection(): HTMLDivElement {
55
66
  if (this._menubar === null) {
56
67
  const section = document.createElement('div');
57
68
  section.className = 'adw-gtk-application-window-menubar';
@@ -95,14 +106,14 @@ export class GtkApplicationWindow extends GtkWindow {
95
106
  * The observer is what makes the second half live: `bindSlottedChildren` adopts a
96
107
  * `slot="menubar"` child after connect, and a bar that appeared then must appear.
97
108
  */
98
- private _followMenubar(): void {
109
+ protected _followMenubar(): void {
99
110
  this._menubarObserver?.disconnect();
100
111
  this._menubarObserver = new MutationObserver(() => this._renderMenubar());
101
112
  this._menubarObserver.observe(this.menubarSection(), { childList: true });
102
113
  this._renderMenubar();
103
114
  }
104
115
 
105
- private _renderMenubar(): void {
116
+ protected _renderMenubar(): void {
106
117
  const menubar = this.menubarSection();
107
118
  menubar.hidden = !this.showMenubar || menubar.childElementCount === 0;
108
119
  }