apple2ix/src/video/gltouchmenu.c
2016-02-22 22:41:37 -08:00

586 lines
17 KiB
C

/*
* Apple // emulator for *ix
*
* This software package is subject to the GNU General Public License
* version 3 or later (your choice) as published by the Free Software
* Foundation.
*
* Copyright 2013-2015 Aaron Culliney
*
*/
#include "video/glhudmodel.h"
#include "video/glnode.h"
#if !INTERFACE_TOUCH
#error this is a touch interface module, possibly you mean to not compile this at all?
#endif
#define MODEL_DEPTH -1/32.f
#define MENU_TEMPLATE_COLS 10
#define MENU_TEMPLATE_ROWS 2
#define MENU_FB_WIDTH (MENU_TEMPLATE_COLS * FONT80_WIDTH_PIXELS)
#define MENU_FB_HEIGHT (MENU_TEMPLATE_ROWS * FONT_HEIGHT_PIXELS)
#define MENU_OBJ_W 2.0
#define MENU_OBJ_H_LANDSCAPE (2.0/4.0) // NOTE : intent is to match touch keyboard height in landscape mode
#define MENU_OBJ_H_PORTRAIT (1.0/4.0)
static bool isAvailable = false; // Were there any OpenGL/memory errors on initialization?
static bool isEnabled = true; // Does player want this enabled?
// NOTE : intent is to match touch keyboard width
static uint8_t topMenuTemplate[MENU_TEMPLATE_ROWS][MENU_TEMPLATE_COLS+1] = {
"++ ++",
"++ ++",
};
// touch viewport
static struct {
int width;
int height;
// top left hitbox
int topLeftX;
int topLeftXHalf;
int topLeftXMax;
int topLeftY;
int topLeftYHalf;
int topLeftYMax;
// top right hitbox
int topRightX;
int topRightXHalf;
int topRightXMax;
int topRightY;
int topRightYHalf;
int topRightYMax;
GLfloat modelHeight;
} touchport = { 0 };
// touch menu variables
static struct {
GLModel *model;
unsigned int glyphMultiplier;
bool topLeftShowing;
bool topRightShowing;
struct timespec timingBegin;
float minAlpha; // Minimum alpha value of components (at zero, will not render)
float maxAlpha;
// pending changes requiring reinitialization
unsigned int nextGlyphMultiplier;
} menu = { 0 };
// ----------------------------------------------------------------------------
static inline void _present_menu(GLModel *parent) {
GLModelHUDElement *hudMenu = (GLModelHUDElement *)parent->custom;
memcpy(hudMenu->tpl, topMenuTemplate, sizeof(topMenuTemplate));
hudMenu->colorScheme = RED_ON_BLACK;
glhud_setupDefault(parent);
}
static inline void _show_top_left(void) {
topMenuTemplate[0][0] = ICONTEXT_MENU_SPROUT;
topMenuTemplate[0][1] = MOUSETEXT_RIGHT;
if (joydriver_ownsScreen()) {
topMenuTemplate[1][0] = ICONTEXT_UPPERCASE;
if (joydriver_getTouchVariant() == EMULATED_JOYSTICK) {
topMenuTemplate[1][1] = ICONTEXT_MENU_TOUCHJOY_KPAD;
} else {
topMenuTemplate[1][1] = ICONTEXT_MENU_TOUCHJOY;
}
} else {
topMenuTemplate[1][0] = ICONTEXT_MENU_TOUCHJOY;
topMenuTemplate[1][1] = ICONTEXT_MENU_TOUCHJOY_KPAD;
}
menu.topLeftShowing = true;
_present_menu(menu.model);
}
static inline void _hide_top_left(void) {
topMenuTemplate[0][0] = ICONTEXT_MENU_SPROUT;
topMenuTemplate[0][1] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[1][0] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[1][1] = ICONTEXT_NONACTIONABLE;
menu.topLeftShowing = false;
_present_menu(menu.model);
}
static inline void _show_top_right(void) {
topMenuTemplate[0][MENU_TEMPLATE_COLS-2] = MOUSETEXT_LEFT;
topMenuTemplate[0][MENU_TEMPLATE_COLS-1] = ICONTEXT_MENU_SPROUT;
topMenuTemplate[1][MENU_TEMPLATE_COLS-2] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[1][MENU_TEMPLATE_COLS-1] = MOUSETEXT_CHECKMARK;
menu.topRightShowing = true;
_present_menu(menu.model);
}
static inline void _hide_top_right(void) {
topMenuTemplate[0][MENU_TEMPLATE_COLS-2] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[0][MENU_TEMPLATE_COLS-1] = ICONTEXT_MENU_SPROUT;
topMenuTemplate[1][MENU_TEMPLATE_COLS-2] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[1][MENU_TEMPLATE_COLS-1] = ICONTEXT_NONACTIONABLE;
menu.topRightShowing = false;
_present_menu(menu.model);
}
static inline bool _is_point_on_left_menu(float x, float y) {
if (menu.topLeftShowing) {
return (x >= touchport.topLeftX && x <= touchport.topLeftXMax && y >= touchport.topLeftY && y <= touchport.topLeftYMax);
} else {
return (x >= touchport.topLeftX && x <= touchport.topLeftXHalf && y >= touchport.topLeftY && y <= touchport.topLeftYHalf);
}
}
static inline bool _is_point_on_right_menu(float x, float y) {
if (menu.topRightShowing) {
return (x >= touchport.topRightX && x <= touchport.topRightXMax && y >= touchport.topRightY && y <= touchport.topRightYMax);
} else {
return (x >= touchport.topRightXHalf && x <= touchport.topRightXMax && y >= touchport.topRightY && y <= touchport.topRightYHalf);
}
}
#warning FIXME TODO : make this function generic _screen_to_model() ?
static inline void _screen_to_menu(float x, float y, OUTPARM int *col, OUTPARM int *row) {
GLModelHUDElement *hudMenu = (GLModelHUDElement *)(menu.model->custom);
const unsigned int keyW = touchport.width / hudMenu->tplWidth;
const unsigned int keyH = touchport.topLeftYMax / hudMenu->tplHeight;
*col = x / keyW;
if (*col < 0) {
*col = 0;
} else if (*col >= hudMenu->tplWidth) {
*col = hudMenu->tplWidth-1;
}
*row = y / keyH;
if (*row < 0) {
*row = 0;
} else if (*row >= hudMenu->tplHeight) {
*row = hudMenu->tplWidth-1;
}
//LOG("SCREEN TO MENU : menuX:%d menuXMax:%d menuW:%d keyW:%d ... scrn:(%f,%f)->kybd:(%d,%d)", touchport.topLeftX, touchport.topLeftXMax, touchport.width, keyW, x, y, *col, *row);
}
static inline bool _sprout_menu(float x, float y) {
if (! (_is_point_on_left_menu(x, y) || _is_point_on_right_menu(x, y)) ) {
return false;
}
int col = -1;
int row = -1;
_screen_to_menu(x, y, &col, &row);
bool isTopLeft = (col <= 1);
bool isTopRight = (col >= MENU_TEMPLATE_COLS-2);
if (isTopLeft) {
// hide other
_hide_top_right();
// maybe show this one
if (menu.topLeftShowing) {
if (col == 0 && row == 0) {
_hide_top_left();
}
} else {
if (col == 0 && row == 0) {
_show_top_left();
}
}
return menu.topLeftShowing;
} else if (isTopRight) {
// hide other
_hide_top_left();
// maybe show this one
if (menu.topRightShowing) {
if (col == MENU_TEMPLATE_COLS-1 && row == 0) {
_hide_top_right();
}
} else {
if (col == MENU_TEMPLATE_COLS-1 && row == 0) {
_show_top_right();
}
}
return menu.topRightShowing;
} else {
RELEASE_ERRLOG("This should not happen");
return false;
}
}
static inline int64_t _tap_menu_item(float x, float y) {
if (! (_is_point_on_left_menu(x, y) || _is_point_on_right_menu(x, y)) ) {
return 0x0LL;
}
int col = -1;
int row = -1;
_screen_to_menu(x, y, &col, &row);
uint8_t selectedItem = topMenuTemplate[row][col];
int64_t flags = TOUCH_FLAGS_KEY_TAP | TOUCH_FLAGS_HANDLED;
switch (selectedItem) {
case MOUSETEXT_LEFT:
LOG("decreasing cpu speed...");
flags |= TOUCH_FLAGS_CPU_SPEED_DEC;
break;
case MOUSETEXT_RIGHT:
LOG("increasing cpu speed...");
flags |= TOUCH_FLAGS_CPU_SPEED_INC;
break;
case MOUSETEXT_CHECKMARK:
LOG("showing main menu...");
flags |= TOUCH_FLAGS_REQUEST_HOST_MENU;
_hide_top_right();
break;
case ICONTEXT_MENU_TOUCHJOY:
LOG("switching to joystick ...");
flags |= TOUCH_FLAGS_INPUT_DEVICE_CHANGE;
flags |= TOUCH_FLAGS_JOY;
_hide_top_left();
break;
case ICONTEXT_MENU_TOUCHJOY_KPAD:
LOG("switching to keypad joystick ...");
flags |= TOUCH_FLAGS_INPUT_DEVICE_CHANGE;
flags |= TOUCH_FLAGS_JOY_KPAD;
_hide_top_left();
break;
case ICONTEXT_UPPERCASE:
LOG("switching to keyboard ...");
flags |= TOUCH_FLAGS_INPUT_DEVICE_CHANGE;
flags |= TOUCH_FLAGS_KBD;
_hide_top_left();
break;
case ICONTEXT_MENU_SPROUT:
LOG("sprout ...");
break;
case ICONTEXT_NONACTIONABLE:
default:
LOG("nonactionable ...");
flags = 0x0LL;
_hide_top_left();
_hide_top_right();
break;
}
return flags;
}
// ----------------------------------------------------------------------------
// GLCustom functions
static void *_create_touchmenu_hud(GLModel *parent) {
parent->custom = glhud_createCustom(sizeof(GLModelHUDElement));
GLModelHUDElement *hudMenu = (GLModelHUDElement *)parent->custom;
if (!hudMenu) {
return NULL;
}
hudMenu->blackIsTransparent = true;
hudMenu->opaquePixelHalo = true;
hudMenu->glyphMultiplier = menu.glyphMultiplier;
hudMenu->tplWidth = MENU_TEMPLATE_COLS;
hudMenu->tplHeight = MENU_TEMPLATE_ROWS;
hudMenu->pixWidth = MENU_FB_WIDTH;
hudMenu->pixHeight = MENU_FB_HEIGHT;
topMenuTemplate[0][0] = ICONTEXT_MENU_SPROUT;
topMenuTemplate[0][1] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[1][0] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[1][1] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[0][MENU_TEMPLATE_COLS-2] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[0][MENU_TEMPLATE_COLS-1] = ICONTEXT_MENU_SPROUT;
topMenuTemplate[1][MENU_TEMPLATE_COLS-2] = ICONTEXT_NONACTIONABLE;
topMenuTemplate[1][MENU_TEMPLATE_COLS-1] = ICONTEXT_NONACTIONABLE;
for (unsigned int row=0; row<MENU_TEMPLATE_ROWS; row++) {
for (unsigned int col=2; col<MENU_TEMPLATE_COLS-2; col++) {
topMenuTemplate[row][col] = ICONTEXT_NONACTIONABLE;
}
}
const unsigned int size = sizeof(topMenuTemplate);
hudMenu->tpl = CALLOC(size, 1);
hudMenu->pixels = CALLOC(MENU_FB_WIDTH * MENU_FB_HEIGHT, 1);
_present_menu(parent);
return hudMenu;
}
// ----------------------------------------------------------------------------
// GLNode functions
static void gltouchmenu_shutdown(void) {
LOG("gltouchmenu_shutdown ...");
if (!isAvailable) {
return;
}
isAvailable = false;
menu.topLeftShowing = false;
menu.topRightShowing = false;
menu.nextGlyphMultiplier = 0;
mdlDestroyModel(&menu.model);
}
static void gltouchmenu_setup(void) {
LOG("gltouchmenu_setup ...");
gltouchmenu_shutdown();
menu.model = mdlCreateQuad((GLModelParams_s){
.skew_x = -1.0,
.skew_y = 1.0-touchport.modelHeight,
.z = MODEL_DEPTH,
.obj_w = MENU_OBJ_W,
.obj_h = touchport.modelHeight,
.positionUsageHint = GL_STATIC_DRAW, // positions don't change
.tex_w = MENU_FB_WIDTH * menu.glyphMultiplier,
.tex_h = MENU_FB_HEIGHT * menu.glyphMultiplier,
.texcoordUsageHint = GL_DYNAMIC_DRAW, // but menu texture does
}, (GLCustom){
.create = &_create_touchmenu_hud,
.destroy = &glhud_destroyDefault,
});
if (!menu.model) {
LOG("gltouchmenu initialization problem");
return;
}
if (!menu.model->custom) {
LOG("gltouchmenu HUD initialization problem");
return;
}
clock_gettime(CLOCK_MONOTONIC, &menu.timingBegin);
isAvailable = true;
GL_ERRLOG("gltouchmenu_setup");
}
static void gltouchmenu_render(void) {
if (!isAvailable) {
return;
}
if (!isEnabled) {
return;
}
if (menu.nextGlyphMultiplier) {
menu.glyphMultiplier = menu.nextGlyphMultiplier;
menu.nextGlyphMultiplier = 0;
gltouchmenu_setup();
}
float alpha = glhud_getTimedVisibility(menu.timingBegin, menu.minAlpha, menu.maxAlpha);
if (alpha < menu.minAlpha) {
alpha = menu.minAlpha;
}
if (alpha <= 0.0) {
return;
}
glViewport(0, 0, touchport.width, touchport.height); // NOTE : show these HUD elements beyond the A2 framebuffer dimensions
glUniform1f(alphaValue, alpha);
// render top sprouting menu(s)
glActiveTexture(TEXTURE_ACTIVE_TOUCHMENU);
glBindTexture(GL_TEXTURE_2D, menu.model->textureName);
if (menu.model->texDirty) {
menu.model->texDirty = false;
_HACKAROUND_GLTEXIMAGE2D_PRE(TEXTURE_ACTIVE_TOUCHMENU, menu.model->textureName);
glTexImage2D(GL_TEXTURE_2D, /*level*/0, TEX_FORMAT_INTERNAL, menu.model->texWidth, menu.model->texHeight, /*border*/0, TEX_FORMAT, TEX_TYPE, menu.model->texPixels);
}
glUniform1i(texSamplerLoc, TEXTURE_ID_TOUCHMENU);
glhud_renderDefault(menu.model);
GL_ERRLOG("gltouchmenu_render");
}
static void gltouchmenu_reshape(int w, int h, bool landscape) {
LOG("w:%d h:%d landscape:%d", w, h, landscape);
touchport.topLeftX = 0;
touchport.topLeftY = 0;
touchport.topRightY = 0;
swizzleDimensions(&w, &h, landscape);
touchport.width = w;
touchport.height = h;
touchport.modelHeight = landscape ? MENU_OBJ_H_LANDSCAPE : MENU_OBJ_H_PORTRAIT;;
const unsigned int keyW = touchport.width / MENU_TEMPLATE_COLS;
touchport.topLeftXHalf = keyW;
touchport.topLeftXMax = keyW*2;
touchport.topRightX = w - (keyW*2);
touchport.topRightXHalf = w - keyW;
touchport.topRightXMax = w;
const unsigned int menuH = h * (touchport.modelHeight/2.0);
touchport.topLeftYHalf = menuH/2;
touchport.topLeftYMax = menuH;
touchport.topRightYHalf = menuH/2;
touchport.topRightYMax = menuH;
}
static int64_t gltouchmenu_onTouchEvent(interface_touch_event_t action, int pointer_count, int pointer_idx, float *x_coords, float *y_coords) {
if (!isAvailable) {
return 0x0;
}
if (!isEnabled) {
return 0x0;
}
//LOG("gltouchmenu_onTouchEvent ...");
float x = x_coords[pointer_idx];
float y = y_coords[pointer_idx];
int flags = TOUCH_FLAGS_MENU;
static int trackingIndex = TRACKING_NONE;
switch (action) {
case TOUCH_DOWN:
case TOUCH_POINTER_DOWN:
if (_is_point_on_left_menu(x, y) || _is_point_on_right_menu(x, y)) {
trackingIndex = pointer_idx;
_sprout_menu(x, y);
flags |= TOUCH_FLAGS_HANDLED;
}
break;
case TOUCH_MOVE:
flags |= ((pointer_idx == trackingIndex) ? TOUCH_FLAGS_HANDLED : 0);
break;
case TOUCH_UP:
case TOUCH_POINTER_UP:
if (trackingIndex == pointer_idx) {
flags |= _tap_menu_item(x, y);
trackingIndex = TRACKING_NONE;
}
break;
case TOUCH_CANCEL:
trackingIndex = TRACKING_NONE;
LOG("---MENU TOUCH CANCEL");
return 0x0;
default:
trackingIndex = TRACKING_NONE;
LOG("!!!MENU UNKNOWN TOUCH EVENT : %d", action);
return 0x0;
}
if (flags & TOUCH_FLAGS_HANDLED) {
clock_gettime(CLOCK_MONOTONIC, &menu.timingBegin);
}
return flags;
}
// ----------------------------------------------------------------------------
// Animation and settings handling
static bool gltouchmenu_isTouchMenuAvailable(void) {
return isAvailable;
}
static void gltouchmenu_setTouchMenuEnabled(bool enabled) {
isEnabled = enabled;
}
static void _animation_showTouchMenu(void) {
clock_gettime(CLOCK_MONOTONIC, &menu.timingBegin);
}
static void _animation_hideTouchMenu(void) {
_hide_top_left();
_hide_top_right();
menu.timingBegin = (struct timespec){ 0 };
}
static void gltouchmenu_setTouchMenuVisibility(float inactiveAlpha, float activeAlpha) {
menu.minAlpha = inactiveAlpha;
menu.maxAlpha = activeAlpha;
}
static void gltouchmenu_setGlyphScale(int glyphScale) {
if (glyphScale == 0) {
glyphScale = 1;
}
menu.nextGlyphMultiplier = glyphScale;
}
// ----------------------------------------------------------------------------
// Constructor
static void _init_gltouchmenu(void) {
LOG("Registering OpenGL software touch menu");
video_animations->animation_showTouchMenu = &_animation_showTouchMenu;
video_animations->animation_hideTouchMenu = &_animation_hideTouchMenu;
interface_isTouchMenuAvailable = &gltouchmenu_isTouchMenuAvailable;
interface_setTouchMenuEnabled = &gltouchmenu_setTouchMenuEnabled;
interface_setTouchMenuVisibility = &gltouchmenu_setTouchMenuVisibility;
interface_setGlyphScale = &gltouchmenu_setGlyphScale;
menu.glyphMultiplier = 1;
menu.minAlpha = 1/4.f; // Minimum alpha value of components (at zero, will not render)
menu.maxAlpha = 1.f;
glnode_registerNode(RENDER_TOP, (GLNode){
.type = TOUCH_DEVICE_TOPMENU,
.setup = &gltouchmenu_setup,
.shutdown = &gltouchmenu_shutdown,
.render = &gltouchmenu_render,
.reshape = &gltouchmenu_reshape,
.onTouchEvent = &gltouchmenu_onTouchEvent,
.setData = NULL,
});
}
static __attribute__((constructor)) void __init_gltouchmenu(void) {
emulator_registerStartupCallback(CTOR_PRIORITY_LATE, &_init_gltouchmenu);
}