Skip to content

Commit d57c1c7

Browse files
committed
widgets: work in progress
1 parent dc770c9 commit d57c1c7

File tree

6 files changed

+124
-43
lines changed

6 files changed

+124
-43
lines changed

src/packages/frontend/jupyter/widgets/k3d/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The contents of this directory is just a rewrite of https://github.com/K3D-tools/K3D-jupyter/blob/main/js/src/k3d.js using Typescript and more modularity, and also using ES6 classes which is more "future proof" regarding Backbone.js and newer versions of Ipywidgets.
22

3-
The license is all code in this directory is the same as for upstream, namely the MIT license as given here:
3+
The license for all code in this directory is the same as for upstream, namely the MIT license as given here:
44

55
https://github.com/K3D-tools/K3D-jupyter/blob/main/LICENSE.txt
66

src/packages/frontend/jupyter/widgets/manager.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import * as react_output from "./output";
1616
import * as react_controls from "./controls";
1717
import { size } from "lodash";
1818
import { delay } from "awaiting";
19-
//import * as k3d from "./k3d";
19+
import * as k3d from "./k3d";
2020

2121
/*
2222
NOTES: Third party custom widgets:
@@ -590,9 +590,9 @@ export class WidgetManager extends base.ManagerBase<HTMLElement> {
590590
} else if (moduleName === "@jupyter-widgets/output") {
591591
module = react_output;
592592
} else if (moduleName === "k3d") {
593-
// NOTE: I completely rewrote the entire k3d widget interface...
594-
//module = k3d;
595-
throw Error("k3d disabled");
593+
// NOTE: I completely rewrote the entire k3d widget interface, since
594+
// it made tons of assumptions that break RTC.
595+
module = k3d;
596596
} else if (moduleName === "jupyter-matplotlib") {
597597
//module = await import("jupyter-matplotlib");
598598
throw Error(`custom widgets: ${moduleName} not installed`);

src/packages/frontend/jupyter/widgets/manager2.ts

Lines changed: 85 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
ModelState,
1818
} from "@cocalc/sync/editor/generic/ipywidgets-state";
1919
import { once } from "@cocalc/util/async-utils";
20-
import { copy, is_array, is_object, len, uuid } from "@cocalc/util/misc";
20+
import { is_array, is_object, len, uuid } from "@cocalc/util/misc";
2121
import { fromJS } from "immutable";
2222
import { CellOutputMessage } from "@cocalc/frontend/jupyter/output-messages/message";
2323
import React from "react";
@@ -116,16 +116,17 @@ export class WidgetManager {
116116
if (state == null) {
117117
return;
118118
}
119-
await this.updateModel(model_id, state!);
119+
await this.updateModel(model_id, state!, false);
120120
};
121121

122122
private updateModel = async (
123123
model_id: string,
124124
changed: ModelState,
125+
merge: boolean,
125126
): Promise<void> => {
126127
const model: base.DOMWidgetModel | undefined =
127128
await this.manager.get_model(model_id);
128-
log("updateModel", { model, changed });
129+
log("updateModel", { model_id, merge, changed });
129130
if (model == null) {
130131
return;
131132
}
@@ -140,17 +141,33 @@ export class WidgetManager {
140141
);
141142
return;
142143
}
143-
const state = await this.deserializeState(model, changed);
144-
if (state.hasOwnProperty("outputs") && state["outputs"] == null) {
144+
changed = await this.deserializeState(model, changed);
145+
if (changed.hasOwnProperty("outputs") && changed["outputs"] == null) {
145146
// It can definitely be 'undefined' but set, e.g., the 'out.clear_output()' example at
146147
// https://ipywidgets.readthedocs.io/en/latest/examples/Output%20Widget.html
147148
// causes this, which then totally breaks rendering (due to how the
148149
// upstream widget manager works). This works around that.
149-
state["outputs"] = [];
150+
changed["outputs"] = [];
151+
}
152+
if (merge) {
153+
const state = model.get_state(false);
154+
const x: ModelState = {};
155+
for (const k in changed) {
156+
if (state[k] != null && is_object(state[k]) && is_object(changed[k])) {
157+
x[k] = { ...state[k], ...changed[k] };
158+
} else {
159+
x[k] = changed[k];
160+
}
161+
}
162+
changed = x;
163+
}
164+
log("updateModel -- doing set_state", { model_id, merge, changed });
165+
try {
166+
model.set(changed);
167+
} catch (err) {
168+
//window.z = { merge, model, model_id, changed };
169+
console.error("saved to z", err);
150170
}
151-
152-
log("set_state", state);
153-
model.set_state(state);
154171
};
155172

156173
// ipywidgets_state_ValueChange is called when a value entry of the ipywidgets_state
@@ -192,7 +209,7 @@ export class WidgetManager {
192209
}
193210
this.state_lock.add(model_id);
194211
log("handleValueChange: got model and now making this change -- ", changed);
195-
await this.updateModel(model_id, changed);
212+
await this.updateModel(model_id, changed, true);
196213
const model = await this.manager.get_model(model_id);
197214
if (model != null) {
198215
await model.state_change;
@@ -239,20 +256,31 @@ export class WidgetManager {
239256
A simple example that uses buffers is this image one:
240257
https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20List.html#image
241258
*/
242-
const model = await this.manager.get_model(model_id);
243259
const { buffer_paths, buffers } =
244260
await this.ipywidgets_state.get_model_buffers(model_id);
245261
log("handleBuffersChange: ", { model_id, buffer_paths, buffers });
246-
const deserialized_state = model.get_state(true);
262+
if (buffer_paths.length == 0) {
263+
return;
264+
}
265+
const state = this.ipywidgets_state.get_model_state(model_id);
266+
if (state == null) {
267+
return;
268+
}
247269
const change: { [key: string]: any } = {};
248270
for (let i = 0; i < buffer_paths.length; i++) {
249271
const key = buffer_paths[i][0];
250-
setInObject(deserialized_state[key], buffer_paths[i], buffers[i]);
251-
change[key] = deserialized_state[key];
272+
setInObject(state, buffer_paths[i], buffers[i]);
273+
change[key] = state[key];
252274
}
253-
log("handleBuffersChange: ", model_id, change);
275+
log("handleBuffersChange: ", model_id, { change });
254276
if (len(change) > 0) {
255-
model.set_state(change);
277+
const model = await this.manager.get_model(model_id);
278+
try {
279+
model.set(change);
280+
} catch (err) {
281+
// window.y = { model_id, model, change, buffer_paths, buffers };
282+
console.error("saved to y", err);
283+
}
256284
}
257285
};
258286

@@ -301,18 +329,20 @@ export class WidgetManager {
301329
// ipywidgets_state, so that it is gets sync'd to the backend
302330
// and any other clients.
303331
private handleModelChange = async (model): Promise<void> => {
304-
log("handleModelChange", model);
305332
const { model_id } = model;
333+
let changed = model.changed;
334+
log("handleModelChange", model_id, changed);
306335
await model.state_change;
307336
if (this.state_lock.has(model_id)) {
308-
// log("handleModelChange: ignoring change due to state lock");
337+
log("handleModelChange: ignoring change due to state lock");
309338
return;
310339
}
311-
const changed: any = copy(model.serialize(model.changed));
340+
changed = model.serialize(changed);
312341
delete changed.children; // sometimes they are in there, but shouldn't be sync'ed.
313342
const { last_changed } = changed;
314343
delete changed.last_changed;
315344
if (len(changed) == 0) {
345+
log("handleModelChange: nothing changed");
316346
return; // nothing
317347
}
318348
// increment sequence number.
@@ -543,6 +573,18 @@ class Environment implements WidgetEnvironment {
543573
this.manager = manager;
544574
}
545575

576+
async loadClass(
577+
className: string,
578+
moduleName: string,
579+
_moduleVersion: string,
580+
): Promise<any> {
581+
if (false && moduleName === "k3d") {
582+
// NOTE: I completely rewrote the entire k3d widget interface...
583+
console.log("using builtin k3d");
584+
return await import("k3d")[className];
585+
}
586+
}
587+
546588
async getModelState(model_id) {
547589
// log("getModelState", model_id);
548590
if (this.manager.ipywidgets_state.get_state() != "ready") {
@@ -556,6 +598,23 @@ class Environment implements WidgetEnvironment {
556598
state = this.manager.ipywidgets_state.get_model_state(model_id);
557599
}
558600
}
601+
if (state == null) {
602+
throw Error("bug");
603+
}
604+
if (state._model_module == "k3d" && state.type != null) {
605+
while (!state?.type || !state?.id) {
606+
log(
607+
"getModelState",
608+
model_id,
609+
"k3d: waiting for state.type to be defined",
610+
);
611+
await once(this.manager.ipywidgets_state, "change");
612+
state = this.manager.ipywidgets_state.get_model_state(model_id);
613+
}
614+
}
615+
if (state == null) {
616+
throw Error("bug");
617+
}
559618
if (state.hasOwnProperty("outputs") && state["outputs"] == null) {
560619
// It can definitely be 'undefined' but set, e.g., the 'out.clear_output()' example at
561620
// https://ipywidgets.readthedocs.io/en/latest/examples/Output%20Widget.html
@@ -566,12 +625,15 @@ class Environment implements WidgetEnvironment {
566625
const { buffer_paths, buffers } =
567626
await this.manager.ipywidgets_state.get_model_buffers(model_id);
568627

569-
for (let i = 0; i < buffer_paths.length; i++) {
570-
const buffer = buffers[i];
571-
setInObject(state, buffer_paths[i], buffer);
628+
if (buffers.length > 0) {
629+
for (let i = 0; i < buffer_paths.length; i++) {
630+
const buffer = buffers[i];
631+
setInObject(state, buffer_paths[i], buffer);
632+
}
572633
}
573-
574634
setTimeout(() => this.manager.watchModel(model_id), 1);
635+
636+
log("getModelState", { model_id, state });
575637
return {
576638
modelName: state._model_name,
577639
modelModule: state._model_module,

src/packages/frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"@cocalc/local-storage-lru": "^2.4.3",
4646
"@cocalc/sync": "workspace:*",
4747
"@cocalc/util": "workspace:*",
48-
"@cocalc/widgets": "^1.1.0",
48+
"@cocalc/widgets": "^1.1.1",
4949
"@cocalc/xpra-lz4": "^1.1.0",
5050
"@dnd-kit/core": "^6.0.7",
5151
"@dnd-kit/modifiers": "^6.0.1",

src/packages/pnpm-lock.yaml

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/packages/sync/editor/generic/ipywidgets-state.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Map as iMap } from "immutable";
1414
import {
1515
close,
1616
delete_null_fields,
17+
is_object,
1718
len,
1819
auxFileToOriginal,
1920
} from "@cocalc/util/misc";
@@ -350,20 +351,28 @@ export class IpywidgetsState extends EventEmitter {
350351
fire_change_event: boolean = true,
351352
merge?: "none" | "shallow" | "deep",
352353
): void => {
354+
const dbg = this.dbg("set");
353355
const string_id = this.syncdoc.get_string_id();
354356
if (typeof data != "object") {
355357
throw Error("TypeError -- data must be a map");
356358
}
357359
let defaultMerge: "none" | "shallow" | "deep";
358360
if (type == "value") {
361+
//defaultMerge = "shallow";
359362
// we manually do the shallow merge only on the data field.
360-
const data0 = this.get_model_value(model_id);
361-
if (data0 != null) {
363+
const current = this.get_model_value(model_id);
364+
dbg("value: before", { data, current });
365+
if (current != null) {
362366
for (const k in data) {
363-
data0[k] = data[k];
367+
if (is_object(data[k]) && is_object(current[k])) {
368+
current[k] = { ...current[k], ...data[k] };
369+
} else {
370+
current[k] = data[k];
371+
}
364372
}
365-
data = data0;
373+
data = current;
366374
}
375+
dbg("value -- after", { merged: data });
367376
defaultMerge = "none";
368377
} else if (type == "buffers") {
369378
// we keep around the buffers that were
@@ -646,8 +655,18 @@ export class IpywidgetsState extends EventEmitter {
646655
this.sendCustomMessage(model_id, message, false);
647656
break;
648657

658+
case "echo_update":
659+
// just ignore echo_update -- it's a new ipywidgets 8 mechanism
660+
// for some level of RTC sync between clients -- we don't need that
661+
// since we have our own, obviously. Setting the env var
662+
// JUPYTER_WIDGETS_ECHO to 0 will disable these messages to slightly
663+
// reduce traffic.
664+
return;
665+
649666
case "update":
650-
if (state == null) return;
667+
if (state == null) {
668+
return;
669+
}
651670
dbg("method -- update");
652671
if (this.clear_output[model_id] && state.outputs != null) {
653672
// we are supposed to clear the output before inserting

0 commit comments

Comments
 (0)