quakeforge/libs/ui/imui.c

495 lines
12 KiB
C
Raw Normal View History

/*
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),
.hot = nullent,
.active = nullent,
.mouse_position = {-1, -1},
};
ctx->tab = Hash_NewTable (511, imui_state_getkey, 0, ctx, &ctx->hashctx);
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);
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)
{
}