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