diff options
| -rw-r--r-- | cmake/platforms/emscripten.cmake | 67 | ||||
| -rw-r--r-- | emccpre.js | 10 | ||||
| -rwxr-xr-x | kaios/apppage.pl | 337 | ||||
| -rwxr-xr-x | kaios/manifest.pl | 36 |
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() @@ -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, +}) |