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