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