flexdesk 0.14.1 → 0.15.1
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/README.md +38 -4
- package/css/flexdesk.css +34 -0
- package/dist/flexdesk.css +34 -0
- package/dist/host.js +28 -5
- package/dist/host.js.map +2 -2
- package/dist/widgets.js +100 -9
- package/dist/widgets.js.map +2 -2
- package/dist/wm.js +3 -1
- package/dist/wm.js.map +2 -2
- package/package.json +1 -1
- package/src/host/host.js +2 -1
- package/src/host/photino_host.js +31 -3
- package/src/host/photino_host.md +56 -3
- package/src/tiling/tile_breadcrumb.js +5 -1
- package/src/ui/controllers/window_chrome_controller.js +106 -11
package/package.json
CHANGED
package/src/host/host.js
CHANGED
|
@@ -19,7 +19,8 @@ export const HOST_CONTRACT = Object.freeze({
|
|
|
19
19
|
window: Object.freeze({
|
|
20
20
|
required: Object.freeze(['chrome', 'minimize', 'close', 'isMaximized', 'setMaximized',
|
|
21
21
|
'isFullscreen', 'toggleFullscreen', 'getBounds', 'setBounds']),
|
|
22
|
-
|
|
22
|
+
// `chrome()` answers 'native' or 'custom', or a promise of one.
|
|
23
|
+
optional: Object.freeze(['startNativeDrag', 'startNativeResize']),
|
|
23
24
|
}),
|
|
24
25
|
dialogs: Object.freeze({ required: Object.freeze(['saveFile']), optional: Object.freeze([]) }),
|
|
25
26
|
state: Object.freeze({ required: Object.freeze(['read', 'write']), optional: Object.freeze([]) }),
|
package/src/host/photino_host.js
CHANGED
|
@@ -99,6 +99,10 @@ export function createPhotinoTransport({ external, timeoutMs = 10000, onEvent =
|
|
|
99
99
|
* @param {number} [opts.timeoutMs=10000]
|
|
100
100
|
* @param {(name: string, value: any) => void} [opts.onEvent]
|
|
101
101
|
* @param {object} [opts.logger=console]
|
|
102
|
+
* @param {'auto'|'native'|'custom'} [opts.chrome='auto'] who draws the window's
|
|
103
|
+
* title bar and borders. `auto` asks the native side once (`host.describe`,
|
|
104
|
+
* its `frame` field), the first time somebody needs to know; `native`
|
|
105
|
+
* or `custom` fixes the answer without asking. See photino_host.md.
|
|
102
106
|
* @param {object|null} [opts.window] replace the window capability, or `null` to leave it out
|
|
103
107
|
* @param {object|null} [opts.dialogs] replace the dialogs capability, or `null` to leave it out
|
|
104
108
|
* @param {object|null} [opts.state] replace the state capability, or `null` to leave it out
|
|
@@ -110,6 +114,7 @@ export function createPhotinoHost(opts = {}) {
|
|
|
110
114
|
timeoutMs = 10000,
|
|
111
115
|
onEvent = null,
|
|
112
116
|
logger = console,
|
|
117
|
+
chrome = 'auto',
|
|
113
118
|
} = opts;
|
|
114
119
|
if (!isPhotinoWindow(external)) return NULL_HOST;
|
|
115
120
|
|
|
@@ -121,11 +126,32 @@ export function createPhotinoHost(opts = {}) {
|
|
|
121
126
|
};
|
|
122
127
|
const fire = (cap, op, ...args) => { void soft(null, cap, op, ...args); };
|
|
123
128
|
|
|
129
|
+
// Who draws the frame. Asked once and remembered: a string once known, the
|
|
130
|
+
// pending question before that. A native side that predates `frame`, or
|
|
131
|
+
// doesn't answer within 3 s, has native chrome.
|
|
132
|
+
let frame = chrome === 'custom' || chrome === 'native' ? chrome : null;
|
|
133
|
+
let asking = null;
|
|
134
|
+
const askFrame = () => {
|
|
135
|
+
if (frame) return frame;
|
|
136
|
+
asking ??= request('host', 'describe', [], { wait: 3000 })
|
|
137
|
+
.then((d) => (d && d.frame === 'custom' ? 'custom' : 'native'), () => 'native')
|
|
138
|
+
.then((f) => { frame = f; return f; });
|
|
139
|
+
return asking;
|
|
140
|
+
};
|
|
141
|
+
// A drag or resize handed to the OS: the reply comes when the mouse is
|
|
142
|
+
// released, however long that takes. Refused (a native side without the op,
|
|
143
|
+
// or a window with native chrome) means "not handled": the caller does it in JS.
|
|
144
|
+
const gesture = async (op, ...args) => {
|
|
145
|
+
try { await request('window', op, args, { wait: 0 }); return true; }
|
|
146
|
+
catch (e) { return false; }
|
|
147
|
+
};
|
|
148
|
+
|
|
124
149
|
const defaults = {
|
|
125
150
|
window: {
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
151
|
+
// 'native' (the OS draws the frame: FlexDesk's own buttons would be
|
|
152
|
+
// a second set) or 'custom' (the native side made the window
|
|
153
|
+
// frameless). A promise until the native side has said which.
|
|
154
|
+
chrome: () => askFrame(),
|
|
129
155
|
minimize: () => { fire('window', 'minimize'); },
|
|
130
156
|
close: () => { fire('window', 'close'); },
|
|
131
157
|
isMaximized: async () => (await soft(false, 'window', 'isMaximized')) === true,
|
|
@@ -137,6 +163,8 @@ export function createPhotinoHost(opts = {}) {
|
|
|
137
163
|
if (!b || typeof b !== 'object') return null;
|
|
138
164
|
return { x: b.x, y: b.y, width: b.width, height: b.height };
|
|
139
165
|
},
|
|
166
|
+
startNativeDrag: () => gesture('startDrag'),
|
|
167
|
+
startNativeResize: (edge) => gesture('startResize', String(edge || '')),
|
|
140
168
|
// Not async, like pywebview's: a rAF hot path during a drag; nobody awaits.
|
|
141
169
|
setBounds({ x = null, y = null, width = null, height = null } = {}) {
|
|
142
170
|
fire('window', 'setBounds', { x, y, width, height });
|
package/src/host/photino_host.md
CHANGED
|
@@ -28,6 +28,7 @@ host → page {"event": "window.maximizedChanged", "value": true}
|
|
|
28
28
|
|
|
29
29
|
| `cap.op` | `args` | reply `value` | Photino |
|
|
30
30
|
|---|---|---|---|
|
|
31
|
+
| `host.describe` | `[]` | `{frame: "native" \| "custom", …}` | asked once, for `chrome()` (see below) |
|
|
31
32
|
| `window.minimize` | `[]` | anything | `Minimized = true` |
|
|
32
33
|
| `window.close` | `[]` | anything | `Close()` |
|
|
33
34
|
| `window.isMaximized` | `[]` | `bool` | `Maximized` |
|
|
@@ -36,13 +37,65 @@ host → page {"event": "window.maximizedChanged", "value": true}
|
|
|
36
37
|
| `window.toggleFullscreen` | `[]` | `bool`, the state now | `FullScreen = !FullScreen` |
|
|
37
38
|
| `window.getBounds` | `[]` | `{x, y, width, height}` | `Location`, `Size` |
|
|
38
39
|
| `window.setBounds` | `[{x, y, width, height}]`, any of them `null` = leave alone | anything | `Location`, `Size` |
|
|
40
|
+
| `window.startDrag` | `[]` | anything, **after mouse-up** | custom chrome only: the OS move loop (below) |
|
|
41
|
+
| `window.startResize` | `[edge]`: `n` `s` `e` `w` `ne` `nw` `se` `sw` | anything, **after mouse-up** | custom chrome only: the OS size loop (below) |
|
|
39
42
|
| `dialogs.saveFile` | `[{data, filename, kind}]` | see below | `ShowSaveFile(...)` + write |
|
|
40
43
|
| `state.read` | `[key]` | the stored value, or `null` | a file per key |
|
|
41
44
|
| `state.write` | `[key, value]` | `true` | a file per key |
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
## Native or custom chrome
|
|
47
|
+
|
|
48
|
+
A FlexDesk app runs in one of three ways, and the page needs no switch for it:
|
|
49
|
+
|
|
50
|
+
| | title bar and borders | `chrome()` | FlexDesk draws |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| a browser tab | the browser's | (no Photino host) | nothing |
|
|
53
|
+
| a Photino window, native frame | the OS's | `'native'` | nothing |
|
|
54
|
+
| a Photino window, custom frame | none: the page's top bar | `'custom'` | buttons, resize handles, drag |
|
|
55
|
+
|
|
56
|
+
`window.chrome()` asks the native side once, with `host.describe`, the first
|
|
57
|
+
time it's called (that is `WindowChromeController.initialize()`). It resolves
|
|
58
|
+
`'custom'` when the reply's `frame` is `"custom"`, and `'native'` for anything
|
|
59
|
+
else: no `frame` field, an `ok: false`, no answer within 3 s. An app that knows
|
|
60
|
+
already can say so: `createPhotinoHost({ chrome: 'custom' })` or `'native'`
|
|
61
|
+
answers without asking.
|
|
62
|
+
|
|
63
|
+
**What the native side does for a custom frame (Windows).** Don't use
|
|
64
|
+
Photino's `SetChromeless(true)`: that makes a bare popup window, with no
|
|
65
|
+
snapping, no Win+arrows, no shadow and square corners, and Photino then reports
|
|
66
|
+
it as fullscreen. Keep the normal window and stop drawing its frame:
|
|
67
|
+
|
|
68
|
+
1. After the window exists, add `WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX |
|
|
69
|
+
WS_MAXIMIZEBOX | WS_SYSMENU` to its style, and subclass it
|
|
70
|
+
(`SetWindowSubclass`). These styles are what make Windows treat it as a
|
|
71
|
+
normal window: Aero Snap, Win+arrows, the shadow, the rounded corners on
|
|
72
|
+
Windows 11.
|
|
73
|
+
2. In the subclass, answer `WM_NCCALCSIZE` (with `wParam` true) with `0`, so
|
|
74
|
+
the whole window is client area and nothing of the frame is drawn. While
|
|
75
|
+
maximised (`IsZoomed`), set the proposed rect to the monitor's work area
|
|
76
|
+
first: a maximised window is larger than the monitor by the frame's
|
|
77
|
+
thickness, and the taskbar must stay visible.
|
|
78
|
+
3. Answer `WM_NCACTIVATE` with `DefSubclassProc(h, msg, wParam, -1)`, so
|
|
79
|
+
Windows repaints no caption on activation.
|
|
80
|
+
4. `DwmExtendFrameIntoClientArea` with a top margin of 1 keeps DWM drawing
|
|
81
|
+
the shadow. `DWMWA_WINDOW_CORNER_PREFERENCE = DWMWCP_ROUND` asks for the
|
|
82
|
+
rounded corners.
|
|
83
|
+
5. Call `SetWindowPos(..., SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE |
|
|
84
|
+
SWP_NOZORDER)` to apply it.
|
|
85
|
+
|
|
86
|
+
The WebView covers the whole window, so the window never sees the mouse. That
|
|
87
|
+
is what `startDrag` and `startResize` are for. The page asks when the mouse
|
|
88
|
+
goes down on its top bar or on a resize handle. On the window's thread, call
|
|
89
|
+
`ReleaseCapture()`, then `SendMessage(hwnd, WM_NCLBUTTONDOWN, hit, cursor)`
|
|
90
|
+
with `HTCAPTION` for a drag, or `HTTOP` / `HTBOTTOM` / `HTLEFT` / `HTRIGHT` /
|
|
91
|
+
`HTTOPLEFT` / … for the edge. Windows then runs its own move or size loop,
|
|
92
|
+
exactly as for a click on a real caption or border, and `SendMessage` returns
|
|
93
|
+
when the mouse is released. Reply then. The page waits without a timeout, and a
|
|
94
|
+
reply of `ok: false` makes it move or resize the window from JS instead.
|
|
95
|
+
|
|
96
|
+
`describe`'s reply says `frame: "custom"` only when all of this was done. A
|
|
97
|
+
Photino window on Linux or macOS keeps its native frame and says `"native"`, or
|
|
98
|
+
leaves `frame` out.
|
|
46
99
|
|
|
47
100
|
**`dialogs.saveFile`.** `kind` is the file type the caller is saving, `'csv'`
|
|
48
101
|
or `'png'` today. `data` is the file's text for `csv` and the file's bytes,
|
|
@@ -238,14 +238,18 @@ function _renderInto(container, segments) {
|
|
|
238
238
|
const isLast = i === segments.length - 1;
|
|
239
239
|
if (isLast || !seg.onClick) {
|
|
240
240
|
li.classList.add('twm-topbar-breadcrumb__item--current');
|
|
241
|
+
// The current level, for assistive technology; the title carries
|
|
242
|
+
// the full label once a host's CSS truncates it (Tables BUG-0109).
|
|
243
|
+
li.setAttribute('aria-current', 'page');
|
|
241
244
|
li.innerHTML = `
|
|
242
245
|
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${_esc(seg.icon)}</span>
|
|
243
|
-
<span class="twm-topbar-breadcrumb__current">${_esc(seg.label)}</span>
|
|
246
|
+
<span class="twm-topbar-breadcrumb__current" title="${_esc(seg.label)}">${_esc(seg.label)}</span>
|
|
244
247
|
`;
|
|
245
248
|
} else {
|
|
246
249
|
const btn = document.createElement('button');
|
|
247
250
|
btn.className = 'twm-topbar-breadcrumb__link';
|
|
248
251
|
btn.type = 'button';
|
|
252
|
+
btn.title = String(seg.label ?? '');
|
|
249
253
|
btn.innerHTML = `
|
|
250
254
|
<span class="material-symbols-outlined twm-topbar-breadcrumb__icon">${_esc(seg.icon)}</span>
|
|
251
255
|
<span>${_esc(seg.label)}</span>
|
|
@@ -8,10 +8,29 @@
|
|
|
8
8
|
* - F11 fullscreen toggle
|
|
9
9
|
*
|
|
10
10
|
* Talks to the embedder ONLY through a Host's `window` capability
|
|
11
|
-
* (see
|
|
11
|
+
* (see src/host/host.js). A host with no `window` capability — a browser
|
|
12
12
|
* tab, a VS Code webview, the standalone demo — is legal: we simply don't
|
|
13
13
|
* install custom chrome. That degraded path is also the production path on
|
|
14
14
|
* Linux, macOS and `--browser`, where the OS/browser draws the title bar.
|
|
15
|
+
*
|
|
16
|
+
* THREE MODES, decided by `host.window.chrome()` (which may answer with a
|
|
17
|
+
* promise: the Photino host asks its native side): a browser tab and a window
|
|
18
|
+
* with native chrome get NOTHING from this class (no handles, no buttons, no
|
|
19
|
+
* body class), and only a window that answers `custom` gets the chrome.
|
|
20
|
+
*
|
|
21
|
+
* NATIVE GESTURES. A host that offers `startNativeDrag()` and
|
|
22
|
+
* `startNativeResize(edge)` gets drags on the top bar and presses on the
|
|
23
|
+
* resize handles handed to the OS, which brings snapping and a resize that
|
|
24
|
+
* keeps up with the pointer. A host without them gets the JS move and resize
|
|
25
|
+
* over getBounds/setBounds.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} [opts]
|
|
28
|
+
* @param {object} [opts.host] a Host; only its `window` capability is used
|
|
29
|
+
* @param {string|Element} [opts.topBar='.twm-global-top-bar'] the bar that drags the window
|
|
30
|
+
* @param {string|Element} [opts.controls] where to draw minimise/maximise/close when
|
|
31
|
+
* the page has no `#win-minimize`/`#win-maximize`/`#win-close` of its own
|
|
32
|
+
* (default: the top bar's `.twm-bar-right`, else the top bar itself)
|
|
33
|
+
* @param {string} [opts.noDrag] more selectors that never start a drag
|
|
15
34
|
*/
|
|
16
35
|
|
|
17
36
|
const SUPPORTS_POINTER = typeof window !== 'undefined' && 'onpointerdown' in window;
|
|
@@ -20,9 +39,13 @@ const MIN_HEIGHT = 350;
|
|
|
20
39
|
const DRAG_THRESHOLD = 4;
|
|
21
40
|
|
|
22
41
|
export class WindowChromeController {
|
|
23
|
-
constructor({ eventBus, logger, host } = {}) {
|
|
42
|
+
constructor({ eventBus, logger, host, topBar = '.twm-global-top-bar', controls = null, noDrag = '' } = {}) {
|
|
24
43
|
this.eventBus = eventBus;
|
|
25
44
|
this.logger = logger || console;
|
|
45
|
+
this._topBarOpt = topBar;
|
|
46
|
+
this._controlsOpt = controls;
|
|
47
|
+
this._noDrag = noDrag;
|
|
48
|
+
this._createdControls = null;
|
|
26
49
|
|
|
27
50
|
// The host's window capability, or null. Never throw on a missing
|
|
28
51
|
// one — see the class comment.
|
|
@@ -40,17 +63,22 @@ export class WindowChromeController {
|
|
|
40
63
|
/**
|
|
41
64
|
* Initialize the window chrome controller
|
|
42
65
|
*/
|
|
43
|
-
initialize() {
|
|
44
|
-
if (this._disposed) return;
|
|
66
|
+
async initialize() {
|
|
67
|
+
if (this._disposed) return false;
|
|
45
68
|
|
|
46
|
-
|
|
69
|
+
let chrome = 'native';
|
|
70
|
+
try { chrome = this.win ? await this.win.chrome() : 'native'; } catch (_) { chrome = 'native'; }
|
|
71
|
+
if (this._disposed) return false;
|
|
72
|
+
if (chrome !== 'custom') {
|
|
47
73
|
// Native window decorations (Linux/macOS) or the browser already
|
|
48
74
|
// provide a title bar + window controls; our custom chrome would be
|
|
49
75
|
// a redundant second set. Skip it entirely.
|
|
50
76
|
this.logger.info?.('[WindowChrome] Native/browser chrome detected — custom window chrome disabled');
|
|
51
|
-
return;
|
|
77
|
+
return false;
|
|
52
78
|
}
|
|
53
79
|
|
|
80
|
+
document.body.classList.add('twm-custom-chrome');
|
|
81
|
+
this.#createWindowControls();
|
|
54
82
|
this.#wireWindowButtons();
|
|
55
83
|
this.#createResizeHandles();
|
|
56
84
|
this.#wireTopBarDrag();
|
|
@@ -59,6 +87,47 @@ export class WindowChromeController {
|
|
|
59
87
|
this.#initializeState();
|
|
60
88
|
|
|
61
89
|
this.logger.info?.('[WindowChrome] Initialized');
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** An element from a selector or an element; null when there is none. */
|
|
94
|
+
#resolve(target, within = document) {
|
|
95
|
+
if (!target) return null;
|
|
96
|
+
if (typeof target !== 'string') return target;
|
|
97
|
+
return within.querySelector(target);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Draw minimise, maximise/restore and close, unless the page has its own
|
|
102
|
+
* `#win-*` buttons (EcoSim's markup), which are then wired instead.
|
|
103
|
+
*/
|
|
104
|
+
#createWindowControls() {
|
|
105
|
+
if (document.getElementById('win-minimize') || document.getElementById('win-close')) return;
|
|
106
|
+
const bar = this.#resolve(this._topBarOpt);
|
|
107
|
+
const host = this.#resolve(this._controlsOpt)
|
|
108
|
+
|| bar?.querySelector('.twm-bar-right')
|
|
109
|
+
|| bar;
|
|
110
|
+
if (!host) return;
|
|
111
|
+
const box = document.createElement('div');
|
|
112
|
+
box.className = 'twm-window-controls';
|
|
113
|
+
const button = (id, icon, label, extra = '') => {
|
|
114
|
+
const b = document.createElement('button');
|
|
115
|
+
b.type = 'button';
|
|
116
|
+
b.id = id;
|
|
117
|
+
b.className = `twm-window-controls__btn${extra}`;
|
|
118
|
+
b.setAttribute('aria-label', label);
|
|
119
|
+
b.title = label;
|
|
120
|
+
const i = document.createElement('span');
|
|
121
|
+
i.className = 'material-symbols-outlined';
|
|
122
|
+
i.textContent = icon;
|
|
123
|
+
b.appendChild(i);
|
|
124
|
+
box.appendChild(b);
|
|
125
|
+
};
|
|
126
|
+
button('win-minimize', 'remove', 'Minimize');
|
|
127
|
+
button('win-maximize', 'check_box_outline_blank', 'Maximize');
|
|
128
|
+
button('win-close', 'close', 'Close', ' twm-window-controls__btn--close');
|
|
129
|
+
host.appendChild(box);
|
|
130
|
+
this._createdControls = box;
|
|
62
131
|
}
|
|
63
132
|
|
|
64
133
|
/**
|
|
@@ -110,6 +179,7 @@ export class WindowChromeController {
|
|
|
110
179
|
const tooltipText = isMax ? 'Restore' : 'Maximize';
|
|
111
180
|
window.LatexTooltip?.set(maxBtn, tooltipText);
|
|
112
181
|
maxBtn.setAttribute('aria-label', tooltipText);
|
|
182
|
+
maxBtn.title = tooltipText;
|
|
113
183
|
|
|
114
184
|
document.body.classList.toggle('twm-window-maximized', isMax);
|
|
115
185
|
}
|
|
@@ -188,8 +258,23 @@ export class WindowChromeController {
|
|
|
188
258
|
|
|
189
259
|
let activePointerId = null;
|
|
190
260
|
|
|
261
|
+
const EDGE = {
|
|
262
|
+
right: 'e', left: 'w', top: 'n', bottom: 's',
|
|
263
|
+
cornerBR: 'se', cornerBL: 'sw', cornerTR: 'ne', cornerTL: 'nw',
|
|
264
|
+
};
|
|
265
|
+
|
|
191
266
|
const startDrag = async (e) => {
|
|
267
|
+
if (e.button != null && e.button !== 0) return;
|
|
192
268
|
e.preventDefault();
|
|
269
|
+
// ── Native resize: the OS runs its own size loop (it blocks until
|
|
270
|
+
// mouse-up), so the edge stays under the pointer. ──
|
|
271
|
+
if (this.win.startNativeResize) {
|
|
272
|
+
this.resizingActive = true;
|
|
273
|
+
let handled = false;
|
|
274
|
+
try { handled = await this.win.startNativeResize(EDGE[mode]); } catch (_) { handled = false; }
|
|
275
|
+
this.resizingActive = false;
|
|
276
|
+
if (handled) return;
|
|
277
|
+
}
|
|
193
278
|
this.resizingActive = true;
|
|
194
279
|
startX = e.clientX;
|
|
195
280
|
startY = e.clientY;
|
|
@@ -313,10 +398,18 @@ export class WindowChromeController {
|
|
|
313
398
|
* Setup top bar dragging for window movement
|
|
314
399
|
*/
|
|
315
400
|
#wireTopBarDrag() {
|
|
316
|
-
this.topBar =
|
|
401
|
+
this.topBar = this.#resolve(this._topBarOpt);
|
|
317
402
|
if (!this.topBar) return;
|
|
318
403
|
|
|
319
|
-
|
|
404
|
+
// What never starts a drag: anything you can click or type into. An
|
|
405
|
+
// embedder adds its own with `noDrag`. (`.window-controls` and
|
|
406
|
+
// `.panel-toggles` are kept for EcoSim-shaped markup.)
|
|
407
|
+
const NO_DRAG_SELECTOR = [
|
|
408
|
+
'.no-drag', 'button', 'a', 'input', 'select', 'textarea', 'label',
|
|
409
|
+
'[role="button"]', '[role="menuitem"]', '[role="tab"]', '[contenteditable="true"]',
|
|
410
|
+
'.twm-window-controls', '.window-controls', '.panel-toggles',
|
|
411
|
+
this._noDrag,
|
|
412
|
+
].filter(Boolean).join(', ');
|
|
320
413
|
|
|
321
414
|
let dragging = false;
|
|
322
415
|
let startSX = 0, startSY = 0, startX = null, startY = null, winW = 0, winH = 0;
|
|
@@ -533,8 +626,7 @@ export class WindowChromeController {
|
|
|
533
626
|
// Double-click to toggle maximize (only on blank surfaces)
|
|
534
627
|
const dblClickHandler = async (e) => {
|
|
535
628
|
// Skip maximize if double-click is on interactive elements
|
|
536
|
-
|
|
537
|
-
if (e.target.closest(interactiveSelector)) {
|
|
629
|
+
if (isInteractiveTarget(e.target)) {
|
|
538
630
|
return;
|
|
539
631
|
}
|
|
540
632
|
|
|
@@ -646,13 +738,16 @@ export class WindowChromeController {
|
|
|
646
738
|
});
|
|
647
739
|
this._disposers = [];
|
|
648
740
|
|
|
649
|
-
// Remove resize handles
|
|
741
|
+
// Remove resize handles, and the buttons this drew
|
|
650
742
|
Object.values(this.handles).forEach(handle => {
|
|
651
743
|
if (handle && handle.parentNode) {
|
|
652
744
|
handle.parentNode.removeChild(handle);
|
|
653
745
|
}
|
|
654
746
|
});
|
|
655
747
|
this.handles = {};
|
|
748
|
+
this._createdControls?.remove();
|
|
749
|
+
this._createdControls = null;
|
|
750
|
+
document.body.classList.remove('twm-custom-chrome', 'twm-window-maximized', 'window-fullscreen');
|
|
656
751
|
|
|
657
752
|
this.logger.info?.('[WindowChrome] Disposed');
|
|
658
753
|
}
|