From 3ebb1faece7d167ef248fd79b2c2da56a9498d74 Mon Sep 17 00:00:00 2001 From: kichikuou Date: Tue, 30 Jun 2026 16:31:18 +0900 Subject: [PATCH] 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. --- CHANGELOG.md | 2 +- android/app/jni/CMakeLists.txt | 1 + .../xsystem35/LicensesMenuActivity.kt | 1 + src/CMakeLists.txt | 2 - src/event.c | 4 +- src/menu_android.c | 65 ------------------- src/menu_sdl.c | 26 ++++++-- src/microui/microui.c | 6 ++ src/modal.c | 63 ++++++++++++++++++ src/xsystem35.c | 2 +- 10 files changed, 98 insertions(+), 74 deletions(-) delete mode 100644 src/menu_android.c diff --git a/CHANGELOG.md b/CHANGELOG.md index f2a0f82..5984ffe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/android/app/jni/CMakeLists.txt b/android/app/jni/CMakeLists.txt index ab69245..065a422 100644 --- a/android/app/jni/CMakeLists.txt +++ b/android/app/jni/CMakeLists.txt @@ -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) diff --git a/android/app/src/main/java/io/github/kichikuou/xsystem35/LicensesMenuActivity.kt b/android/app/src/main/java/io/github/kichikuou/xsystem35/LicensesMenuActivity.kt index f236526..4050b81 100644 --- a/android/app/src/main/java/io/github/kichikuou/xsystem35/LicensesMenuActivity.kt +++ b/android/app/src/main/java/io/github/kichikuou/xsystem35/LicensesMenuActivity.kt @@ -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"), diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f16f9f5..979c8ab 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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) diff --git a/src/event.c b/src/event.c index dd44f7e..83071de 100644 --- a/src/event.c +++ b/src/event.c @@ -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; diff --git a/src/menu_android.c b/src/menu_android.c deleted file mode 100644 index dded1a2..0000000 --- a/src/menu_android.c +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2020 - * - * 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 - -#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) { -} diff --git a/src/menu_sdl.c b/src/menu_sdl.c index 2e035e0..3b96450 100644 --- a/src/menu_sdl.c +++ b/src/menu_sdl.c @@ -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); diff --git a/src/microui/microui.c b/src/microui/microui.c index 0415e45..23e851e 100644 --- a/src/microui/microui.c +++ b/src/microui/microui.c @@ -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; diff --git a/src/modal.c b/src/modal.c index 3e5d1a0..30037e6 100644 --- a/src/modal.c +++ b/src/modal.c @@ -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); diff --git a/src/xsystem35.c b/src/xsystem35.c index 17ad723..721cecd 100644 --- a/src/xsystem35.c +++ b/src/xsystem35.c @@ -44,7 +44,7 @@ #include "win/dialog.h" #endif -#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) +#ifndef __EMSCRIPTEN__ #define HAVE_VOLUME_VALANCER 1 #endif