chiark / gitweb /
Apply a bodge to arrange that if the user selects Custom from the game
[sgt-puzzles.git] / emcclib.js
1 /*
2  * emcclib.js: one of the Javascript components of an Emscripten-based
3  * web/Javascript front end for Puzzles.
4  *
5  * The other parts of this system live in emcc.c and emccpre.js.
6  *
7  * This file contains a set of Javascript functions which we insert
8  * into Emscripten's library object via the --js-library option; this
9  * allows us to provide JS code which can be called from the
10  * Emscripten-compiled C, mostly dealing with UI interaction of
11  * various kinds.
12  */
13
14 mergeInto(LibraryManager.library, {
15     /*
16      * void js_debug(const char *message);
17      *
18      * A function to write a diagnostic to the Javascript console.
19      * Unused in production, but handy in development.
20      */
21     js_debug: function(ptr) {
22         console.log(Pointer_stringify(ptr));
23     },
24
25     /*
26      * void js_error_box(const char *message);
27      *
28      * A wrapper around Javascript's alert(), so the C code can print
29      * simple error message boxes (e.g. when invalid data is entered
30      * in a configuration dialog).
31      */
32     js_error_box: function(ptr) {
33         alert(Pointer_stringify(ptr));
34     },
35
36     /*
37      * void js_remove_type_dropdown(void);
38      *
39      * Get rid of the drop-down list on the web page for selecting
40      * game presets. Called at setup time if the game back end
41      * provides neither presets nor configurability.
42      */
43     js_remove_type_dropdown: function() {
44         document.getElementById("gametype").style.display = "none";
45     },
46
47     /*
48      * void js_remove_solve_button(void);
49      *
50      * Get rid of the Solve button on the web page. Called at setup
51      * time if the game doesn't support an in-game solve function.
52      */
53     js_remove_solve_button: function() {
54         document.getElementById("solve").style.display = "none";
55     },
56
57     /*
58      * void js_add_preset(const char *name);
59      *
60      * Add a preset to the drop-down types menu. The provided text is
61      * the name of the preset. (The corresponding game_params stays on
62      * the C side and never comes out this far; we just pass a numeric
63      * index back to the C code when a selection is made.)
64      *
65      * The special 'Custom' preset is requested by passing NULL to
66      * this function, rather than the string "Custom", since in that
67      * case we need to do something special - see below.
68      */
69     js_add_preset: function(ptr) {
70         var name = (ptr == 0 ? "Custom" : Pointer_stringify(ptr));
71         var value = gametypeoptions.length;
72
73         var option = document.createElement("option");
74         option.value = value;
75         option.appendChild(document.createTextNode(name));
76         gametypeselector.appendChild(option);
77         gametypeoptions.push(option);
78
79         if (ptr == 0) {
80             // Create a _second_ element called 'Custom', which is
81             // hidden.
82             //
83             // Hiding this element (that is, setting it display:none)
84             // has the effect of making it not show up when the
85             // drop-down list is actually opened, but still show up
86             // when the item is selected.
87             //
88             // So what happens is that there's one element marked
89             // 'Custom' that the _user_ selects, but a second one to
90             // which we reset the dropdown after the config box
91             // returns (if we don't then turn out to select a
92             // different preset anyway). The point is that if the user
93             // has 'Custom' selected, but then wants to customise
94             // their settings a second time, we still get an onchange
95             // event when they select the Custom option again, which
96             // we wouldn't get if the browser thought it was already
97             // the selected one. But here, it's _not_ the selected
98             // option already; its invisible evil twin is selected.
99             option = document.createElement("option");
100             option.value = value;
101             option.appendChild(document.createTextNode(name));
102             option.style.display = "none";
103             gametypeselector.appendChild(option);
104             gametypehiddencustom = option;
105         }
106     },
107
108     /*
109      * int js_get_selected_preset(void);
110      *
111      * Return the index of the currently selected value in the type
112      * dropdown.
113      */
114     js_get_selected_preset: function() {
115         for (var i in gametypeoptions) {
116             if (gametypeoptions[i].selected) {
117                 return gametypeoptions[i].value;
118             }
119         }
120         return 0;
121     },
122
123     /*
124      * void js_select_preset(int n);
125      *
126      * Cause a different value to be selected in the type dropdown
127      * (for when the user selects values from the Custom configurer
128      * which turn out to exactly match a preset).
129      */
130     js_select_preset: function(n) {
131         if (gametypeoptions[n].value == gametypehiddencustom.value) {
132             // If we're asked to select the visible Custom option,
133             // select the invisible one instead. See comment above in
134             // js_add_preset.
135             gametypehiddencustom.selected = true;
136         } else {
137             gametypeoptions[n].selected = true;
138         }
139     },
140
141     /*
142      * void js_get_date_64(unsigned *p);
143      *
144      * Return the current date, in milliseconds since the epoch
145      * (Javascript's native format), as a 64-bit integer. Used to
146      * invent an initial random seed for puzzle generation.
147      */
148     js_get_date_64: function(ptr) {
149         var d = (new Date()).valueOf();
150         setValue(ptr, d, 'i64');
151     },
152
153     /*
154      * void js_update_permalinks(const char *desc, const char *seed);
155      *
156      * Update the permalinks on the web page for a new game
157      * description and optional random seed. desc can never be NULL,
158      * but seed might be (if the game was generated by entering a
159      * descriptive id by hand), in which case we suppress display of
160      * the random seed permalink.
161      */
162     js_update_permalinks: function(desc, seed) {
163         desc = Pointer_stringify(desc);
164         permalink_desc.href = "#" + desc;
165
166         if (seed == 0) {
167             permalink_seed.style.display = "none";
168         } else {
169             seed = Pointer_stringify(seed);
170             permalink_seed.href = "#" + seed;
171             permalink_seed.style.display = "inline";
172         }
173     },
174
175     /*
176      * void js_enable_undo_redo(int undo, int redo);
177      *
178      * Set the enabled/disabled states of the undo and redo buttons,
179      * after a move.
180      */
181     js_enable_undo_redo: function(undo, redo) {
182         undo_button.disabled = (undo == 0);
183         redo_button.disabled = (redo == 0);
184     },
185
186     /*
187      * void js_activate_timer();
188      *
189      * Start calling the C timer_callback() function every 20ms.
190      */
191     js_activate_timer: function() {
192         if (timer === null) {
193             timer_reference_date = (new Date()).valueOf();
194             timer = setInterval(function() {
195                 var now = (new Date()).valueOf();
196                 timer_callback((now - timer_reference_date) / 1000.0);
197                 timer_reference_date = now;
198                 return true;
199             }, 20);
200         }
201     },
202
203     /*
204      * void js_deactivate_timer();
205      *
206      * Stop calling the C timer_callback() function every 20ms.
207      */
208     js_deactivate_timer: function() {
209         if (timer !== null) {
210             clearInterval(timer);
211             timer = null;
212         }
213     },
214
215     /*
216      * void js_canvas_start_draw(void);
217      *
218      * Prepare to do some drawing on the canvas.
219      */
220     js_canvas_start_draw: function() {
221         ctx = offscreen_canvas.getContext('2d');
222         update_xmin = update_xmax = update_ymin = update_ymax = undefined;
223     },
224
225     /*
226      * void js_canvas_draw_update(int x, int y, int w, int h);
227      *
228      * Mark a rectangle of the off-screen canvas as needing to be
229      * copied to the on-screen one.
230      */
231     js_canvas_draw_update: function(x, y, w, h) {
232         /*
233          * Currently we do this in a really simple way, just by taking
234          * the smallest rectangle containing all updates so far. We
235          * could instead keep the data in a richer form (e.g. retain
236          * multiple smaller rectangles needing update, and only redraw
237          * the whole thing beyond a certain threshold) but this will
238          * do for now.
239          */
240         if (update_xmin === undefined || update_xmin > x) update_xmin = x;
241         if (update_ymin === undefined || update_ymin > y) update_ymin = y;
242         if (update_xmax === undefined || update_xmax < x+w) update_xmax = x+w;
243         if (update_ymax === undefined || update_ymax < y+h) update_ymax = y+h;
244     },
245
246     /*
247      * void js_canvas_end_draw(void);
248      *
249      * Finish the drawing, by actually copying the newly drawn stuff
250      * to the on-screen canvas.
251      */
252     js_canvas_end_draw: function() {
253         if (update_xmin !== undefined) {
254             var onscreen_ctx = onscreen_canvas.getContext('2d');
255             onscreen_ctx.drawImage(offscreen_canvas,
256                                    update_xmin, update_ymin,
257                                    update_xmax - update_xmin,
258                                    update_ymax - update_ymin,
259                                    update_xmin, update_ymin,
260                                    update_xmax - update_xmin,
261                                    update_ymax - update_ymin);
262         }
263         ctx = null;
264     },
265
266     /*
267      * void js_canvas_draw_rect(int x, int y, int w, int h,
268      *                          const char *colour);
269      * 
270      * Draw a rectangle.
271      */
272     js_canvas_draw_rect: function(x, y, w, h, colptr) {
273         ctx.fillStyle = Pointer_stringify(colptr);
274         ctx.fillRect(x, y, w, h);
275     },
276
277     /*
278      * void js_canvas_clip_rect(int x, int y, int w, int h);
279      * 
280      * Set a clipping rectangle.
281      */
282     js_canvas_clip_rect: function(x, y, w, h) {
283         ctx.save();
284         ctx.beginPath();
285         ctx.rect(x, y, w, h);
286         ctx.clip();
287     },
288
289     /*
290      * void js_canvas_unclip(void);
291      * 
292      * Reset to no clipping.
293      */
294     js_canvas_unclip: function() {
295         ctx.restore();
296     },
297
298     /*
299      * void js_canvas_draw_line(float x1, float y1, float x2, float y2,
300      *                          int width, const char *colour);
301      * 
302      * Draw a line. We must adjust the coordinates by 0.5 because
303      * Javascript's canvas coordinates appear to be pixel corners,
304      * whereas we want pixel centres. Also, we manually draw the pixel
305      * at each end of the line, which our clients will expect but
306      * Javascript won't reliably do by default (in common with other
307      * Postscriptish drawing frameworks).
308      */
309     js_canvas_draw_line: function(x1, y1, x2, y2, width, colour) {
310         colour = Pointer_stringify(colour);
311
312         ctx.beginPath();
313         ctx.moveTo(x1 + 0.5, y1 + 0.5);
314         ctx.lineTo(x2 + 0.5, y2 + 0.5);
315         ctx.lineWidth = width;
316         ctx.lineCap = '1';
317         ctx.lineJoin = '1';
318         ctx.strokeStyle = colour;
319         ctx.stroke();
320         ctx.fillStyle = colour;
321         ctx.fillRect(x1, y1, 1, 1);
322         ctx.fillRect(x2, y2, 1, 1);
323     },
324
325     /*
326      * void js_canvas_draw_poly(int *points, int npoints,
327      *                          const char *fillcolour,
328      *                          const char *outlinecolour);
329      * 
330      * Draw a polygon.
331      */
332     js_canvas_draw_poly: function(pointptr, npoints, fill, outline) {
333         ctx.beginPath();
334         ctx.moveTo(getValue(pointptr  , 'i32') + 0.5,
335                    getValue(pointptr+4, 'i32') + 0.5);
336         for (var i = 1; i < npoints; i++)
337             ctx.lineTo(getValue(pointptr+8*i  , 'i32') + 0.5,
338                        getValue(pointptr+8*i+4, 'i32') + 0.5);
339         ctx.closePath();
340         if (fill != 0) {
341             ctx.fillStyle = Pointer_stringify(fill);
342             ctx.fill();
343         }
344         ctx.lineWidth = '1';
345         ctx.lineCap = '1';
346         ctx.lineJoin = '1';
347         ctx.strokeStyle = Pointer_stringify(outline);
348         ctx.stroke();
349     },
350
351     /*
352      * void js_canvas_draw_circle(int x, int y, int r,
353      *                            const char *fillcolour,
354      *                            const char *outlinecolour);
355      * 
356      * Draw a circle.
357      */
358     js_canvas_draw_circle: function(x, y, r, fill, outline) {
359         ctx.beginPath();
360         ctx.arc(x + 0.5, y + 0.5, r, 0, 2*Math.PI);
361         if (fill != 0) {
362             ctx.fillStyle = Pointer_stringify(fill);
363             ctx.fill();
364         }
365         ctx.lineWidth = '1';
366         ctx.lineCap = '1';
367         ctx.lineJoin = '1';
368         ctx.strokeStyle = Pointer_stringify(outline);
369         ctx.stroke();
370     },
371
372     /*
373      * int js_canvas_find_font_midpoint(int height, const char *fontptr);
374      * 
375      * Return the adjustment required for text displayed using
376      * ALIGN_VCENTRE. We want to place the midpoint between the
377      * baseline and the cap-height at the specified position; so this
378      * function returns the adjustment which, when added to the
379      * desired centre point, returns the y-coordinate at which you
380      * should put the baseline.
381      *
382      * There is no sensible method of querying this kind of font
383      * metric in Javascript, so instead we render a piece of test text
384      * to a throwaway offscreen canvas and then read the pixel data
385      * back out to find the highest and lowest pixels. That's good
386      * _enough_ (in that we only needed the answer to the nearest
387      * pixel anyway), but rather disgusting!
388      *
389      * Since this is a very expensive operation, we cache the results
390      * per (font,height) pair.
391      */
392     js_canvas_find_font_midpoint: function(height, font) {
393         font = Pointer_stringify(font);
394
395         // Reuse cached value if possible
396         if (midpoint_cache[font] !== undefined)
397             return midpoint_cache[font];
398
399         // Find the width of the string
400         var ctx1 = onscreen_canvas.getContext('2d');
401         ctx1.font = font;
402         var width = ctx1.measureText(midpoint_test_str).width;
403
404         // Construct a test canvas of appropriate size, initialise it to
405         // black, and draw the string on it in white
406         var measure_canvas = document.createElement('canvas');
407         var ctx2 = measure_canvas.getContext('2d');
408         ctx2.canvas.width = width;
409         ctx2.canvas.height = 2*height;
410         ctx2.fillStyle = "#000000";
411         ctx2.fillRect(0, 0, width, 2*height);
412         var baseline = (1.5*height) | 0;
413         ctx2.fillStyle = "#ffffff";
414         ctx2.font = font;
415         ctx2.fillText(midpoint_test_str, 0, baseline);
416
417         // Scan the contents of the test canvas to find the top and bottom
418         // set pixels.
419         var pixels = ctx2.getImageData(0, 0, width, 2*height).data;
420         var ymin = 2*height, ymax = -1;
421         for (var y = 0; y < 2*height; y++) {
422             for (var x = 0; x < width; x++) {
423                 if (pixels[4*(y*width+x)] != 0) {
424                     if (ymin > y) ymin = y;
425                     if (ymax < y) ymax = y;
426                     break;
427                 }
428             }
429         }
430
431         var ret = (baseline - (ymin + ymax) / 2) | 0;
432         midpoint_cache[font] = ret;
433         return ret;
434     },
435
436     /*
437      * void js_canvas_draw_text(int x, int y, int halign,
438      *                          const char *colptr, const char *fontptr,
439      *                          const char *text);
440      * 
441      * Draw text. Vertical alignment has been taken care of on the C
442      * side, by optionally calling the above function. Horizontal
443      * alignment is handled here, since we can get the canvas draw
444      * function to do it for us with almost no extra effort.
445      */
446     js_canvas_draw_text: function(x, y, halign, colptr, fontptr, text) {
447         ctx.font = Pointer_stringify(fontptr);
448         ctx.fillStyle = Pointer_stringify(colptr);
449         ctx.textAlign = (halign == 0 ? 'left' :
450                          halign == 1 ? 'center' : 'right');
451         ctx.textBaseline = 'alphabetic';
452         ctx.fillText(Pointer_stringify(text), x, y);
453     },
454
455     /*
456      * int js_canvas_new_blitter(int w, int h);
457      * 
458      * Create a new blitter object, which is just an offscreen canvas
459      * of the specified size.
460      */
461     js_canvas_new_blitter: function(w, h) {
462         var id = blittercount++;
463         blitters[id] = document.createElement("canvas");
464         blitters[id].width = w;
465         blitters[id].height = h;
466     },
467
468     /*
469      * void js_canvas_free_blitter(int id);
470      * 
471      * Free a blitter (or rather, destroy our reference to it so JS
472      * can garbage-collect it, and also enforce that we don't
473      * accidentally use it again afterwards).
474      */
475     js_canvas_free_blitter: function(id) {
476         blitters[id] = null;
477     },
478
479     /*
480      * void js_canvas_copy_to_blitter(int id, int x, int y, int w, int h);
481      * 
482      * Copy from the puzzle image to a blitter. The size is passed to
483      * us, partly so we don't have to remember the size of each
484      * blitter, but mostly so that the C side can adjust the copy
485      * rectangle in the case where it partially overlaps the edge of
486      * the screen.
487      */
488     js_canvas_copy_to_blitter: function(id, x, y, w, h) {
489         var blitter_ctx = blitters[id].getContext('2d');
490         blitter_ctx.drawImage(offscreen_canvas,
491                               x, y, w, h,
492                               0, 0, w, h);
493     },
494
495     /*
496      * void js_canvas_copy_from_blitter(int id, int x, int y, int w, int h);
497      * 
498      * Copy from a blitter back to the puzzle image. As above, the
499      * size of the copied rectangle is passed to us from the C side
500      * and may already have been modified.
501      */
502     js_canvas_copy_from_blitter: function(id, x, y, w, h) {
503         ctx.drawImage(blitters[id],
504                       0, 0, w, h,
505                       x, y, w, h);
506     },
507
508     /*
509      * void js_canvas_make_statusbar(void);
510      * 
511      * Cause a status bar to exist. Called at setup time if the puzzle
512      * back end turns out to want one.
513      */
514     js_canvas_make_statusbar: function() {
515         var statustd = document.getElementById("statusbarholder");
516         statusbar = document.createElement("div");
517         statusbar.style.overflow = "hidden";
518         statusbar.style.width = onscreen_canvas.width - 4;
519         statusbar.style.height = "1.2em";
520         statusbar.style.background = "#d8d8d8";
521         statusbar.style.borderLeft = '2px solid #c8c8c8';
522         statusbar.style.borderTop = '2px solid #c8c8c8';
523         statusbar.style.borderRight = '2px solid #e8e8e8';
524         statusbar.style.borderBottom = '2px solid #e8e8e8';
525         statusbar.appendChild(document.createTextNode(" "));
526         statustd.appendChild(statusbar);
527     },
528
529     /*
530      * void js_canvas_set_statusbar(const char *text);
531      * 
532      * Set the text in the status bar.
533      */
534     js_canvas_set_statusbar: function(ptr) {
535         var text = Pointer_stringify(ptr);
536         statusbar.replaceChild(document.createTextNode(text),
537                                statusbar.lastChild);
538     },
539
540     /*
541      * void js_canvas_set_size(int w, int h);
542      * 
543      * Set the size of the puzzle canvas. Called at setup, and every
544      * time the user picks new puzzle settings requiring a different
545      * size.
546      */
547     js_canvas_set_size: function(w, h) {
548         onscreen_canvas.width = w;
549         offscreen_canvas.width = w;
550         if (statusbar !== null)
551             statusbar.style.width = w - 4;
552
553         onscreen_canvas.height = h;
554         offscreen_canvas.height = h;
555     },
556
557     /*
558      * void js_dialog_init(const char *title);
559      * 
560      * Begin constructing a 'dialog box' which will be popped up in an
561      * overlay on top of the rest of the puzzle web page.
562      */
563     js_dialog_init: function(titletext) {
564         // Create an overlay on the page which darkens everything
565         // beneath it.
566         dlg_dimmer = document.createElement("div");
567         dlg_dimmer.style.width = "100%";
568         dlg_dimmer.style.height = "100%";
569         dlg_dimmer.style.background = '#000000';
570         dlg_dimmer.style.position = 'fixed';
571         dlg_dimmer.style.opacity = 0.3;
572         dlg_dimmer.style.top = dlg_dimmer.style.left = 0;
573         dlg_dimmer.style["z-index"] = 99;
574
575         // Now create a form which sits on top of that in turn.
576         dlg_form = document.createElement("form");
577         dlg_form.style.width =  window.innerWidth * 2 / 3;
578         dlg_form.style.opacity = 1;
579         dlg_form.style.background = '#ffffff';
580         dlg_form.style.color = '#000000';
581         dlg_form.style.position = 'absolute';
582         dlg_form.style.border = "2px solid black";
583         dlg_form.style.padding = 20;
584         dlg_form.style.top = window.innerHeight / 10;
585         dlg_form.style.left = window.innerWidth / 6;
586         dlg_form.style["z-index"] = 100;
587
588         var title = document.createElement("p");
589         title.style.marginTop = "0px";
590         title.appendChild(document.createTextNode
591                           (Pointer_stringify(titletext)));
592         dlg_form.appendChild(title);
593
594         dlg_return_funcs = [];
595         dlg_next_id = 0;
596     },
597
598     /*
599      * void js_dialog_string(int i, const char *title, const char *initvalue);
600      * 
601      * Add a string control (that is, an edit box) to the dialog under
602      * construction.
603      */
604     js_dialog_string: function(index, title, initialtext) {
605         dlg_form.appendChild(document.createTextNode(Pointer_stringify(title)));
606         var editbox = document.createElement("input");
607         editbox.type = "text";
608         editbox.value = Pointer_stringify(initialtext);
609         dlg_form.appendChild(editbox);
610         dlg_form.appendChild(document.createElement("br"));
611
612         dlg_return_funcs.push(function() {
613             dlg_return_sval(index, editbox.value);
614         });
615     },
616
617     /*
618      * void js_dialog_choices(int i, const char *title, const char *choicelist,
619      *                        int initvalue);
620      * 
621      * Add a choices control (i.e. a drop-down list) to the dialog
622      * under construction. The 'choicelist' parameter is unchanged
623      * from the way the puzzle back end will have supplied it: i.e.
624      * it's still encoded as a single string whose first character
625      * gives the separator.
626      */
627     js_dialog_choices: function(index, title, choicelist, initvalue) {
628         dlg_form.appendChild(document.createTextNode(Pointer_stringify(title)));
629         var dropdown = document.createElement("select");
630         var choicestr = Pointer_stringify(choicelist);
631         var items = choicestr.slice(1).split(choicestr[0]);
632         var options = [];
633         for (var i in items) {
634             var option = document.createElement("option");
635             option.value = i;
636             option.appendChild(document.createTextNode(items[i]));
637             if (i == initvalue) option.selected = true;
638             dropdown.appendChild(option);
639             options.push(option);
640         }
641         dlg_form.appendChild(dropdown);
642         dlg_form.appendChild(document.createElement("br"));
643
644         dlg_return_funcs.push(function() {
645             var val = 0;
646             for (var i in options) {
647                 if (options[i].selected) {
648                     val = options[i].value;
649                     break;
650                 }
651             }
652             dlg_return_ival(index, val);
653         });
654     },
655
656     /*
657      * void js_dialog_boolean(int i, const char *title, int initvalue);
658      * 
659      * Add a boolean control (a checkbox) to the dialog under
660      * construction. Checkboxes are generally expected to be sensitive
661      * on their label text as well as the box itself, so for this
662      * control we create an actual label rather than merely a text
663      * node (and hence we must allocate an id to the checkbox so that
664      * the label can refer to it).
665      */
666     js_dialog_boolean: function(index, title, initvalue) {
667         var checkbox = document.createElement("input");
668         checkbox.type = "checkbox";
669         checkbox.id = "cb" + String(dlg_next_id++);
670         checkbox.checked = (initvalue != 0);
671         dlg_form.appendChild(checkbox);
672         var checkboxlabel = document.createElement("label");
673         checkboxlabel.setAttribute("for", checkbox.id);
674         checkboxlabel.textContent = Pointer_stringify(title);
675         dlg_form.appendChild(checkboxlabel);
676         dlg_form.appendChild(document.createElement("br"));
677
678         dlg_return_funcs.push(function() {
679             dlg_return_ival(index, checkbox.checked ? 1 : 0);
680         });
681     },
682
683     /*
684      * void js_dialog_launch(void);
685      * 
686      * Finish constructing a dialog, and actually display it, dimming
687      * everything else on the page.
688      */
689     js_dialog_launch: function() {
690         // Put in the OK and Cancel buttons at the bottom.
691         var button;
692
693         button = document.createElement("input");
694         button.type = "button";
695         button.value = "OK";
696         button.onclick = function(event) {
697             for (var i in dlg_return_funcs)
698                 dlg_return_funcs[i]();
699             command(3);
700         }
701         dlg_form.appendChild(button);
702
703         button = document.createElement("input");
704         button.type = "button";
705         button.value = "Cancel";
706         button.onclick = function(event) {
707             command(4);
708         }
709         dlg_form.appendChild(button);
710
711         document.body.appendChild(dlg_dimmer);
712         document.body.appendChild(dlg_form);
713     },
714
715     /*
716      * void js_dialog_cleanup(void);
717      * 
718      * Stop displaying a dialog, and clean up the internal state
719      * associated with it.
720      */
721     js_dialog_cleanup: function() {
722         document.body.removeChild(dlg_dimmer);
723         document.body.removeChild(dlg_form);
724         dlg_dimmer = dlg_form = null;
725         onscreen_canvas.focus();
726     },
727
728     /*
729      * void js_focus_canvas(void);
730      * 
731      * Return keyboard focus to the puzzle canvas. Called after a
732      * puzzle-control button is pressed, which tends to have the side
733      * effect of taking focus away from the canvas.
734      */
735     js_focus_canvas: function() {
736         onscreen_canvas.focus();
737     },
738 });