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