mirror of
https://git.code.sf.net/p/quake/quakeforge
synced 2024-11-13 00:24:12 +00:00
6d823cca84
This takes care of element order stability. It did need reworking the mouse tracking code (including adding an active flag to views), but now buttons seem to work correctly.
497 lines
12 KiB
C
497 lines
12 KiB
C
/*
|
|
imui.c
|
|
|
|
Immediate mode user inferface
|
|
|
|
Copyright (C) 2023 Bill Currie <bill@taniwha.org>
|
|
|
|
Author: Bill Currie <bill@taniwha.org>
|
|
Date: 2023/07/01
|
|
|
|
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:
|
|
|
|
Free Software Foundation, Inc.
|
|
59 Temple Place - Suite 330
|
|
Boston, MA 02111-1307, USA
|
|
|
|
*/
|
|
#ifdef HAVE_CONFIG_H
|
|
# include "config.h"
|
|
#endif
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "QF/ecs.h"
|
|
#include "QF/hash.h"
|
|
#include "QF/mathlib.h"
|
|
#include "QF/progs.h"
|
|
#include "QF/quakeio.h"
|
|
|
|
#include "QF/input/event.h"
|
|
|
|
#include "QF/ui/canvas.h"
|
|
#include "QF/ui/font.h"
|
|
#include "QF/ui/imui.h"
|
|
#include "QF/ui/text.h"
|
|
|
|
typedef struct imui_state_s {
|
|
struct imui_state_s *next;
|
|
struct imui_state_s **prev;
|
|
char *label;
|
|
uint32_t label_len;
|
|
int key_offset;
|
|
uint32_t frame_count;
|
|
uint32_t entity;
|
|
} imui_state_t;
|
|
|
|
struct imui_ctx_s {
|
|
canvas_system_t csys;
|
|
uint32_t canvas;
|
|
ecs_system_t vsys;
|
|
text_system_t tsys;
|
|
view_t root_view;
|
|
hashctx_t *hashctx;
|
|
hashtab_t *tab;
|
|
PR_RESMAP (imui_state_t) state_map;
|
|
imui_state_t *states;
|
|
font_t *font;
|
|
|
|
int64_t frame_start;
|
|
int64_t frame_draw;
|
|
int64_t frame_end;
|
|
uint32_t frame_count;
|
|
|
|
uint32_t hot;
|
|
uint32_t active;
|
|
bool mouse_pressed;
|
|
bool mouse_released;
|
|
unsigned mouse_buttons;
|
|
view_pos_t mouse_position;
|
|
unsigned shift;
|
|
int key_code;
|
|
int unicode;
|
|
};
|
|
|
|
static imui_state_t *
|
|
imui_state_new (imui_ctx_t *ctx)
|
|
{
|
|
imui_state_t *state = PR_RESNEW (ctx->state_map);
|
|
*state = (imui_state_t) {
|
|
.next = ctx->states,
|
|
.prev = &ctx->states,
|
|
.entity = nullent,
|
|
};
|
|
if (ctx->states) {
|
|
ctx->states->prev = &state->next;
|
|
}
|
|
ctx->states = state;
|
|
return state;
|
|
}
|
|
|
|
static void
|
|
imui_state_free (imui_ctx_t *ctx, imui_state_t *state)
|
|
{
|
|
if (state->next) {
|
|
state->next->prev = state->prev;
|
|
}
|
|
*state->prev = state->next;
|
|
PR_RESFREE (ctx->state_map, state);
|
|
}
|
|
|
|
static imui_state_t *
|
|
imui_get_state (imui_ctx_t *ctx, const char *label)
|
|
{
|
|
int key_offset = 0;
|
|
uint32_t label_len = ~0u;
|
|
const char *key = strstr (label, "##");
|
|
if (key) {
|
|
// key is '###': hash only past this
|
|
if (key[2] == '#') {
|
|
key_offset = (key += 3) - label;
|
|
}
|
|
label_len = key - label;
|
|
}
|
|
imui_state_t *state = Hash_Find (ctx->tab, label + key_offset);
|
|
if (state) {
|
|
state->frame_count = ctx->frame_count;
|
|
return state;
|
|
}
|
|
state = imui_state_new (ctx);
|
|
state->label = strdup (label);
|
|
state->label_len = label_len == ~0u ? strlen (label) : label_len;
|
|
state->key_offset = key_offset;
|
|
state->frame_count = ctx->frame_count;
|
|
Hash_Add (ctx->tab, state);
|
|
return state;
|
|
}
|
|
|
|
static const char *
|
|
imui_state_getkey (const void *obj, void *data)
|
|
{
|
|
auto state = (const imui_state_t *) obj;
|
|
return state->label + state->key_offset;
|
|
}
|
|
|
|
imui_ctx_t *
|
|
IMUI_NewContext (canvas_system_t canvas_sys, const char *font, float fontsize)
|
|
{
|
|
imui_ctx_t *ctx = malloc (sizeof (imui_ctx_t));
|
|
uint32_t canvas;
|
|
*ctx = (imui_ctx_t) {
|
|
.csys = canvas_sys,
|
|
.canvas = canvas = Canvas_New (canvas_sys),
|
|
.vsys = { canvas_sys.reg, canvas_sys.view_base },
|
|
.tsys = { canvas_sys.reg, canvas_sys.view_base, canvas_sys.text_base },
|
|
.root_view = Canvas_GetRootView (canvas_sys, canvas),
|
|
.tab = Hash_NewTable (511, imui_state_getkey, 0, ctx, &ctx->hashctx),
|
|
.hot = nullent,
|
|
.active = nullent,
|
|
.mouse_position = {-1, -1},
|
|
};
|
|
|
|
auto fpath = Font_SystemFont (font);
|
|
if (fpath) {
|
|
QFile *file = Qopen (fpath, "rb");
|
|
if (file) {
|
|
ctx->font = Font_Load (file, fontsize);
|
|
//Qclose (file); FIXME closed by QFS_LoadFile
|
|
}
|
|
free (fpath);
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
void
|
|
IMUI_DestroyContext (imui_ctx_t *ctx)
|
|
{
|
|
for (auto s = ctx->states; s; s = s->next) {
|
|
free (s->label);
|
|
}
|
|
PR_RESDELMAP (ctx->state_map);
|
|
|
|
if (ctx->font) {
|
|
Font_Free (ctx->font);
|
|
}
|
|
|
|
Hash_DelTable (ctx->tab);
|
|
Hash_DelContext (ctx->hashctx);
|
|
free (ctx);
|
|
}
|
|
|
|
void
|
|
IMUI_SetVisible (imui_ctx_t *ctx, bool visible)
|
|
{
|
|
*Canvas_Visible (ctx->csys, ctx->canvas) = visible;
|
|
}
|
|
|
|
void
|
|
IMUI_SetSize (imui_ctx_t *ctx, int xlen, int ylen)
|
|
{
|
|
View_SetLen (ctx->root_view, xlen, ylen);
|
|
View_UpdateHierarchy (ctx->root_view);
|
|
}
|
|
|
|
void
|
|
IMUI_ProcessEvent (imui_ctx_t *ctx, const IE_event_t *ie_event)
|
|
{
|
|
if (ie_event->type == ie_mouse) {
|
|
auto m = &ie_event->mouse;
|
|
ctx->mouse_position = (view_pos_t) { m->x, m->y };
|
|
|
|
unsigned old = ctx->mouse_buttons & 1;
|
|
unsigned new = m->buttons & 1;
|
|
ctx->mouse_pressed = (old ^ new) & new;
|
|
ctx->mouse_released = (old ^ new) & !new;
|
|
ctx->mouse_buttons = m->buttons;
|
|
} else {
|
|
auto k = &ie_event->key;
|
|
//printf ("imui: %d %d %x\n", k->code, k->unicode, k->shift);
|
|
ctx->shift = k->shift;
|
|
ctx->key_code = k->code;
|
|
ctx->unicode = k->unicode;
|
|
}
|
|
}
|
|
|
|
void
|
|
IMUI_BeginFrame (imui_ctx_t *ctx)
|
|
{
|
|
uint32_t root_ent = ctx->root_view.id;
|
|
Ent_RemoveComponent (root_ent, ctx->root_view.comp, ctx->root_view.reg);
|
|
if (!ECS_EntValid (root_ent, ctx->vsys.reg)) {
|
|
Sys_Error ("root got deleted");
|
|
}
|
|
ctx->root_view = View_AddToEntity (root_ent, ctx->vsys, nullview);
|
|
ctx->frame_start = Sys_LongTime ();
|
|
ctx->frame_count++;
|
|
}
|
|
|
|
static void
|
|
prune_objects (imui_ctx_t *ctx)
|
|
{
|
|
for (auto s = &ctx->states; *s; ) {
|
|
if ((*s)->frame_count == ctx->frame_count) {
|
|
s = &(*s)->next;
|
|
} else {
|
|
View_Delete (View_FromEntity (ctx->vsys, (*s)->entity));
|
|
Hash_Del (ctx->tab, (*s)->label + (*s)->key_offset);
|
|
imui_state_free (ctx, *s);
|
|
}
|
|
}
|
|
}
|
|
|
|
//FIXME currently works properly only for grav_northwest
|
|
static void
|
|
layout_objects (imui_ctx_t *ctx)
|
|
{
|
|
auto ref = View_GetRef (ctx->root_view);
|
|
auto h = ref->hierarchy;
|
|
|
|
byte *modified = h->components[view_modified];
|
|
view_pos_t *pos = h->components[view_pos];
|
|
view_pos_t *len = h->components[view_len];
|
|
viewcont_t *cont = h->components[view_control];
|
|
uint32_t *parent = h->parentIndex;
|
|
struct boolpair {
|
|
bool x, y;
|
|
} down_depend[h->num_objects];
|
|
|
|
// the root view size is always explicity
|
|
down_depend[0] = (struct boolpair) { false, false };
|
|
for (uint32_t i = 1; i < h->num_objects; i++) {
|
|
// printf ("%d %d %d [%d %d] [%d %d]\n", i, parent[i], h->childCount[i],
|
|
// pos[i].x, pos[i].y, len[i].x, len[i].y);
|
|
if (cont[i].semantic_x == IMUI_SizeKind_ChildrenSum) {
|
|
down_depend[i].x = 1;
|
|
} else if (!(down_depend[i].x = down_depend[parent[i]].x)
|
|
&& cont[i].semantic_x == IMUI_SizeKind_PercentOfParent) {
|
|
int x = (len[parent[i]].x * 100) / 100; //FIXME precent
|
|
modified[i] |= len[i].x != x;
|
|
len[i].x = x;
|
|
}
|
|
if (cont[i].semantic_y == IMUI_SizeKind_ChildrenSum) {
|
|
down_depend[i].y = 1;
|
|
} else if (!(down_depend[i].y = down_depend[parent[i]].y)
|
|
&& cont[i].semantic_y == IMUI_SizeKind_PercentOfParent) {
|
|
int y = (len[parent[i]].y * 100) / 100; //FIXME precent
|
|
modified[i] |= len[i].y != y;
|
|
len[i].y = y;
|
|
}
|
|
}
|
|
for (uint32_t i = h->num_objects; --i > 0; ) {
|
|
view_pos_t clen = len[i];
|
|
if (cont[i].semantic_x == IMUI_SizeKind_ChildrenSum) {
|
|
clen.x = 0;
|
|
if (cont[i].vertical) {
|
|
for (uint32_t j = 0; j < h->childCount[i]; j++) {
|
|
uint32_t child = h->childIndex[i] + j;
|
|
clen.x = max (clen.x, len[child].x);
|
|
}
|
|
} else {
|
|
for (uint32_t j = 0; j < h->childCount[i]; j++) {
|
|
uint32_t child = h->childIndex[i] + j;
|
|
clen.x += len[child].x;
|
|
}
|
|
}
|
|
}
|
|
if (cont[i].semantic_y == IMUI_SizeKind_ChildrenSum) {
|
|
clen.y = 0;
|
|
if (!cont[i].vertical) {
|
|
for (uint32_t j = 0; j < h->childCount[i]; j++) {
|
|
uint32_t child = h->childIndex[i] + j;
|
|
clen.y = max (clen.y, len[child].y);
|
|
}
|
|
} else {
|
|
for (uint32_t j = 0; j < h->childCount[i]; j++) {
|
|
uint32_t child = h->childIndex[i] + j;
|
|
clen.y += len[child].y;
|
|
}
|
|
}
|
|
}
|
|
modified[i] |= (len[i].x != clen.x) | (len[i].y != clen.y);
|
|
len[i] = clen;
|
|
}
|
|
|
|
view_pos_t cpos = {};
|
|
uint32_t cur_parent = 0;
|
|
for (uint32_t i = 1; i < h->num_objects; i++) {
|
|
if (parent[i] != cur_parent) {
|
|
cur_parent = parent[i];
|
|
cpos = (view_pos_t) {};
|
|
}
|
|
if (cont[i].semantic_x != IMUI_SizeKind_Null
|
|
&& cont[i].semantic_y != IMUI_SizeKind_Null) {
|
|
modified[i] |= (pos[i].x != cpos.x) | (pos[i].y != cpos.y);
|
|
pos[i] = cpos;
|
|
} else if (cont[i].semantic_x != IMUI_SizeKind_Null) {
|
|
modified[i] |= pos[i].x != cpos.x;
|
|
pos[i].x = cpos.x;
|
|
} else if (cont[i].semantic_y != IMUI_SizeKind_Null) {
|
|
modified[i] |= pos[i].y != cpos.y;
|
|
pos[i].y = cpos.y;
|
|
}
|
|
if (cont[parent[i]].vertical) {
|
|
cpos.y += cont[i].semantic_y == IMUI_SizeKind_Null ? 0 : len[i].y;
|
|
} else {
|
|
cpos.x += cont[i].semantic_x == IMUI_SizeKind_Null ? 0 : len[i].x;
|
|
}
|
|
}
|
|
|
|
View_UpdateHierarchy (ctx->root_view);
|
|
}
|
|
|
|
static void
|
|
check_inside (imui_ctx_t *ctx)
|
|
{
|
|
auto ref = View_GetRef (ctx->root_view);
|
|
auto h = ref->hierarchy;
|
|
|
|
uint32_t *entity = h->ent;
|
|
view_pos_t *abs = h->components[view_abs];
|
|
view_pos_t *len = h->components[view_len];
|
|
viewcont_t *cont = h->components[view_control];
|
|
auto mp = ctx->mouse_position;
|
|
|
|
ctx->hot = nullent;
|
|
for (uint32_t i = 0; i < h->num_objects; i++) {
|
|
if (cont[i].active
|
|
&& mp.x >= abs[i].x && mp.y >= abs[i].y
|
|
&& mp.x < abs[i].x + len[i].x && mp.y < abs[i].y + len[i].y) {
|
|
if (ctx->active == entity[i] || ctx->active == nullent) {
|
|
ctx->hot = entity[i];
|
|
}
|
|
}
|
|
}
|
|
//printf ("check_inside: %8x %8x\n", ctx->hot, ctx->active);
|
|
}
|
|
|
|
void
|
|
IMUI_Draw (imui_ctx_t *ctx)
|
|
{
|
|
ctx->frame_draw = Sys_LongTime ();
|
|
|
|
prune_objects (ctx);
|
|
layout_objects (ctx);
|
|
check_inside (ctx);
|
|
|
|
ctx->frame_end = Sys_LongTime ();
|
|
}
|
|
|
|
static bool
|
|
check_button_state (imui_ctx_t *ctx, uint32_t entity)
|
|
{
|
|
bool result = false;
|
|
//printf ("check_button_state: h:%8x a:%8x e:%8x\n", ctx->hot, ctx->active, entity);
|
|
if (ctx->active == entity) {
|
|
if (ctx->mouse_released) {
|
|
result = ctx->hot == entity;
|
|
ctx->active = nullent;
|
|
}
|
|
} else if (ctx->hot == entity) {
|
|
if (ctx->mouse_pressed) {
|
|
ctx->active = entity;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static view_t
|
|
add_text (view_t view, imui_state_t *state, imui_ctx_t *ctx)
|
|
{
|
|
uint32_t c_glyphs = ctx->csys.base + canvas_glyphs;
|
|
uint32_t c_passage_glyphs = ctx->csys.text_base + text_passage_glyphs;
|
|
auto reg = ctx->csys.reg;
|
|
|
|
auto text = Text_StringView (ctx->tsys, view, ctx->font,
|
|
state->label, state->label_len, 0, 0);
|
|
|
|
int ascender = ctx->font->face->size->metrics.ascender / 64;
|
|
int descender = ctx->font->face->size->metrics.descender / 64;
|
|
auto len = View_GetLen (text);
|
|
View_SetLen (text, len.x, ascender - descender);
|
|
// text is positioned such that 0 is the baseline, and +y offset moves
|
|
// the text down. The font's global ascender is used to find the baseline
|
|
// relative to the top of the view.
|
|
auto pos = View_GetPos (text);
|
|
View_SetPos (text, pos.x, pos.y - len.y + ascender);
|
|
View_SetGravity (text, grav_northwest);
|
|
|
|
View_SetVisible (text, 1);
|
|
Ent_SetComponent (text.id, c_glyphs, reg,
|
|
Ent_GetComponent (text.id, c_passage_glyphs, reg));
|
|
return text;
|
|
}
|
|
|
|
static void
|
|
update_hot_active (imui_ctx_t *ctx, uint32_t old_entity, uint32_t new_entity)
|
|
{
|
|
if (old_entity != nullent) {
|
|
if (ctx->hot == old_entity) {
|
|
ctx->hot = new_entity;
|
|
}
|
|
if (ctx->active == old_entity) {
|
|
ctx->active = new_entity;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool
|
|
IMUI_Button (imui_ctx_t *ctx, const char *label)
|
|
{
|
|
auto state = imui_get_state (ctx, label);
|
|
uint32_t old_entity = state->entity;
|
|
if (!ECS_EntValid (state->entity, ctx->csys.reg)) {
|
|
auto view = View_New (ctx->vsys, ctx->root_view);
|
|
state->entity = view.id;
|
|
|
|
*(byte*) Ent_AddComponent (view.id, ctx->csys.base + canvas_fill,
|
|
ctx->csys.reg) = 0;
|
|
|
|
View_SetVisible (view, 1);
|
|
*View_Control (view) = (viewcont_t) {
|
|
.gravity = grav_northwest,
|
|
.visible = 1,
|
|
.semantic_x = IMUI_SizeKind_Pixels,
|
|
.semantic_y = IMUI_SizeKind_Pixels,
|
|
.active = 1,
|
|
};
|
|
|
|
auto text = add_text (view, state, ctx);
|
|
auto len = View_GetLen (text);
|
|
View_SetLen (view, len.x, len.y);
|
|
}
|
|
update_hot_active (ctx, old_entity, state->entity);
|
|
bool result = check_button_state (ctx, state->entity);
|
|
return result;
|
|
}
|
|
|
|
bool
|
|
IMUI_Checkbox (imui_ctx_t *ctx, bool *flag, const char *label)
|
|
{
|
|
auto state = imui_get_state (ctx, label);
|
|
if (state->entity == nullent) {
|
|
}
|
|
return *flag;
|
|
}
|
|
|
|
void
|
|
IMUI_Radio (imui_ctx_t *ctx, int *state, int value, const char *label)
|
|
{
|
|
}
|
|
|
|
void
|
|
IMUI_Slider (imui_ctx_t *ctx, float *value, float minval, float maxval,
|
|
const char *label)
|
|
{
|
|
}
|