Use the SDL popup menu on Android

The popup menu opens with a three-finger tap. Add touch support to the
modal framework: a tap is replayed as a multi-frame mouse
hover->press->release so microui registers the click, and the press is
held so sliders can be dragged. Fix a microui command-buffer alignment
bug that crashed on 32-bit ARM.
This commit is contained in:
kichikuou
2026-07-01 09:23:38 +09:00
parent 286f150510
commit 3ebb1faece
10 changed files with 98 additions and 74 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# Changelog
## Unreleased
- Replaced the GTK-based popup menu and dialogs with a pure-SDL implementation.
- Replaced the GTK-based popup menu and dialogs with a pure-SDL implementation. On Android, the popup menu opens with a three-finger tap.
## 2.18.0 - 2026-06-23
- Added support for the intro demo of Daiakuji.
+1
View File
@@ -47,6 +47,7 @@ file(COPY_FILE ${PROJECT_ROOT_DIR}/COPYING ${ASSETS_DIR}/licenses/xsystem35)
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/MTLc3m.txt ${ASSETS_DIR}/licenses/MTLc3m)
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/mincho.txt ${ASSETS_DIR}/licenses/mincho)
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/nanojpeg.txt ${ASSETS_DIR}/licenses/nanojpeg)
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/microui.txt ${ASSETS_DIR}/licenses/microui)
file(COPY_FILE ${sdl_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL)
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_ttf)
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/freetype/docs/GPLv2.TXT ${ASSETS_DIR}/licenses/freetype)
@@ -14,6 +14,7 @@ class LicensesMenuActivity : Activity() {
Entry("SDL_mixer", "SDL_mixer", "https://github.com/libsdl-org/SDL_mixer"),
Entry("SDL_ttf", "SDL_ttf", "https://github.com/libsdl-org/SDL_ttf"),
Entry("NanoJPEG", "nanojpeg", "https://keyj.emphy.de/nanojpeg/"),
Entry("microui", "microui", "https://github.com/rxi/microui"),
Entry("FreeType", "freetype", "https://freetype.org/"),
Entry("HarfBuzz", "harfbuzz", "https://harfbuzz.github.io/"),
Entry("MotoyaLCedar W3 mono", "MTLc3m", "https://github.com/aosp-mirror/platform_frameworks_base/tree/lollipop-release/data/fonts"),
-2
View File
@@ -121,8 +121,6 @@ endif()
if (EMSCRIPTEN)
target_sources(xsystem35 PRIVATE menu_emscripten.c)
elseif (ANDROID)
target_sources(xsystem35 PRIVATE menu_android.c)
else ()
target_sources(xsystem35 PRIVATE
menu_sdl.c modal.c microui/microui.c volume.c)
+3 -1
View File
@@ -430,7 +430,9 @@ void event_handle_event(SDL_Event *e) {
break;
case SDL_FINGERDOWN:
if (SDL_GetNumTouchFingers(e->tfinger.touchId) >= 2) {
if (SDL_GetNumTouchFingers(e->tfinger.touchId) >= 3) {
menu_open();
} else if (SDL_GetNumTouchFingers(e->tfinger.touchId) >= 2) {
mouseb &= ~(1 << SDL_BUTTON_LEFT);
mouseb |= 1 << SDL_BUTTON_RIGHT;
RawKeyInfo[mouse_to_rawkey(SDL_BUTTON_LEFT)] = false;
-65
View File
@@ -1,65 +0,0 @@
/*
* Copyright (C) 2020 <KichikuouChrome@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
#include "config.h"
#include <SDL.h>
#include "system.h"
#include "portab.h"
#include "menu.h"
#include "nact.h"
void menu_open(void) {
return;
}
void menu_quitmenu_open(void) {
const SDL_MessageBoxButtonData buttons[] = {
{ SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT, 1, "Quit" },
{ SDL_MESSAGEBOX_BUTTON_ESCAPEKEY_DEFAULT, 0, "Cancel" },
};
const SDL_MessageBoxData messagebox_data = {
.flags = SDL_MESSAGEBOX_INFORMATION,
.window = NULL,
.title = "Quit game?",
.message = "Any unsaved progress will be lost.",
.numbuttons = SDL_arraysize(buttons),
.buttons = buttons,
};
int buttonid = 0;
if (SDL_ShowMessageBox(&messagebox_data, &buttonid) < 0) {
WARNING("error displaying message box");
return;
}
if (buttonid == 1) {
nact_quit(false);
}
}
void menu_init(void) {
return;
}
void menu_render_overlay(void) {
return;
}
void menu_setSkipState(bool enabled, bool activated) {
}
+22 -4
View File
@@ -51,19 +51,37 @@ struct popup_state {
modal base;
mu_Rect rect; // popup window rect from the most recent frame
enum menu_action action; // what the user selected
// A three-finger tap opens the menu, but SDL_GetNumTouchFingers() reports the
// live finger count, so it already returns 3 while the first of the three
// near-simultaneous FINGERDOWN events is being processed - the menu opens on
// that first event. The other two FINGERDOWN events of the same gesture are
// still queued and, once the menu is up, would be seen as taps that dismiss
// it. Ignore touches until every finger has lifted, then arm tap-to-dismiss.
bool touch_armed;
};
// Message-skip state, kept in sync via menu_setSkipState().
static bool skip_enabled = true;
static bool skip_activated;
static bool point_in_rect(int x, int y, mu_Rect r) {
return x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h;
}
static bool menu_popup_handler(const SDL_Event *e, modal *modal) {
struct popup_state *st = (struct popup_state *)modal;
if (e->type == SDL_MOUSEBUTTONUP) {
bool point_in_menu =
e->button.x >= st->rect.x && e->button.x < st->rect.x + st->rect.w &&
e->button.y >= st->rect.y && e->button.y < st->rect.y + st->rect.h;
if (e->button.button == SDL_BUTTON_RIGHT || !point_in_menu)
if (e->button.button == SDL_BUTTON_RIGHT ||
!point_in_rect(e->button.x, e->button.y, st->rect))
st->base.cancelled = true;
} else if (e->type == SDL_FINGERUP) {
// Arm tap-to-dismiss once the opening gesture's fingers are all lifted.
if (SDL_GetNumTouchFingers(e->tfinger.touchId) == 0)
st->touch_armed = true;
} else if (e->type == SDL_FINGERDOWN && st->touch_armed) {
// A tap outside the menu dismisses it.
int x = e->tfinger.x * view_w, y = e->tfinger.y * view_h;
if (!point_in_rect(x, y, st->rect))
st->base.cancelled = true;
}
return modal_default_handler(e, modal);
+6
View File
@@ -426,6 +426,12 @@ void mu_input_text(mu_Context *ctx, const char *text) {
mu_Command* mu_push_command(mu_Context *ctx, int type, int size) {
mu_Command *cmd = (mu_Command*) (ctx->command_list.items + ctx->command_list.idx);
#ifdef __arm__
/* xsystem35 change: round the entry size up to a pointer-aligned boundary.
** Variable-length commands (text) would otherwise leave the following command
** at an unaligned address, which faults on 32-bit ARM. */
size = (size + (int) sizeof(void*) - 1) & ~((int) sizeof(void*) - 1);
#endif
expect(ctx->command_list.idx + size < MU_COMMANDLIST_SIZE);
cmd->base.type = type;
cmd->base.size = size;
+63
View File
@@ -38,6 +38,22 @@
static mu_Context *ctx;
static modal *current_modal;
// Touch input. A tap is translated into a synthetic mouse hover->press->release
// sequence spread across several frames, decoupled from the finger's real
// timing. This is required because microui only registers a control as hovered
// on a frame where no button is held, and the hovered window (hover_root) itself
// updates a frame late; so the press must trail the finger landing by a couple
// of frames or the tap never clicks. Holding the press also lets sliders drag.
#define TOUCH_HOVER_FRAMES 2
static enum {
TOUCH_NONE,
TOUCH_HOVER, // finger down; letting microui settle hover before pressing
TOUCH_PRESS, // mouse button held (a drag tracks the finger in this phase)
} touch_phase;
static int touch_hover_frames; // frames left to dwell in TOUCH_HOVER
static bool touch_release_after_press; // finger lifted early; release once pressed
static int touch_x, touch_y;
static const FontSpec menu_font = { FONT_GOTHIC, FONT_WEIGHT_NORMAL, MODAL_FONT_SIZE };
static int text_width_cb(mu_Font font, const char *text, int len) {
@@ -79,6 +95,8 @@ static void init_context(void) {
ctx->text_width = text_width_cb;
ctx->text_height = text_height_cb;
ctx->style->font = (mu_Font)&menu_font;
ctx->style->spacing = 8;
ctx->style->thumb_size = 16;
// microui uses style->size.y as the default height of a control row, so
// set it to the font height to make each row tall enough for its text.
ctx->style->size.y = text_height_cb(ctx->style->font);
@@ -114,6 +132,32 @@ bool modal_default_handler(const SDL_Event *e, modal *modal) {
mu_input_mouseup(ctx, e->button.x, e->button.y, b);
break;
}
// Touch handling (see touch_phase above). The press/release is driven
// from the frame loop in modal_run(), not emitted here directly.
case SDL_FINGERDOWN:
touch_x = e->tfinger.x * view_w;
touch_y = e->tfinger.y * view_h;
mu_input_mousemove(ctx, touch_x, touch_y);
touch_phase = TOUCH_HOVER;
touch_hover_frames = TOUCH_HOVER_FRAMES;
touch_release_after_press = false;
break;
case SDL_FINGERMOTION:
touch_x = e->tfinger.x * view_w;
touch_y = e->tfinger.y * view_h;
mu_input_mousemove(ctx, touch_x, touch_y);
break;
case SDL_FINGERUP:
touch_x = e->tfinger.x * view_w;
touch_y = e->tfinger.y * view_h;
if (touch_phase == TOUCH_PRESS) {
mu_input_mouseup(ctx, touch_x, touch_y, MU_MOUSE_LEFT);
touch_phase = TOUCH_NONE;
} else if (touch_phase == TOUCH_HOVER) {
// Tap ended before the press was emitted; release right after it.
touch_release_after_press = true;
}
break;
case SDL_KEYDOWN:
case SDL_KEYUP: {
if (e->type == SDL_KEYDOWN && e->key.keysym.sym == SDLK_ESCAPE)
@@ -238,6 +282,8 @@ static void modal_render(void) {
}
}
SDL_RenderSetClipRect(gfx_renderer, NULL);
SDL_SetRenderDrawColor(gfx_renderer, 0, 0, 0, 255);
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_NONE);
}
static bool modal_event_trampoline(const SDL_Event *e) {
@@ -256,8 +302,25 @@ void modal_run(modal *m) {
mu_Id prev_hash = 0;
touch_phase = TOUCH_NONE;
touch_release_after_press = false;
bool open = true;
while (open && !nact->is_quit) {
// Drive the synthetic touch sequence.
if (touch_phase == TOUCH_HOVER) {
if (touch_hover_frames > 0) {
touch_hover_frames--;
} else {
mu_input_mousedown(ctx, touch_x, touch_y, MU_MOUSE_LEFT);
touch_phase = TOUCH_PRESS;
}
}
if (touch_phase == TOUCH_PRESS && touch_release_after_press) {
mu_input_mouseup(ctx, touch_x, touch_y, MU_MOUSE_LEFT);
touch_phase = TOUCH_NONE;
touch_release_after_press = false;
}
event_get_key(); // pump SDL events -> trampoline -> m->handler -> microui
mu_begin(ctx);
+1 -1
View File
@@ -44,7 +44,7 @@
#include "win/dialog.h"
#endif
#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__)
#ifndef __EMSCRIPTEN__
#define HAVE_VOLUME_VALANCER 1
#endif