aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmake/platforms/emscripten.cmake67
-rw-r--r--emccpre.js10
-rwxr-xr-xkaios/apppage.pl337
-rwxr-xr-xkaios/manifest.pl36
4 files changed, 449 insertions, 1 deletions
diff --git a/cmake/platforms/emscripten.cmake b/cmake/platforms/emscripten.cmake
index 244cfcb..3cbcab4 100644
--- a/cmake/platforms/emscripten.cmake
+++ b/cmake/platforms/emscripten.cmake
@@ -6,6 +6,11 @@ set(CMAKE_EXECUTABLE_SUFFIX ".js")
set(WASM ON
CACHE BOOL "Compile to WebAssembly rather than plain JavaScript")
+find_program(HALIBUT halibut)
+if(NOT HALIBUT)
+ message(WARNING "HTML documentation cannot be built (did not find halibut)")
+endif()
+
set(emcc_export_list
# Event handlers for mouse and keyboard input
_mouseup
@@ -62,4 +67,66 @@ function(set_platform_puzzle_target_properties NAME TARGET)
endfunction()
function(build_platform_extras)
+ if(HALIBUT)
+ set(help_dir ${CMAKE_CURRENT_BINARY_DIR}/help)
+ add_custom_command(OUTPUT ${help_dir}/en
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${help_dir}/en)
+ add_custom_command(OUTPUT ${help_dir}/en/index.html
+ COMMAND ${HALIBUT} --html -Chtml-template-fragment:%k
+ ${CMAKE_CURRENT_SOURCE_DIR}/puzzles.but
+ DEPENDS
+ ${help_dir}/en
+ ${CMAKE_CURRENT_SOURCE_DIR}/puzzles.but
+ WORKING_DIRECTORY ${help_dir}/en)
+ add_custom_target(kaios_help ALL
+ DEPENDS ${help_dir}/en/index.html)
+ endif()
+
+ # This is probably not the right way to set the destination.
+ set(CMAKE_INSTALL_PREFIX ${CMAKE_CURRENT_BINARY_DIR} CACHE PATH
+ "Installation path" FORCE)
+
+ add_custom_target(kaios-extras ALL)
+
+ foreach(name ${puzzle_names})
+ add_custom_command(
+ OUTPUT ${name}-manifest.webapp
+ COMMAND ${PERL_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/kaios/manifest.pl
+ "${name}" "${displayname_${name}}" "${description_${name}}"
+ "${objective_${name}}" > "${name}-manifest.webapp"
+ VERBATIM
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/kaios/manifest.pl)
+
+ file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/kaios)
+ add_custom_command(
+ OUTPUT ${name}-kaios.html
+ COMMAND ${PERL_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/kaios/apppage.pl
+ "${name}" "${displayname_${name}}" > "${name}-kaios.html"
+ VERBATIM
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/kaios/apppage.pl)
+
+ add_custom_target(${name}-kaios-extras
+ DEPENDS ${name}-manifest.webapp ${name}-kaios.html)
+ add_dependencies(kaios-extras ${name}-kaios-extras)
+
+ install(TARGETS ${name} DESTINATION kaios/${name})
+ # Release builds generate an initial memory image alongside the
+ # JavaScript, but CMake doesn't seem to know about it to install
+ # it.
+ install(FILES $<TARGET_FILE:${name}>.mem OPTIONAL
+ DESTINATION kaios/${name})
+ install(FILES ${ICON_DIR}/${name}-56kai.png ${ICON_DIR}/${name}-112kai.png
+ DESTINATION kaios/${name} OPTIONAL)
+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${name}-kaios.html
+ RENAME ${name}.html
+ DESTINATION kaios/${name})
+ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${name}-manifest.webapp
+ RENAME manifest.webapp
+ DESTINATION kaios/${name})
+ if (HALIBUT)
+ install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/help
+ DESTINATION kaios/${name})
+ endif()
+
+ endforeach()
endfunction()
diff --git a/emccpre.js b/emccpre.js
index 2bbaa57..f0169ec 100644
--- a/emccpre.js
+++ b/emccpre.js
@@ -534,6 +534,14 @@ function initPuzzle() {
}
menuform.addEventListener("keydown", menukey);
+ // Open documentation links within the application in KaiOS.
+ for (var elem of document.querySelectorAll("#gamemenu a[href]")) {
+ elem.addEventListener("click", function(event) {
+ window.open(event.target.href);
+ event.preventDefault();
+ });
+ }
+
// In IE, the canvas doesn't automatically gain focus on a mouse
// click, so make sure it does
onscreen_canvas.addEventListener("mousedown", function(event) {
@@ -567,7 +575,7 @@ function initPuzzle() {
event.preventDefault();
event.stopPropagation();
}
- }, true);
+ });
// Event handler to fake :focus-within on browsers too old for
// it (like KaiOS 2.5). Browsers without :focus-within are also
diff --git a/kaios/apppage.pl b/kaios/apppage.pl
new file mode 100755
index 0000000..ecdffbc
--- /dev/null
+++ b/kaios/apppage.pl
@@ -0,0 +1,337 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+@ARGV == 2 or die "usage: apppage.pl <name> <displayname>";
+my ($name, $displayname) = @ARGV;
+
+print <<EOF;
+<!DOCTYPE html>
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=ASCII" />
+<meta name="theme-color" content="rgb(50,50,50)" />
+<title>${displayname}</title>
+<script defer type="text/javascript" src="${name}.js"></script>
+<!-- Override some defaults for small screens -->
+<script id="environment" type="application/json">
+{ "PATTERN_DEFAULT": "10x10" }
+</script>
+<style class="text/css">
+body {
+ margin: 0;
+ display: flex;
+ position: fixed;
+ width: 100%;
+ top: 0;
+ bottom: 30px;
+ font-size: 17px;
+}
+
+/* Top-level form for the game menu */
+#gamemenu {
+ /* Add a little mild text formatting */
+ font-weight: bold;
+ font-size: 14px;
+ }
+
+/* Inside that form, the main menu bar and every submenu inside it is a <ul> */
+#gamemenu ul {
+ list-style: none; /* get rid of the normal unordered-list bullets */
+ display: flex;
+ margin: 0;
+ /* Compensate for the negative margins on menu items by adding a
+ * little bit of padding so that the borders of the items don't protrude
+ * beyond the menu. */
+ padding: 0.5px;
+ /* Switch to vertical stacking, for drop-down submenus */
+ flex-direction: column;
+ /* We must specify an explicit background colour for submenus, because
+ * they must be opaque (don't want other page contents showing through
+ * them). */
+ background: white;
+}
+
+/* Individual menu items are <li> elements within such a <ul> */
+#gamemenu li {
+ /* Suppress the text-selection I-beam pointer */
+ cursor: default;
+ /* Surround each menu item with a border. */
+ border: 1px solid rgb(180,180,180);
+ /* Arrange that the borders of each item overlap the ones next to it. */
+ margin: -0.5px;
+}
+
+#gamemenu ul li[role=separator] {
+ color: transparent;
+ border: 0;
+}
+
+/* The interactive contents of menu items are their child elements. */
+#gamemenu li > * {
+ padding: 0.2em 0.75em;
+ margin: 0;
+ display: block;
+}
+
+
+#gamemenu :disabled {
+ /* Grey out disabled buttons */
+ color: rgba(0,0,0,0.5);
+}
+
+#gamemenu li > :hover:not(:disabled),
+#gamemenu li > .focus-within {
+ /* When the mouse is over a menu item, highlight it */
+ background-color: rgba(0,0,0,0.3);
+}
+
+.transient {
+ /* When they are displayed, they are positioned immediately above
+ * their parent <li>, and with the left edge aligning */
+ position: fixed;
+ bottom: 30px;
+ max-height: calc(100vh - 30px);
+ left: 100%;
+ transition: left 0.1s;
+ box-sizing: border-box;
+ width: 100vw;
+ overflow: auto;
+ /* And make sure they appear in front. */
+ z-index: 50;
+}
+
+.transient.focus-within {
+ /* Once a menu is actually focussed, bring it on screen. */
+ left: 0;
+ /* Hiding what's behind. */
+ box-shadow: 0 0 1em 0 rgba(0, 0, 0, 0.8);
+}
+
+#gamemenu :hover > ul,
+#gamemenu .focus-within > ul {
+ /* Last but by no means least, the all-important line that makes
+ * submenus be displayed! Any <ul> whose parent <li> is being
+ * hovered over gets display:flex overriding the display:none
+ * from above. */
+ display: flex;
+}
+
+#gamemenu button {
+ /* Menu items that trigger an action. We put some effort into
+ * removing the default button styling. */
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+ font: inherit;
+ color: inherit;
+ background: initial;
+ border: initial;
+ border-radius: initial;
+ text-align: inherit;
+ width: 100%;
+}
+
+#gamemenu .tick {
+ /* The tick at the start of a menu item, or its unselected equivalent.
+ * This is represented by an <input type="radio">, so we put some
+ * effort into overriding the default style. */
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+ margin: initial;
+ font: inherit;
+}
+
+#gamemenu .tick::before {
+ content: "\\2713";
+}
+
+#gamemenu .tick:not(:checked) {
+ /* Tick for an unselected menu entry. */
+ color: transparent;
+}
+
+#gamemenu li > div::after {
+ content: url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='10'%20height='10'%3E%3Cpolygon%20points='0,0,10,5,0,10'/%3E%3C/svg%3E");
+ float: right;
+}
+
+#puzzle {
+ background: var(--puzzle-background, #e6e6e6);
+ flex: 1 1 auto;
+ flex-direction: column;
+ align-items: center;
+ display: flex;
+ width: 100%
+}
+
+#statusbar {
+ overflow: hidden;
+ text-align: left;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ line-height: 1;
+ background: #d8d8d8;
+ border-left: 2px solid #c8c8c8;
+ border-top: 2px solid #c8c8c8;
+ border-right: 2px solid #e8e8e8;
+ border-bottom: 2px solid #e8e8e8;
+ height: 1em;
+}
+
+#dlgdimmer {
+ width: 100%;
+ height: 100%;
+ background: #000000;
+ position: fixed;
+ opacity: 0.3;
+ left: 0;
+ top: 0;
+ z-index: 99;
+}
+
+#dlgform {
+ width: 66.6667vw;
+ opacity: 1;
+ background: #ffffff;
+ color: #000000;
+ position: absolute;
+ border: 2px solid black;
+ padding: 20px;
+ top: 10vh;
+ left: 16.6667vw;
+ z-index: 100;
+}
+
+#dlgform h2 {
+ margin-top: 0px;
+}
+
+#puzzlecanvascontain {
+ flex: 1 1 auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-width: 0;
+ min-height: 0;
+}
+
+#puzzlecanvas {
+ max-width: 100%;
+ max-height: 100%;
+ background-color: white;
+ font-weight: 600;
+}
+
+#puzzlecanvas:focus {
+ /* The focus will be here iff there's nothing else on
+ * screen that can be focused, so the outline is
+ * redundant. */
+ outline: none;
+}
+
+#puzzle > div {
+ width: 100%;
+}
+
+.softkey {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ height: 30px;
+ font-weight: 600;
+ font-size: 14px;
+ line-height: 1;
+ white-space: nowrap;
+ background: rgb(50,50,50);
+ color: white;
+ z-index: 150;
+}
+
+:not(.focus-within) > .softkey {
+ display: none;
+}
+
+.softkey > * {
+ position: absolute;
+ padding: 8px;
+}
+
+.lsk {
+ left: 0;
+ right: 70%;
+ text-align: left;
+ padding-right: 0;
+}
+
+.csk {
+ left: 30%;
+ right: 30%;
+ text-align: center;
+ text-transform: uppercase;
+ padding-left: 0;
+ padding-right: 0;
+}
+
+.rsk {
+ right: 0;
+ left: 70%;
+ text-align: right;
+ padding-left: 0
+}
+
+</style>
+</head>
+<body>
+<div id="puzzle">
+ <div id="puzzlecanvascontain">
+ <canvas id="puzzlecanvas" width="1px" height="1px" tabindex="0">
+ </canvas>
+ </div>
+ <div id="statusbar">
+ </div>
+ <div class="softkey"><div class="rsk">Menu</div></div>
+</div>
+<form id="gamemenu" class="transient">
+ <ul>
+ <li><div tabindex="0">Game<ul class="transient">
+ <li><button type="button" id="specific">Enter game ID...</button></li>
+ <li><button type="button" id="random">Enter random seed...</button></li>
+ <li><button type="button" id="save">Download save file...</button></li>
+ <li><button type="button" id="load">Upload save file...</button></li>
+ </ul></div></li>
+ <li><div tabindex="0">Type<ul id="gametype" class="transient"></ul></div></li>
+ <li role="separator"></li>
+ <li><button type="button" id="new">
+ New<span class="verbiage"> game</span>
+ </button></li>
+ <li><button type="button" id="restart">
+ Restart<span class="verbiage"> game</span>
+ </button></li>
+ <li><button type="button" id="undo">
+ Undo<span class="verbiage"> move</span>
+ </button></li>
+ <li><button type="button" id="redo">
+ Redo<span class="verbiage"> move</span>
+ </button></li>
+ <li><button type="button" id="solve">
+ Solve<span class="verbiage"> game</span>
+ </button></li>
+ <li><a target="_blank" href="help/en/${name}.html#${name}">
+ Instructions
+ </a></li>
+ <li><a target="_blank" href="help/en/index.html">
+ Full manual
+ </a></li>
+ </ul>
+ <div class="softkey">
+ <div class="csk">Select</div>
+ <div class="rsk">Dismiss</div>
+ </div>
+</form>
+</body>
+</html>
+EOF
diff --git a/kaios/manifest.pl b/kaios/manifest.pl
new file mode 100755
index 0000000..36fc195
--- /dev/null
+++ b/kaios/manifest.pl
@@ -0,0 +1,36 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use JSON::PP;
+
+@ARGV == 4 or
+ die "usage: manifest.pl <name> <displayname> <description> <objective>";
+my ($name, $displayname, $description, $objective) = @ARGV;
+
+# Limits from
+# https://developer.kaiostech.com/docs/getting-started/main-concepts/manifest
+length($displayname) <= 20 or die "Name too long: $displayname";
+length($description) <= 40 or die "Subtitle too long: $description";
+$objective .= " Part of Simon Tatham's Portable Puzzle Collection.";
+# https://developer.kaiostech.com/docs/distribution/submission-guideline
+length($objective) <= 220 or die "Description too long: $objective";
+
+print encode_json({
+ name => $displayname,
+ subtitle => $description,
+ description => $objective,
+ launch_path => "/${name}.html",
+ icons => {
+ "56" => "/${name}-56kai.png",
+ "112" => "/${name}-112kai.png",
+ },
+ developer => {
+ name => "Ben Harris",
+ url => "https://bjh21.me.uk",
+ },
+ default_locale => "en-GB",
+ categories => ["games"],
+ cursor => JSON::PP::false,
+})