Builds a browser-playable .html/.js/.wasm/.data via `./build-web.sh` (no
new CMake top-level target — Emscripten is detected at configure time and
takes the existing kobodl through a web-flavored pipeline).
Key pieces:
- CMakeLists.txt: route SDL3 through `--use-port=sdl3`; substitute
`--use-port=libpng` for the missing sdl3_image port (Emscripten 5.0.7
ships only sdl3 + sdl3_ttf). pthread (atomics+bulk-memory) lifted to
kobo_deps so every TU agrees with wasm-ld's --shared-memory. Link with
-sASYNCIFY (avoids rewriting kobo's blocking main loop around
emscripten_set_main_loop), -sALLOW_MEMORY_GROWTH/-sMAXIMUM_MEMORY=1gb,
-sPTHREAD_POOL_SIZE=4, -lidbfs.js, and --preload-file data@/data.
- graphics/img_compat.{h,c}: libpng-backed IMG_Load shim, used in
sprite.c / gfxengine.cpp / filters.c via #include "img_compat.h".
Native builds passthrough to <SDL3_image/SDL_image.h> unchanged.
- web/shell.html: HTML wrapper that mounts /home as IDBFS for persistent
scores/config. Startup gated on addRunDependency until syncfs(true)
finishes, then /home/scores is mkdir'd post-load (the game's addPlayer
doesn't auto-create the score dir; see TODO in score.cpp).
beforeunload + visibilitychange flushes /home back to IndexedDB.
- build-web.sh: emcmake wrapper + a static server that sets the
COOP/COEP headers SharedArrayBuffer/pthreads require.
Source fixes needed to build under Emscripten's libc/libcxx:
- Renamed enemy_kind `pipe2` -> `pipe2k` (collided with libc pipe2(2)
always declared in Emscripten's <unistd.h>).
- graphics/toolkit.cpp: added missing <stdio.h> for snprintf (Emscripten's
libcxx headers don't pull it in transitively).
- states.cpp: gated the "Quit Kobo Deluxe" main-menu button on
!__EMSCRIPTEN__; exit() on the web leaves a dead canvas.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
766 lines
12 KiB
C++
766 lines
12 KiB
C++
/*(LGPL)
|
|
---------------------------------------------------------------------------
|
|
toolkit.cpp - Simple "GUI" toolkit for config screens.
|
|
---------------------------------------------------------------------------
|
|
* Copyright (C) 2001 David Olofson
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or (at
|
|
* your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with this program; if not, write to the Free Software Foundation,
|
|
* Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
|
|
#include "window.h"
|
|
#include "toolkit.h"
|
|
|
|
|
|
|
|
/*----------------------------------------------------------
|
|
ct_widget_t::
|
|
----------------------------------------------------------*/
|
|
|
|
ct_widget_t::ct_widget_t()
|
|
{
|
|
interactive = 0;
|
|
transparent = 1;
|
|
highlighted = 0;
|
|
user = NULL;
|
|
user2 = 0;
|
|
tag = 0;
|
|
_color = 0;
|
|
next = prev = NULL;
|
|
_widget_index = -1;
|
|
halign(ALIGN_DEFAULT);
|
|
valign(ALIGN_DEFAULT);
|
|
_token = 0;
|
|
xo = yo = 0.0;
|
|
}
|
|
|
|
|
|
void ct_widget_t::halign(ct_align_t ha)
|
|
{
|
|
if(ALIGN_DEFAULT == ha)
|
|
_halign = ALIGN_RIGHT;
|
|
else
|
|
_halign = ha;
|
|
}
|
|
|
|
|
|
void ct_widget_t::valign(ct_align_t va)
|
|
{
|
|
if(ALIGN_DEFAULT == va)
|
|
_valign = ALIGN_TOP;
|
|
else
|
|
_valign = va;
|
|
}
|
|
|
|
|
|
void ct_widget_t::init(gfxengine_t *e)
|
|
{
|
|
window_t::init(e);
|
|
_color = map_rgb(0,0,0);
|
|
}
|
|
|
|
|
|
void ct_widget_t::transparency(int t)
|
|
{
|
|
if(t == transparent)
|
|
return;
|
|
|
|
transparent = t;
|
|
if(!transparent)
|
|
render();
|
|
}
|
|
|
|
|
|
void ct_widget_t::highlight(int hl)
|
|
{
|
|
if(hl == highlighted)
|
|
return;
|
|
|
|
highlighted = hl;
|
|
if(!transparent)
|
|
render();
|
|
}
|
|
|
|
|
|
void ct_widget_t::color(Uint32 _cl)
|
|
{
|
|
if(_cl == _color)
|
|
return;
|
|
|
|
_color = _cl;
|
|
if(!transparent)
|
|
render();
|
|
}
|
|
|
|
|
|
void ct_widget_t::render()
|
|
{
|
|
if(!transparent)
|
|
{
|
|
foreground(_color);
|
|
fillrect(0, 0, width(), height());
|
|
}
|
|
if(highlighted)
|
|
ct_engine.render_highlight(this);
|
|
}
|
|
|
|
|
|
/* Handy tool function used by some descendents. */
|
|
void ct_widget_t::render_text_aligned(const char *buf)
|
|
{
|
|
int yy = 0;
|
|
switch(_valign)
|
|
{
|
|
case ALIGN_DEFAULT: //dummy
|
|
case ALIGN_NONE:
|
|
case ALIGN_TOP:
|
|
yy = yoffs();
|
|
break;
|
|
case ALIGN_BOTTOM:
|
|
yy = height() - fontheight() - yoffs();
|
|
break;
|
|
case ALIGN_CENTER:
|
|
case ALIGN_CENTER_TOKEN:
|
|
yy = (height() - fontheight()) / 2 + yoffs();
|
|
break;
|
|
}
|
|
switch(_halign)
|
|
{
|
|
case ALIGN_DEFAULT: //dummy
|
|
case ALIGN_NONE:
|
|
case ALIGN_LEFT:
|
|
string(xoffs(), yy, buf);
|
|
break;
|
|
case ALIGN_RIGHT:
|
|
center_token(width() - xoffs(), yy, buf);
|
|
break;
|
|
case ALIGN_CENTER:
|
|
center_token(xoffs(), yy, buf, -1);
|
|
break;
|
|
case ALIGN_CENTER_TOKEN:
|
|
center_token(xoffs(), yy, buf, _token);
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void ct_widget_t::change(int delta)
|
|
{
|
|
_value += delta;
|
|
}
|
|
|
|
|
|
void ct_widget_t::value(int val)
|
|
{
|
|
_value = val;
|
|
}
|
|
|
|
|
|
int ct_widget_t::value()
|
|
{
|
|
return _value;
|
|
}
|
|
|
|
|
|
|
|
/*----------------------------------------------------------
|
|
ct_engine_t::
|
|
----------------------------------------------------------*/
|
|
|
|
void default_render_highlight(ct_widget_t *wg)
|
|
{
|
|
Uint32 hlc[6];
|
|
hlc[0] = wg->map_rgb(0x440000);
|
|
hlc[1] = wg->map_rgb(0x660000);
|
|
hlc[2] = wg->map_rgb(0x880000);
|
|
hlc[3] = wg->map_rgb(0x990000);
|
|
hlc[4] = wg->map_rgb(0xaa0000);
|
|
hlc[5] = wg->map_rgb(0xbb0000);
|
|
wg->foreground(hlc[5]);
|
|
wg->fillrect(0, 6, wg->width(), wg->height()-6-1);
|
|
for(int i = 0; i < 6; ++i)
|
|
{
|
|
wg->foreground(hlc[i]);
|
|
wg->fillrect(0, i, wg->width(), 1);
|
|
wg->fillrect(0, wg->height()-1-i, wg->width(), 1);
|
|
}
|
|
}
|
|
|
|
|
|
ct_engine_t::ct_engine_t()
|
|
{
|
|
ct_engine.render_highlight = default_render_highlight;
|
|
}
|
|
|
|
|
|
ct_engine_t ct_engine;
|
|
|
|
|
|
|
|
/*----------------------------------------------------------
|
|
ct_label_t::
|
|
----------------------------------------------------------*/
|
|
|
|
ct_label_t::ct_label_t(const char *cap)
|
|
{
|
|
xo = 0.5;
|
|
if(cap)
|
|
caption(cap);
|
|
else
|
|
caption("Label");
|
|
}
|
|
|
|
|
|
void ct_label_t::halign(ct_align_t ha)
|
|
{
|
|
if(ALIGN_DEFAULT == ha)
|
|
_halign = ALIGN_CENTER;
|
|
else
|
|
_halign = ha;
|
|
}
|
|
|
|
|
|
void ct_label_t::caption(const char *cap)
|
|
{
|
|
strncpy(_caption, cap, sizeof(_caption));
|
|
_caption[sizeof(_caption)-1] = 0;
|
|
if(!transparent)
|
|
render();
|
|
}
|
|
|
|
|
|
void ct_label_t::render()
|
|
{
|
|
ct_widget_t::render();
|
|
render_text_aligned(_caption);
|
|
}
|
|
|
|
|
|
/*----------------------------------------------------------
|
|
ct_item_t::
|
|
----------------------------------------------------------*/
|
|
|
|
ct_item_t::ct_item_t(const char *cap, int val)
|
|
{
|
|
if(cap)
|
|
caption(cap);
|
|
else
|
|
caption("Item");
|
|
_value = val;
|
|
}
|
|
|
|
|
|
void ct_item_t::caption(const char *cap)
|
|
{
|
|
strncpy(_caption, cap, sizeof(_caption));
|
|
_caption[sizeof(_caption)-1] = 0;
|
|
}
|
|
|
|
|
|
static ct_item_t dummy_item("<error>", -1);
|
|
|
|
|
|
/*----------------------------------------------------------
|
|
ct_list_t::
|
|
----------------------------------------------------------*/
|
|
|
|
ct_list_t::ct_list_t(const char *cap) : ct_label_t(cap)
|
|
{
|
|
interactive = 1;
|
|
if(cap)
|
|
caption(cap);
|
|
else
|
|
caption("List");
|
|
items = NULL;
|
|
_selected = NULL;
|
|
|
|
_token = ':';
|
|
xo = 0.55;
|
|
}
|
|
|
|
|
|
ct_list_t::~ct_list_t()
|
|
{
|
|
clean();
|
|
}
|
|
|
|
|
|
void ct_list_t::halign(ct_align_t ha)
|
|
{
|
|
if(ALIGN_DEFAULT == ha)
|
|
_halign = ALIGN_CENTER_TOKEN;
|
|
else
|
|
_halign = ha;
|
|
}
|
|
|
|
|
|
void ct_list_t::clean()
|
|
{
|
|
if(!items)
|
|
return;
|
|
|
|
/*
|
|
* Break the circular list first, or we'll
|
|
* blow up the memory manager...! :-)
|
|
*/
|
|
items->prev->next = NULL;
|
|
|
|
while(items)
|
|
{
|
|
ct_item_t *i = items;
|
|
items = i->next;
|
|
delete i;
|
|
}
|
|
|
|
items = NULL;
|
|
_selected = NULL;
|
|
}
|
|
|
|
|
|
void ct_list_t::add(ct_item_t *item)
|
|
{
|
|
if(items)
|
|
{
|
|
item->next = items;
|
|
item->prev = items->prev;
|
|
items->prev->next = item;
|
|
items->prev = item;
|
|
}
|
|
else
|
|
{
|
|
item->next = item->prev = item;
|
|
items = item;
|
|
_selected = item;
|
|
if(!transparent)
|
|
render();
|
|
}
|
|
}
|
|
|
|
|
|
void ct_list_t::add(const char *cap, int val)
|
|
{
|
|
ct_item_t *it = new ct_item_t(cap, val);
|
|
add(it);
|
|
}
|
|
|
|
|
|
void ct_list_t::select(ct_item_t *item)
|
|
{
|
|
_selected = item;
|
|
#if 0
|
|
ct_item_t *i = items;
|
|
if(i)
|
|
for(int n = 0; n < 10000; ++n) //Just in case...
|
|
{
|
|
if(i == _selected)
|
|
{
|
|
_value = n;
|
|
break;
|
|
}
|
|
i = i->next;
|
|
}
|
|
#endif
|
|
if(_selected)
|
|
_value = _selected->value();
|
|
if(!transparent)
|
|
render();
|
|
}
|
|
|
|
|
|
void ct_list_t::select(int ind)
|
|
{
|
|
if(!items)
|
|
return;
|
|
|
|
_selected = items;
|
|
for(int i = 0; i < ind; ++i)
|
|
_selected = _selected->next;
|
|
select(_selected); //To update _value, render etc...
|
|
}
|
|
|
|
|
|
ct_item_t *ct_list_t::selected()
|
|
{
|
|
if(!items)
|
|
return &dummy_item;
|
|
|
|
return _selected;
|
|
}
|
|
|
|
|
|
void ct_list_t::value(int val)
|
|
{
|
|
if(!items)
|
|
return;
|
|
|
|
ct_item_t *best = items;
|
|
_selected = items;
|
|
while(_selected->value() != val)
|
|
{
|
|
if(labs(_selected->value()-val) < labs(best->value()-val))
|
|
best = _selected;
|
|
if(_selected->next == items)
|
|
break;
|
|
_selected = _selected->next;
|
|
}
|
|
if(_selected->value() != val)
|
|
_selected = best;
|
|
select(_selected); //To update _value, render etc...
|
|
}
|
|
|
|
|
|
int ct_list_t::value()
|
|
{
|
|
if(!items)
|
|
return dummy_item.value();
|
|
if(_selected)
|
|
return _selected->value();
|
|
else
|
|
return dummy_item.value();
|
|
}
|
|
|
|
|
|
void ct_list_t::change(int delta)
|
|
{
|
|
if(_selected)
|
|
{
|
|
if(0 == delta)
|
|
delta = 1;
|
|
if(delta > 0)
|
|
for(int i = 0; i < delta; ++i)
|
|
_selected = _selected->next;
|
|
else if(delta < 0)
|
|
for(int i = 0; i > delta; --i)
|
|
_selected = _selected->prev;
|
|
select(_selected); //To update _value, render etc...
|
|
}
|
|
ct_widget_t::change(delta);
|
|
}
|
|
|
|
|
|
void ct_list_t::render()
|
|
{
|
|
char buf[128];
|
|
const char *ic;
|
|
if(_selected)
|
|
ic = _selected->caption();
|
|
else
|
|
ic = dummy_item.caption();
|
|
ct_widget_t::render();
|
|
snprintf(buf, sizeof(buf), "%s: %s", caption(), ic);
|
|
//center(0, buf);
|
|
//center_token(width()*3/5, 0, buf, ':');
|
|
render_text_aligned(buf);
|
|
}
|
|
|
|
|
|
|
|
/*----------------------------------------------------------
|
|
ct_spin_t::
|
|
----------------------------------------------------------*/
|
|
|
|
ct_spin_t::ct_spin_t(const char *cap, int _min, int _max,
|
|
const char *__unit) : ct_label_t(cap)
|
|
{
|
|
interactive = 1;
|
|
min = _min;
|
|
max = _max;
|
|
_token = ':';
|
|
xo = 0.55;
|
|
if(__unit)
|
|
unit(__unit);
|
|
else
|
|
unit("");
|
|
}
|
|
|
|
|
|
void ct_spin_t::unit(const char *txt)
|
|
{
|
|
strncpy(_unit, txt, sizeof(_unit));
|
|
_unit[sizeof(_unit)-1] = 0;
|
|
if(!transparent)
|
|
render();
|
|
}
|
|
|
|
|
|
void ct_spin_t::value(int val)
|
|
{
|
|
val -= min;
|
|
while(val < 0)
|
|
val += max - min + 1;
|
|
val %= max - min + 1;
|
|
val += min;
|
|
ct_label_t::value(val);
|
|
}
|
|
|
|
|
|
void ct_spin_t::change(int delta)
|
|
{
|
|
value(_value + delta);
|
|
}
|
|
|
|
|
|
void ct_spin_t::render()
|
|
{
|
|
char buf[128];
|
|
ct_widget_t::render();
|
|
snprintf(buf, sizeof(buf), "%s: %d %s", caption(), _value, unit());
|
|
//center(0, buf);
|
|
//center_token(width()*3/5, 0, buf, ':');
|
|
render_text_aligned(buf);
|
|
}
|
|
|
|
|
|
|
|
/*----------------------------------------------------------
|
|
ct_button_t::
|
|
----------------------------------------------------------*/
|
|
|
|
ct_button_t::ct_button_t(const char *cap) : ct_label_t(cap)
|
|
{
|
|
interactive = 1;
|
|
}
|
|
|
|
|
|
void ct_button_t::render()
|
|
{
|
|
ct_label_t::render();
|
|
}
|
|
|
|
|
|
void ct_button_t::change(int delta)
|
|
{
|
|
ct_label_t::change(delta);
|
|
if(!transparent)
|
|
render();
|
|
}
|
|
|
|
|
|
|
|
/*----------------------------------------------------------
|
|
ct_form_t::
|
|
----------------------------------------------------------*/
|
|
|
|
ct_form_t::ct_form_t()
|
|
{
|
|
widgets = NULL;
|
|
_selected = NULL;
|
|
}
|
|
|
|
|
|
ct_form_t::~ct_form_t()
|
|
{
|
|
if(!widgets)
|
|
return;
|
|
|
|
/*
|
|
* Break the circular list first, or we'll
|
|
* blow up the memory manager...! :-)
|
|
*/
|
|
widgets->prev->next = NULL;
|
|
|
|
while(widgets)
|
|
{
|
|
ct_widget_t *i = widgets;
|
|
widgets = i->next;
|
|
delete i;
|
|
}
|
|
}
|
|
|
|
|
|
void ct_form_t::add(ct_widget_t *widget)
|
|
{
|
|
if(widgets)
|
|
{
|
|
widget->next = widgets;
|
|
widget->prev = widgets->prev;
|
|
widgets->prev->next = widget;
|
|
widgets->prev = widget;
|
|
widget->_widget_index = widget->prev->_widget_index + 1;
|
|
if(!_selected->interactive)
|
|
select(widget);
|
|
}
|
|
else
|
|
{
|
|
widget->_widget_index = 0;
|
|
widget->next = widget->prev = widget;
|
|
widgets = widget;
|
|
select(widget);
|
|
}
|
|
render_nontransparent();
|
|
}
|
|
|
|
|
|
void ct_form_t::clean()
|
|
{
|
|
if(!widgets)
|
|
return;
|
|
|
|
/*
|
|
* Break the circular list first, or we'll
|
|
* blow up the memory manager...! :-)
|
|
*/
|
|
widgets->prev->next = NULL;
|
|
|
|
while(widgets)
|
|
{
|
|
ct_widget_t *w = widgets;
|
|
widgets = w->next;
|
|
delete w;
|
|
}
|
|
|
|
widgets = NULL;
|
|
_selected = NULL;
|
|
}
|
|
|
|
|
|
void ct_form_t::select(ct_widget_t *widget)
|
|
{
|
|
if(_selected)
|
|
_selected->highlight(0);
|
|
_selected = widget;
|
|
if(_selected)
|
|
{
|
|
_selected->highlight(1);
|
|
if(!_selected->transparent)
|
|
_selected->render();
|
|
}
|
|
}
|
|
|
|
|
|
void ct_form_t::select(int ind)
|
|
{
|
|
if(!widgets)
|
|
return;
|
|
|
|
if(!widgets)
|
|
return;
|
|
|
|
ct_widget_t *sel = widgets;
|
|
for(int i = 0; i < ind; ++i)
|
|
sel = sel->next;
|
|
|
|
int lim = 10000;
|
|
while(!sel->interactive)
|
|
{
|
|
if(!lim--)
|
|
break;
|
|
sel = sel->next;
|
|
}
|
|
select(sel);
|
|
}
|
|
|
|
|
|
void ct_form_t::next()
|
|
{
|
|
if(!widgets)
|
|
return;
|
|
|
|
if(!_selected)
|
|
{
|
|
select(0);
|
|
return;
|
|
}
|
|
|
|
ct_widget_t *sel = _selected->next;
|
|
|
|
int lim = 1000;
|
|
while(!sel->interactive)
|
|
{
|
|
if(!lim--)
|
|
break;
|
|
sel = sel->next;
|
|
}
|
|
select(sel);
|
|
}
|
|
|
|
|
|
void ct_form_t::prev()
|
|
{
|
|
if(!widgets)
|
|
return;
|
|
|
|
if(!_selected)
|
|
{
|
|
select(0);
|
|
return;
|
|
}
|
|
|
|
ct_widget_t *sel = _selected->prev;
|
|
|
|
int lim = 1000;
|
|
while(!sel->interactive)
|
|
{
|
|
if(!lim--)
|
|
break;
|
|
sel = sel->prev;
|
|
}
|
|
select(sel);
|
|
}
|
|
|
|
|
|
ct_widget_t *ct_form_t::selected()
|
|
{
|
|
return _selected;
|
|
}
|
|
|
|
|
|
int ct_form_t::selected_index()
|
|
{
|
|
if(_selected)
|
|
return _selected->_widget_index;
|
|
else
|
|
return -1;
|
|
}
|
|
|
|
|
|
void ct_form_t::change(int delta)
|
|
{
|
|
if(_selected)
|
|
_selected->change(delta);
|
|
}
|
|
|
|
|
|
void ct_form_t::render()
|
|
{
|
|
ct_widget_t *w = widgets;
|
|
while(w)
|
|
{
|
|
w->render();
|
|
w = w->next;
|
|
if(w == widgets)
|
|
break; //Done!
|
|
}
|
|
}
|
|
|
|
|
|
void ct_form_t::render_nontransparent()
|
|
{
|
|
ct_widget_t *w = widgets;
|
|
while(w)
|
|
{
|
|
if(!w->transparent)
|
|
w->render();
|
|
w = w->next;
|
|
if(w == widgets)
|
|
break; //Done!
|
|
}
|
|
}
|