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