@netless/window-manager 1.0.0-canary.7 → 1.0.0-canary.71
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 +30 -6
- package/dist/index.js +13625 -0
- package/dist/index.mjs +13622 -0
- package/dist/index.umd.js +13620 -46
- package/dist/{App → src/App}/AppContext.d.ts +16 -14
- package/dist/{App → src/App}/AppPageStateImpl.d.ts +0 -0
- package/dist/{App → src/App}/AppProxy.d.ts +30 -11
- package/dist/{App → src/App}/MagixEvent/index.d.ts +0 -0
- package/dist/src/App/WhiteboardView.d.ts +27 -0
- package/dist/{App → src/App}/index.d.ts +1 -0
- package/dist/src/App/type.d.ts +21 -0
- package/dist/{AppListener.d.ts → src/AppListener.d.ts} +2 -2
- package/dist/{AppManager.d.ts → src/AppManager.d.ts} +12 -7
- package/dist/{AttributesDelegate.d.ts → src/AttributesDelegate.d.ts} +5 -2
- package/dist/{BoxEmitter.d.ts → src/BoxEmitter.d.ts} +0 -0
- package/dist/{BoxManager.d.ts → src/BoxManager.d.ts} +12 -6
- package/dist/{BuiltinApps.d.ts → src/BuiltinApps.d.ts} +3 -0
- package/dist/{Cursor → src/Cursor}/Cursor.d.ts +0 -0
- package/dist/{Cursor → src/Cursor}/icons.d.ts +0 -0
- package/dist/{Cursor → src/Cursor}/index.d.ts +4 -3
- package/dist/{Helper.d.ts → src/Helper.d.ts} +4 -8
- package/dist/{InternalEmitter.d.ts → src/InternalEmitter.d.ts} +1 -4
- package/dist/{Page → src/Page}/PageController.d.ts +3 -1
- package/dist/{Page → src/Page}/index.d.ts +0 -0
- package/dist/{PageState.d.ts → src/PageState.d.ts} +1 -0
- package/dist/{ReconnectRefresher.d.ts → src/ReconnectRefresher.d.ts} +0 -0
- package/dist/{RedoUndo.d.ts → src/RedoUndo.d.ts} +0 -0
- package/dist/{Register → src/Register}/index.d.ts +4 -2
- package/dist/{Register → src/Register}/loader.d.ts +1 -1
- package/dist/src/Register/storage.d.ts +11 -0
- package/dist/{Utils → src/Utils}/AppCreateQueue.d.ts +0 -0
- package/dist/{Utils → src/Utils}/Common.d.ts +0 -0
- package/dist/{Utils → src/Utils}/Reactive.d.ts +1 -1
- package/dist/{Utils → src/Utils}/RoomHacker.d.ts +0 -0
- package/dist/{Utils → src/Utils}/error.d.ts +4 -1
- package/dist/{Utils → src/Utils}/log.d.ts +0 -0
- package/dist/src/View/CameraSynchronizer.d.ts +21 -0
- package/dist/{View → src/View}/MainView.d.ts +25 -7
- package/dist/src/View/ScrollMode.d.ts +32 -0
- package/dist/{View → src/View}/ViewManager.d.ts +0 -0
- package/dist/src/View/ViewSync.d.ts +32 -0
- package/dist/{callback.d.ts → src/callback.d.ts} +12 -1
- package/dist/{constants.d.ts → src/constants.d.ts} +12 -5
- package/dist/src/image.d.ts +19 -0
- package/dist/{index.d.ts → src/index.d.ts} +63 -17
- package/dist/src/shim.d.ts +11 -0
- package/dist/src/storage.d.ts +7 -0
- package/dist/{typings.d.ts → src/typings.d.ts} +21 -8
- package/dist/style.css +810 -1
- package/docs/api.md +10 -0
- package/docs/app-context.md +155 -27
- package/docs/mirgrate-to-1.0.md +68 -0
- package/package.json +27 -22
- package/playwright.config.ts +29 -0
- package/src/App/AppContext.ts +81 -46
- package/src/App/AppPageStateImpl.ts +3 -0
- package/src/App/AppProxy.ts +249 -141
- package/src/App/WhiteboardView.ts +37 -14
- package/src/App/index.ts +1 -0
- package/src/App/type.ts +22 -0
- package/src/AppListener.ts +27 -21
- package/src/AppManager.ts +96 -50
- package/src/AttributesDelegate.ts +6 -3
- package/src/BoxManager.ts +76 -38
- package/src/BuiltinApps.ts +9 -8
- package/src/Cursor/Cursor.svelte +6 -2
- package/src/Cursor/Cursor.ts +15 -4
- package/src/Cursor/icons.ts +6 -0
- package/src/Cursor/index.ts +16 -11
- package/src/Helper.ts +25 -7
- package/src/InternalEmitter.ts +1 -4
- package/src/Page/PageController.ts +3 -1
- package/src/PageState.ts +8 -1
- package/src/ReconnectRefresher.ts +7 -3
- package/src/Register/index.ts +36 -14
- package/src/Register/loader.ts +20 -9
- package/src/Register/storage.ts +26 -5
- package/src/Utils/Common.ts +3 -0
- package/src/Utils/Reactive.ts +29 -27
- package/src/Utils/RoomHacker.ts +3 -0
- package/src/Utils/error.ts +6 -2
- package/src/View/CameraSynchronizer.ts +55 -36
- package/src/View/MainView.ts +163 -77
- package/src/View/ScrollMode.ts +240 -0
- package/src/View/ViewSync.ts +138 -6
- package/src/callback.ts +8 -1
- package/src/constants.ts +11 -3
- package/src/image/pencil-eraser-1.svg +3 -0
- package/src/image/pencil-eraser-2.svg +3 -0
- package/src/image/pencil-eraser-3.svg +3 -0
- package/src/index.ts +197 -60
- package/src/storage.ts +15 -0
- package/src/style.css +18 -47
- package/src/typings.ts +24 -7
- package/vite.config.js +12 -7
- package/dist/App/AppViewSync.d.ts +0 -11
- package/dist/App/Storage/StorageEvent.d.ts +0 -8
- package/dist/App/Storage/index.d.ts +0 -39
- package/dist/App/Storage/typings.d.ts +0 -22
- package/dist/App/Storage/utils.d.ts +0 -5
- package/dist/App/WhiteboardView.d.ts +0 -21
- package/dist/Register/storage.d.ts +0 -8
- package/dist/View/CameraSynchronizer.d.ts +0 -17
- package/dist/View/ViewSync.d.ts +0 -7
- package/dist/index.cjs.js +0 -46
- package/dist/index.es.js +0 -16161
- package/pnpm-lock.yaml +0 -6302
- package/src/App/AppViewSync.ts +0 -68
- package/src/App/Storage/StorageEvent.ts +0 -21
- package/src/App/Storage/index.ts +0 -295
- package/src/App/Storage/typings.ts +0 -23
- package/src/App/Storage/utils.ts +0 -17
package/src/View/MainView.ts
CHANGED
@@ -1,74 +1,146 @@
|
|
1
1
|
import { callbacks } from "../callback";
|
2
|
-
import { CameraSynchronizer } from "./CameraSynchronizer";
|
3
2
|
import { createView } from "./ViewManager";
|
4
3
|
import { debounce, get, isEqual } from "lodash";
|
5
4
|
import { emitter } from "../InternalEmitter";
|
6
5
|
import { Events } from "../constants";
|
7
6
|
import { Fields } from "../AttributesDelegate";
|
8
|
-
import { reaction } from "white-web-sdk";
|
9
|
-
import { releaseView, setViewFocusScenePath } from "../Utils/Common";
|
7
|
+
import { AnimationMode, reaction, toJS } from "white-web-sdk";
|
8
|
+
import { releaseView, setScenePath, setViewFocusScenePath } from "../Utils/Common";
|
10
9
|
import { SideEffectManager } from "side-effect-manager";
|
11
|
-
import
|
10
|
+
import { Val } from "value-enhancer";
|
11
|
+
import { ViewSync } from "./ViewSync";
|
12
|
+
import type { ICamera, ISize } from "../AttributesDelegate";
|
13
|
+
import type { Size, View } from "white-web-sdk";
|
12
14
|
import type { AppManager } from "../AppManager";
|
15
|
+
import type { MoveCameraParams } from "../typings";
|
13
16
|
|
14
17
|
export class MainViewProxy {
|
15
18
|
private started = false;
|
16
19
|
private mainViewIsAddListener = false;
|
17
20
|
private mainView: View;
|
18
21
|
private store = this.manager.store;
|
19
|
-
private synchronizer: CameraSynchronizer;
|
20
22
|
|
21
23
|
private sideEffectManager = new SideEffectManager();
|
22
24
|
|
25
|
+
public camera$ = new Val<ICamera | undefined>(undefined);
|
26
|
+
public size$ = new Val<ISize | undefined>(undefined);
|
27
|
+
public view$ = new Val<View | undefined>(undefined);
|
28
|
+
private cameraUpdatePromise?: Promise<boolean>;
|
29
|
+
|
30
|
+
public viewSync?: ViewSync;
|
31
|
+
|
23
32
|
constructor(private manager: AppManager) {
|
24
|
-
this.synchronizer = new CameraSynchronizer(camera =>
|
25
|
-
this.store.setMainViewCamera({ ...camera, id: this.manager.uid })
|
26
|
-
);
|
27
33
|
this.mainView = this.createMainView();
|
28
|
-
this.moveCameraSizeByAttributes();
|
29
34
|
emitter.once("mainViewMounted").then(() => {
|
30
35
|
this.addMainViewListener();
|
31
36
|
this.start();
|
32
37
|
this.ensureCameraAndSize();
|
33
38
|
this.startListenWritableChange();
|
34
39
|
});
|
35
|
-
this.sideEffectManager.add(() =>
|
36
|
-
|
37
|
-
});
|
38
|
-
this.sideEffectManager.add(() => {
|
39
|
-
return emitter.on("startReconnect", () => {
|
40
|
+
this.sideEffectManager.add(() => [
|
41
|
+
emitter.on("startReconnect", () => {
|
40
42
|
releaseView(this.mainView);
|
41
|
-
})
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
43
|
+
}),
|
44
|
+
]);
|
45
|
+
this.createViewSync();
|
46
|
+
this.sideEffectManager.add(() => emitter.on("focusedChange", ({ focused }) => {
|
47
|
+
if (focused === undefined) {
|
48
|
+
const scenePath = this.store.getMainViewScenePath();
|
49
|
+
if (scenePath) {
|
50
|
+
setScenePath(this.manager.room, scenePath);
|
51
|
+
}
|
52
|
+
}
|
53
|
+
}));
|
54
|
+
this.size$.reaction(size => {
|
55
|
+
if (size) {
|
56
|
+
callbacks.emit("baseSizeChange", size);
|
57
|
+
}
|
52
58
|
});
|
53
59
|
}
|
54
60
|
|
61
|
+
public createViewSync = () => {
|
62
|
+
// 滚动模式下,不需要同步
|
63
|
+
if (this.manager.windowManger.viewMode$.value === 'scroll') return
|
64
|
+
if (this.manager.boxManager && !this.viewSync) {
|
65
|
+
this.viewSync = new ViewSync({
|
66
|
+
uid: this.manager.uid,
|
67
|
+
view$: this.view$,
|
68
|
+
camera$: this.camera$,
|
69
|
+
size$: this.size$,
|
70
|
+
stageRect$: this.manager.boxManager?.stageRect$,
|
71
|
+
viewMode$: this.manager.windowManger.viewMode$,
|
72
|
+
storeCamera: this.storeCamera,
|
73
|
+
storeSize: this.storeSize,
|
74
|
+
});
|
75
|
+
}
|
76
|
+
};
|
77
|
+
|
55
78
|
private startListenWritableChange = () => {
|
56
|
-
this.sideEffectManager.add(() =>
|
57
|
-
|
79
|
+
this.sideEffectManager.add(() =>
|
80
|
+
emitter.on("writableChange", isWritable => {
|
58
81
|
if (isWritable) {
|
59
82
|
this.ensureCameraAndSize();
|
60
83
|
}
|
61
|
-
})
|
62
|
-
|
84
|
+
})
|
85
|
+
);
|
63
86
|
};
|
64
87
|
|
65
88
|
public ensureCameraAndSize() {
|
66
89
|
if (!this.mainViewCamera || !this.mainViewSize) {
|
67
90
|
this.manager.dispatchInternalEvent(Events.InitMainViewCamera);
|
68
|
-
this.
|
91
|
+
this.storeCamera({
|
92
|
+
id: this.manager.uid,
|
93
|
+
...this.view.camera
|
94
|
+
});
|
95
|
+
// FIX 没有 mainViewSize 需要初始化一个 baseSize
|
96
|
+
const stageRect = this.manager.boxManager?.stageRect;
|
97
|
+
if (stageRect && !this.mainViewSize) {
|
98
|
+
this.storeSize({
|
99
|
+
id: this.manager.uid,
|
100
|
+
width: stageRect.width,
|
101
|
+
height: stageRect.height
|
102
|
+
});
|
103
|
+
}
|
69
104
|
}
|
70
105
|
}
|
71
106
|
|
107
|
+
public moveCamera = (camera: MoveCameraParams) => {
|
108
|
+
this.debouncedStoreCamera();
|
109
|
+
this.moveCameraToPromise(camera);
|
110
|
+
};
|
111
|
+
|
112
|
+
public moveCameraToPromise = (camera: MoveCameraParams) => {
|
113
|
+
const promise = new Promise<boolean>((resolve) => {
|
114
|
+
const cameraListener = debounce(() => {
|
115
|
+
this.mainView.callbacks.off("onCameraUpdated", cameraListener);
|
116
|
+
this.cameraUpdatePromise = undefined;
|
117
|
+
resolve(true);
|
118
|
+
}, 50);
|
119
|
+
this.mainView.callbacks.on("onCameraUpdated", cameraListener);
|
120
|
+
this.mainView.moveCamera(camera);
|
121
|
+
});
|
122
|
+
this.cameraUpdatePromise = promise;
|
123
|
+
return promise;
|
124
|
+
}
|
125
|
+
|
126
|
+
private debouncedStoreCamera = () => {
|
127
|
+
this.storeCurrentSize();
|
128
|
+
const cameraListener = debounce(() => {
|
129
|
+
this.saveToCamera$();
|
130
|
+
this.storeCurrentCameraSize();
|
131
|
+
this.mainView.callbacks.off("onCameraUpdated", cameraListener);
|
132
|
+
}, 50);
|
133
|
+
this.mainView.callbacks.on("onCameraUpdated", cameraListener);
|
134
|
+
}
|
135
|
+
|
136
|
+
private storeCurrentCameraSize = debounce(async () => {
|
137
|
+
if (this.cameraUpdatePromise) {
|
138
|
+
await this.cameraUpdatePromise;
|
139
|
+
}
|
140
|
+
this.storeCurrentCamera();
|
141
|
+
this.storeCurrentSize();
|
142
|
+
}, 500);
|
143
|
+
|
72
144
|
private get mainViewCamera() {
|
73
145
|
return this.store.getMainViewCamera();
|
74
146
|
}
|
@@ -81,55 +153,82 @@ export class MainViewProxy {
|
|
81
153
|
return get(this.view, ["didRelease"]);
|
82
154
|
}
|
83
155
|
|
84
|
-
private moveCameraSizeByAttributes() {
|
85
|
-
this.synchronizer.onRemoteUpdate(this.mainViewCamera, this.mainViewSize);
|
86
|
-
}
|
87
|
-
|
88
156
|
public start() {
|
89
157
|
if (this.started) return;
|
90
|
-
this.
|
158
|
+
this.removeCameraListener();
|
91
159
|
this.addCameraListener();
|
92
160
|
this.addCameraReaction();
|
93
161
|
this.started = true;
|
94
162
|
}
|
95
163
|
|
96
164
|
public addCameraReaction = () => {
|
97
|
-
this.manager.refresher
|
165
|
+
this.manager.refresher.add(Fields.MainViewCamera, this.cameraReaction);
|
166
|
+
this.manager.refresher.add(Fields.MainViewSize, this.sizeReaction);
|
98
167
|
};
|
99
168
|
|
100
|
-
public
|
101
|
-
const
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
169
|
+
public saveToCamera$ = () => {
|
170
|
+
const camera = { ...this.view.camera, id: this.manager.uid };
|
171
|
+
this.camera$.setValue(camera, true);
|
172
|
+
}
|
173
|
+
|
174
|
+
public storeCurrentCamera = () => {
|
175
|
+
const iCamera = this.view.camera;
|
176
|
+
this.storeCamera({
|
177
|
+
id: this.manager.uid,
|
178
|
+
...iCamera
|
179
|
+
});
|
180
|
+
}
|
181
|
+
|
182
|
+
public storeCurrentSize = () => {
|
183
|
+
const rect = this.manager.boxManager?.stageRect;
|
184
|
+
if (rect) {
|
185
|
+
const size = {
|
186
|
+
id: this.manager.uid,
|
187
|
+
width: rect.width,
|
188
|
+
height: rect.height
|
189
|
+
}
|
190
|
+
if (!isEqual(size, this.mainViewSize)) {
|
191
|
+
this.storeSize(size);
|
192
|
+
}
|
106
193
|
}
|
107
194
|
}
|
108
195
|
|
196
|
+
public storeCamera = (camera: ICamera) => {
|
197
|
+
this.store.setMainViewCamera(camera);
|
198
|
+
};
|
199
|
+
|
200
|
+
public storeSize = (size: ISize) => {
|
201
|
+
this.store.setMainViewSize(size);
|
202
|
+
};
|
203
|
+
|
109
204
|
private cameraReaction = () => {
|
110
205
|
return reaction(
|
111
206
|
() => this.mainViewCamera,
|
112
207
|
camera => {
|
113
|
-
if (camera
|
114
|
-
|
208
|
+
if (camera) {
|
209
|
+
const rawCamera = toJS(camera);
|
210
|
+
if (!isEqual(rawCamera, this.camera$.value)) {
|
211
|
+
this.camera$.setValue(rawCamera);
|
212
|
+
}
|
115
213
|
}
|
116
214
|
},
|
117
215
|
{ fireImmediately: true }
|
118
216
|
);
|
119
217
|
};
|
120
218
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
219
|
+
private sizeReaction = () => {
|
220
|
+
return reaction(
|
221
|
+
() => this.mainViewSize,
|
222
|
+
size => {
|
223
|
+
if (size) {
|
224
|
+
const rawSize = toJS(size);
|
225
|
+
if (!isEqual(rawSize, this.size$.value)) {
|
226
|
+
this.size$.setValue(rawSize);
|
227
|
+
}
|
228
|
+
}
|
229
|
+
},
|
230
|
+
{ fireImmediately: true }
|
231
|
+
);
|
133
232
|
};
|
134
233
|
|
135
234
|
public get view(): View {
|
@@ -146,7 +245,7 @@ export class MainViewProxy {
|
|
146
245
|
if (mainViewScenePath) {
|
147
246
|
setViewFocusScenePath(mainView, mainViewScenePath);
|
148
247
|
}
|
149
|
-
this.
|
248
|
+
this.view$.setValue(mainView);
|
150
249
|
return mainView;
|
151
250
|
}
|
152
251
|
|
@@ -168,31 +267,18 @@ export class MainViewProxy {
|
|
168
267
|
public rebind(): void {
|
169
268
|
const divElement = this.mainView.divElement;
|
170
269
|
const disableCameraTransform = this.mainView.disableCameraTransform;
|
270
|
+
const camera = { ...this.mainView.camera };
|
171
271
|
this.stop();
|
172
272
|
releaseView(this.mainView);
|
173
273
|
this.removeMainViewListener();
|
174
274
|
this.mainView = this.createMainView();
|
175
275
|
this.mainView.disableCameraTransform = disableCameraTransform;
|
176
276
|
this.mainView.divElement = divElement;
|
277
|
+
this.mainView.moveCamera({ ...camera, animationMode: AnimationMode.Immediately });
|
177
278
|
this.addMainViewListener();
|
178
279
|
this.start();
|
179
280
|
}
|
180
281
|
|
181
|
-
private onCameraUpdatedByDevice = (camera: Camera) => {
|
182
|
-
this.synchronizer.onLocalCameraUpdate(camera);
|
183
|
-
const size = this.getStageSize();
|
184
|
-
if (size && !isEqual(size, this.mainViewSize)) {
|
185
|
-
this.setMainViewSize(size);
|
186
|
-
}
|
187
|
-
};
|
188
|
-
|
189
|
-
private getStageSize(): Size | undefined {
|
190
|
-
const stage = this.manager.boxManager?.stageRect;
|
191
|
-
if (stage) {
|
192
|
-
return { width: stage.width, height: stage.height };
|
193
|
-
}
|
194
|
-
}
|
195
|
-
|
196
282
|
public addMainViewListener(): void {
|
197
283
|
if (this.mainViewIsAddListener) return;
|
198
284
|
if (this.view.divElement) {
|
@@ -225,13 +311,11 @@ export class MainViewProxy {
|
|
225
311
|
}, 50);
|
226
312
|
|
227
313
|
private addCameraListener() {
|
228
|
-
this.view.callbacks.on("onCameraUpdatedByDevice", this.onCameraUpdatedByDevice);
|
229
314
|
this.view.callbacks.on("onCameraUpdated", this.onCameraOrSizeUpdated);
|
230
315
|
this.view.callbacks.on("onSizeUpdated", this.onCameraOrSizeUpdated);
|
231
316
|
}
|
232
317
|
|
233
318
|
private removeCameraListener() {
|
234
|
-
this.view.callbacks.off("onCameraUpdatedByDevice", this.onCameraUpdatedByDevice);
|
235
319
|
this.view.callbacks.off("onCameraUpdated", this.onCameraOrSizeUpdated);
|
236
320
|
this.view.callbacks.off("onSizeUpdated", this.onCameraOrSizeUpdated);
|
237
321
|
}
|
@@ -241,13 +325,15 @@ export class MainViewProxy {
|
|
241
325
|
};
|
242
326
|
|
243
327
|
public stop() {
|
244
|
-
this.
|
245
|
-
this.manager.refresher
|
246
|
-
this.manager.refresher?.remove(Fields.MainViewSize);
|
328
|
+
this.manager.refresher.remove(Fields.MainViewCamera);
|
329
|
+
this.manager.refresher.remove(Fields.MainViewSize);
|
247
330
|
this.started = false;
|
248
331
|
}
|
249
332
|
|
250
333
|
public destroy() {
|
334
|
+
this.camera$.destroy();
|
335
|
+
this.size$.destroy();
|
336
|
+
this.view$.destroy();
|
251
337
|
this.removeMainViewListener();
|
252
338
|
this.stop();
|
253
339
|
this.sideEffectManager.flushAll();
|
@@ -0,0 +1,240 @@
|
|
1
|
+
import { AnimationMode } from "white-web-sdk";
|
2
|
+
import { callbacks } from "../callback";
|
3
|
+
import { combine, derive, Val } from "value-enhancer";
|
4
|
+
import { createScrollStorage } from "../storage";
|
5
|
+
import { SCROLL_MODE_BASE_HEIGHT, SCROLL_MODE_BASE_WIDTH } from "../constants";
|
6
|
+
import { SideEffectManager } from "side-effect-manager";
|
7
|
+
import { round } from "lodash";
|
8
|
+
import type { ReadonlyVal } from "value-enhancer";
|
9
|
+
import type { AppManager } from "../AppManager";
|
10
|
+
import type { ScrollStorage } from "../storage";
|
11
|
+
import type { Camera, Size, View } from "white-web-sdk";
|
12
|
+
|
13
|
+
function clamp(x: number, min: number, max: number): number {
|
14
|
+
return x < min ? min : x > max ? max : x;
|
15
|
+
}
|
16
|
+
|
17
|
+
export type ScrollState = {
|
18
|
+
scrollTop: number;
|
19
|
+
page: number;
|
20
|
+
maxScrollPage: number;
|
21
|
+
};
|
22
|
+
|
23
|
+
export class ScrollMode {
|
24
|
+
public readonly sideEffect = new SideEffectManager();
|
25
|
+
|
26
|
+
private readonly _root$: Val<HTMLElement | null>;
|
27
|
+
private readonly _whiteboard$: ReadonlyVal<HTMLElement | null>;
|
28
|
+
private readonly _scrollTop$: Val<number>;
|
29
|
+
public readonly _page$: ReadonlyVal<number>;
|
30
|
+
private readonly _scale$: ReadonlyVal<number>;
|
31
|
+
private readonly _size$: Val<Size>;
|
32
|
+
private readonly _mainView$: Val<View>;
|
33
|
+
|
34
|
+
private baseWidth = SCROLL_MODE_BASE_WIDTH;
|
35
|
+
private baseHeight = SCROLL_MODE_BASE_HEIGHT;
|
36
|
+
|
37
|
+
public scrollStorage: ScrollStorage;
|
38
|
+
public readonly scrollState$: ReadonlyVal<ScrollState>;
|
39
|
+
|
40
|
+
public setRoot(root: HTMLElement): void {
|
41
|
+
this._root$.setValue(root);
|
42
|
+
}
|
43
|
+
|
44
|
+
constructor(private manager: AppManager) {
|
45
|
+
this._root$ = new Val<HTMLElement | null>(null);
|
46
|
+
this._mainView$ = new Val<View>(this.manager.mainView);
|
47
|
+
// 滚动模式下确保 disableCameraTransform 为 false, 否则触摸屏无法滚动
|
48
|
+
this._mainView$.value.disableCameraTransform = false;
|
49
|
+
|
50
|
+
if (manager.scrollBaseSize$?.value) {
|
51
|
+
this.baseWidth = manager.scrollBaseSize$.value.width;
|
52
|
+
this.baseHeight = manager.scrollBaseSize$.value.height;
|
53
|
+
}
|
54
|
+
|
55
|
+
this.scrollStorage = createScrollStorage(manager);
|
56
|
+
const scrollTop$ = new Val(this.scrollStorage.state.scrollTop);
|
57
|
+
this._scrollTop$ = scrollTop$;
|
58
|
+
|
59
|
+
this.sideEffect.push(
|
60
|
+
this.scrollStorage.on("stateChanged", () => {
|
61
|
+
this._scrollTop$.setValue(this.scrollStorage.state.scrollTop);
|
62
|
+
})
|
63
|
+
);
|
64
|
+
|
65
|
+
const size$ = new Val<Size>(
|
66
|
+
{ width: 0, height: 0 },
|
67
|
+
{ compare: (a, b) => a.width === b.width && a.height === b.height }
|
68
|
+
);
|
69
|
+
this._size$ = size$;
|
70
|
+
this.sideEffect.add(() => {
|
71
|
+
const onSizeUpdated = size$.setValue.bind(size$);
|
72
|
+
onSizeUpdated(this._mainView$.value.size);
|
73
|
+
this._mainView$.value.callbacks.on("onSizeUpdated", onSizeUpdated);
|
74
|
+
return () => this._mainView$.value.callbacks.off("onSizeUpdated", onSizeUpdated);
|
75
|
+
});
|
76
|
+
|
77
|
+
this.sideEffect.add(() => {
|
78
|
+
const onCameraUpdated = (camera: Camera): void => {
|
79
|
+
if (!this.manager.canOperate) return;
|
80
|
+
const halfWbHeight = size$.value.height / 2 / scale$.value;
|
81
|
+
const scrollTop = camera.centerY;
|
82
|
+
this.scrollStorage.setState({
|
83
|
+
scrollTop: clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight),
|
84
|
+
});
|
85
|
+
callbacks.emit("userScroll");
|
86
|
+
};
|
87
|
+
this._mainView$.value.callbacks.on("onCameraUpdatedByDevice", onCameraUpdated);
|
88
|
+
return () =>
|
89
|
+
this._mainView$.value.callbacks.off("onCameraUpdatedByDevice", onCameraUpdated);
|
90
|
+
});
|
91
|
+
|
92
|
+
const scale$ = derive(size$, size => size.width / this.baseWidth);
|
93
|
+
this._scale$ = scale$;
|
94
|
+
|
95
|
+
const page$ = new Val(0);
|
96
|
+
this.sideEffect.push(
|
97
|
+
combine([scrollTop$, size$, scale$]).subscribe(([scrollTop, size, scale]) => {
|
98
|
+
if (scale > 0) {
|
99
|
+
const wbHeight = size.height / scale;
|
100
|
+
page$.setValue(Math.max(scrollTop / wbHeight - 0.5, 0));
|
101
|
+
}
|
102
|
+
})
|
103
|
+
);
|
104
|
+
this._page$ = page$;
|
105
|
+
|
106
|
+
// 5. bound$ = { contentMode: () => scale$, centerX: W / 2, centerY: H / 2, width: W, height: H }
|
107
|
+
this.sideEffect.push(
|
108
|
+
combine([scrollTop$, scale$]).subscribe(([scrollTop, scale]) => {
|
109
|
+
this.updateBound(scrollTop, size$.value, scale);
|
110
|
+
})
|
111
|
+
);
|
112
|
+
|
113
|
+
this.sideEffect.push(
|
114
|
+
size$.reaction(() => {
|
115
|
+
this.updateScroll(scrollTop$.value);
|
116
|
+
})
|
117
|
+
);
|
118
|
+
|
119
|
+
const whiteboard$ = derive(this._root$, this.getWhiteboardElement);
|
120
|
+
this._whiteboard$ = whiteboard$;
|
121
|
+
this.sideEffect.push(
|
122
|
+
whiteboard$.reaction(el => {
|
123
|
+
if (el?.parentElement) {
|
124
|
+
this.sideEffect.addEventListener(
|
125
|
+
el.parentElement,
|
126
|
+
"wheel",
|
127
|
+
this.onWheel,
|
128
|
+
{ capture: true, passive: false },
|
129
|
+
"wheel"
|
130
|
+
);
|
131
|
+
}
|
132
|
+
})
|
133
|
+
);
|
134
|
+
|
135
|
+
const maxScrollPage$ = combine([this._size$, this._scale$], ([size, scale]) => {
|
136
|
+
const halfWbHeight = size.height / 2 / scale;
|
137
|
+
return (this.baseHeight - halfWbHeight) / halfWbHeight / 2 - 0.51;
|
138
|
+
});
|
139
|
+
|
140
|
+
this.scrollState$ = combine(
|
141
|
+
[this._scrollTop$, this._page$, maxScrollPage$],
|
142
|
+
([scrollTop, page, maxScrollPage]) => {
|
143
|
+
return {
|
144
|
+
scrollTop: round(scrollTop, 2),
|
145
|
+
page: round(page, 2),
|
146
|
+
maxScrollPage: round(maxScrollPage, 2),
|
147
|
+
};
|
148
|
+
}
|
149
|
+
);
|
150
|
+
|
151
|
+
this.updateScroll(scrollTop$.value);
|
152
|
+
this.sideEffect.push(
|
153
|
+
this.scrollState$.subscribe(state => callbacks.emit("scrollStateChange", state))
|
154
|
+
);
|
155
|
+
|
156
|
+
this.sideEffect.push(
|
157
|
+
combine([this._size$, this._scale$]).subscribe(([size, scale]) => {
|
158
|
+
if (size.height > 0 && scale > 0) {
|
159
|
+
this.initScroll();
|
160
|
+
this.sideEffect.flush("initScroll");
|
161
|
+
}
|
162
|
+
}),
|
163
|
+
"initScroll"
|
164
|
+
);
|
165
|
+
}
|
166
|
+
|
167
|
+
private initScroll = (): void => {
|
168
|
+
const halfWbHeight = this._size$.value.height / 2 / this._scale$.value;
|
169
|
+
const scrollTop = this._scrollTop$.value;
|
170
|
+
// HACK: set a different value (+0.01) to trigger all effects above
|
171
|
+
this._scrollTop$.setValue(
|
172
|
+
clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight) - 0.01
|
173
|
+
);
|
174
|
+
};
|
175
|
+
|
176
|
+
private updateScroll(scrollTop: number): void {
|
177
|
+
this._mainView$.value.moveCamera({
|
178
|
+
centerY: scrollTop,
|
179
|
+
animationMode: AnimationMode.Immediately,
|
180
|
+
});
|
181
|
+
}
|
182
|
+
|
183
|
+
private updateBound(scrollTop: number, { height }: Size, scale: number): void {
|
184
|
+
if (scale > 0) {
|
185
|
+
this._mainView$.value.moveCameraToContain({
|
186
|
+
originX: 0,
|
187
|
+
originY: scrollTop - height / scale / 2,
|
188
|
+
width: this.baseWidth,
|
189
|
+
height: height / scale,
|
190
|
+
animationMode: AnimationMode.Immediately,
|
191
|
+
});
|
192
|
+
|
193
|
+
this._mainView$.value.setCameraBound({
|
194
|
+
damping: 1,
|
195
|
+
maxContentMode: () => scale,
|
196
|
+
minContentMode: () => scale,
|
197
|
+
centerX: this.baseWidth / 2,
|
198
|
+
centerY: this.baseHeight / 2,
|
199
|
+
width: this.baseWidth,
|
200
|
+
height: this.baseHeight,
|
201
|
+
});
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
public dispose(): void {
|
206
|
+
this.sideEffect.flushAll();
|
207
|
+
this.scrollStorage.disconnect();
|
208
|
+
this._root$.destroy();
|
209
|
+
this._scale$.destroy();
|
210
|
+
this._scrollTop$.destroy();
|
211
|
+
this._whiteboard$.destroy();
|
212
|
+
this.scrollState$.destroy();
|
213
|
+
this._page$.destroy();
|
214
|
+
this._size$.destroy();
|
215
|
+
this._mainView$.destroy();
|
216
|
+
}
|
217
|
+
|
218
|
+
private getWhiteboardElement = (root: HTMLElement | null): HTMLElement | null => {
|
219
|
+
const className = ".netless-window-manager-main-view";
|
220
|
+
return root && root.querySelector(className);
|
221
|
+
};
|
222
|
+
|
223
|
+
private onWheel = (ev: WheelEvent): void => {
|
224
|
+
const target = ev.target as HTMLElement | null;
|
225
|
+
if (this.manager.canOperate && this._whiteboard$.value?.contains(target)) {
|
226
|
+
ev.preventDefault();
|
227
|
+
ev.stopPropagation();
|
228
|
+
const dy = ev.deltaY || 0;
|
229
|
+
const { width } = this._size$.value;
|
230
|
+
if (dy && width > 0) {
|
231
|
+
const halfWbHeight = this._size$.value.height / 2 / this._scale$.value;
|
232
|
+
const scrollTop = this._scrollTop$.value + dy / this._scale$.value;
|
233
|
+
this.scrollStorage.setState({
|
234
|
+
scrollTop: clamp(scrollTop, halfWbHeight, this.baseHeight - halfWbHeight),
|
235
|
+
});
|
236
|
+
callbacks.emit("userScroll");
|
237
|
+
}
|
238
|
+
}
|
239
|
+
};
|
240
|
+
}
|