quakeforge/libs/ui/imui.c
Bill Currie 6d7e1064ec [ui] Support anchoring and extending panels
Panels can be anchored to a widget in another hierarchy, allowing for
things like cascading menus. They can also be extended via referencing
them by name, allowing for subsystems to add items to an already panel
(eg, extending a menu).
2023-07-13 14:22:31 +09:00

1244 lines
32 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/darray.h"
#include "QF/dstring.h"
#include "QF/ecs.h"
#include "QF/hash.h"
#include "QF/heapsort.h"
#include "QF/mathlib.h"
#include "QF/progs.h"
#include "QF/quakeio.h"
#include "QF/va.h"
#include "QF/input/event.h"
#include "QF/ui/canvas.h"
#include "QF/ui/font.h"
#include "QF/ui/imui.h"
#include "QF/ui/shaper.h"
#include "QF/ui/text.h"
#define c_percent_x (ctx->csys.imui_base + imui_percent_x)
#define c_percent_y (ctx->csys.imui_base + imui_percent_y)
#define c_reference (ctx->csys.imui_base + imui_reference)
#define c_glyphs (ctx->csys.base + canvas_glyphs)
#define c_passage_glyphs (ctx->csys.text_base + text_passage_glyphs)
#define c_color (ctx->tsys.text_base + text_color)
#define c_fill (ctx->csys.base + canvas_fill)
#define imui_draw_group ((1 << 14) - 1)
#define imui_ontop ((1 << 15) - 1)
#define imui_onbottom (-(1 << 15) + 1)
const component_t imui_components[imui_comp_count] = {
[imui_percent_x] = {
.size = sizeof (int),
.name = "percent x",
},
[imui_percent_y] = {
.size = sizeof (int),
.name = "percent y",
},
[imui_reference] = {
.size = sizeof (imui_reference_t),
.name = "reference",
},
};
typedef struct imui_state_s {
struct imui_state_s *next;
struct imui_state_s **prev;
char *label;
uint32_t label_len;
int key_offset;
int16_t draw_order; // for window canvases
uint32_t frame_count;
uint32_t entity;
uint32_t content;
} imui_state_t;
struct imui_ctx_s {
canvas_system_t csys;
uint32_t canvas;
ecs_system_t vsys;
text_system_t tsys;
text_shaper_t *shaper;
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;
view_t root_view;
view_t current_parent;
struct DARRAY_TYPE(view_t) parent_stack;
struct DARRAY_TYPE(imui_state_t *) windows;
int16_t draw_order;
dstring_t *dstr;
uint32_t hot;
uint32_t active;
view_pos_t mouse_active;
bool mouse_pressed;
bool mouse_released;
unsigned mouse_buttons;
view_pos_t mouse_position;
unsigned shift;
int key_code;
int unicode;
imui_style_t style;
struct DARRAY_TYPE(imui_style_t) style_stack;
};
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_find_state (imui_ctx_t *ctx, const char *label)
{
int key_offset = 0;
const char *key = strstr (label, "##");
if (key) {
// key is '###': hash only past this
if (key[2] == '#') {
key_offset = (key += 3) - label;
}
}
return Hash_Find (ctx->tab, label + key_offset);
}
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) {
label_len = key - label;
// key is '###': hash only past this
if (key[2] == '#') {
key_offset = (key += 3) - label;
}
}
auto state = imui_find_state (ctx, label);
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 },
.shaper = Shaper_New (),
.root_view = Canvas_GetRootView (canvas_sys, canvas),
.parent_stack = DARRAY_STATIC_INIT (8),
.windows = DARRAY_STATIC_INIT (8),
.dstr = dstring_newstr (),
.hot = nullent,
.active = nullent,
.mouse_position = {-1, -1},
.style_stack = DARRAY_STATIC_INIT (8),
.style = {
.background = {
.normal = 0x04,
.hot = 0x04,
.active = 0x04,
},
.foreground = {
.normal = 0x08,
.hot = 0x0f,
.active = 0x0c,
},
.text = {
.normal = 0xfe,
.hot = 0x5f,
.active = 0x6f,
},
},
};
*Canvas_DrawGroup (ctx->csys, ctx->canvas) = imui_draw_group;
ctx->tab = Hash_NewTable (511, imui_state_getkey, 0, ctx, &ctx->hashctx);
ctx->current_parent = ctx->root_view;
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);
}
DARRAY_CLEAR (&ctx->parent_stack);
DARRAY_CLEAR (&ctx->windows);
DARRAY_CLEAR (&ctx->style_stack);
dstring_delete (ctx->dstr);
Hash_DelTable (ctx->tab);
Hash_DelContext (ctx->hashctx);
Shaper_Delete (ctx->shaper);
free (ctx);
}
void
IMUI_SetVisible (imui_ctx_t *ctx, bool visible)
{
if (!visible) {
ctx->active = nullent;
}
*Canvas_Visible (ctx->csys, ctx->canvas) = visible;
for (uint32_t i = 0; i < ctx->windows.size; i++) {
*Canvas_Visible (ctx->csys, ctx->windows.a[i]->entity) = visible;
}
}
void
IMUI_SetSize (imui_ctx_t *ctx, int xlen, int ylen)
{
Canvas_SetLen (ctx->csys, ctx->canvas, (view_pos_t) { xlen, ylen });
}
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)
{
Shaper_FlushUnused (ctx->shaper);
uint32_t root_ent = ctx->root_view.id;
auto root_size = View_GetLen (ctx->root_view);
Ent_RemoveComponent (root_ent, ctx->root_view.comp, ctx->root_view.reg);
ctx->root_view = View_AddToEntity (root_ent, ctx->vsys, nullview);
auto ref = View_GetRef (ctx->root_view);
Hierarchy_SetTreeMode (ref->hierarchy, true);
View_SetLen (ctx->root_view, root_size.x, root_size.y);
ctx->frame_start = Sys_LongTime ();
ctx->frame_count++;
ctx->current_parent = ctx->root_view;
for (uint32_t i = 0; i < ctx->windows.size; i++) {
auto window = View_FromEntity (ctx->vsys, ctx->windows.a[i]->entity);
View_Delete (window);
}
ctx->draw_order = ctx->windows.size;
DARRAY_RESIZE (&ctx->parent_stack, 0);
DARRAY_RESIZE (&ctx->windows, 0);
DARRAY_RESIZE (&ctx->style_stack, 0);
}
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 {
Hash_Del (ctx->tab, (*s)->label + (*s)->key_offset);
free ((*s)->label);
imui_state_free (ctx, *s);
}
}
}
#define DFL "\e[39;49m"
#define BLK "\e[30;40m"
#define RED "\e[31;40m"
#define GRN "\e[32;40m"
#define ONG "\e[33;40m"
#define BLU "\e[34;40m"
#define MAG "\e[35;40m"
#define CYN "\e[36;40m"
#define WHT "\e[37;40m"
static const char *
view_color (hierarchy_t *h, uint32_t ind, imui_ctx_t *ctx, bool for_y)
{
auto reg = h->reg;
uint32_t e = h->ent[ind];
viewcont_t *cont = h->components[view_control];
auto semantic = (imui_size_t) cont[ind].semantic_x;
if (for_y) {
semantic = cont[ind].semantic_y;
}
switch (semantic) {
case imui_size_none:
if (Ent_HasComponent (e, c_glyphs, reg)) {
return CYN;
}
return DFL;
case imui_size_pixels: return GRN;
case imui_size_fittext: return CYN;
case imui_size_percent: return ONG;
case imui_size_fitchildren: return MAG;
case imui_size_expand: return RED;
}
return BLU;
}
static void __attribute__((used))
dump_tree (hierarchy_t *h, uint32_t ind, int level, imui_ctx_t *ctx)
{
view_pos_t *len = h->components[view_len];
auto c = ((viewcont_t *)h->components[view_control])[ind];
uint32_t e = h->ent[ind];
printf ("%2d: %*s[%s%d %s%d"DFL"] %c %s%d %s%d"DFL, ind,
level * 3, "",
view_color (h, ind, ctx, false), len[ind].x,
view_color (h, ind, ctx, true), len[ind].y,
c.vertical ? 'v' : 'h',
view_color (h, ind, ctx, false), c.semantic_x,
view_color (h, ind, ctx, true), c.semantic_y);
for (uint32_t j = 0; j < h->reg->components.size; j++) {
if (Ent_HasComponent (e, j, h->reg)) {
printf (", %s", h->reg->components.a[j].name);
if (j == c_percent_x || j == c_percent_y) {
auto val = *(int *) Ent_GetComponent (e, j, h->reg);
printf ("(%s%d"DFL")", view_color (h, ind, ctx,
j == c_percent_y), val);
}
}
}
printf (DFL"\n");
if (h->childIndex[ind] > ind) {
for (uint32_t i = 0; i < h->childCount[ind]; i++) {
if (h->childIndex[ind] + i >= h->num_objects) {
break;
}
dump_tree (h, h->childIndex[ind] + i, level + 1, ctx);
}
}
if (!level) {
puts ("");
}
}
typedef struct {
bool x, y;
} boolpair_t;
static void
calc_upwards_dependent (imui_ctx_t *ctx, hierarchy_t *h,
boolpair_t *down_depend)
{
auto reg = ctx->csys.reg;
uint32_t *ent = h->ent;
view_pos_t *len = h->components[view_len];
viewcont_t *cont = h->components[view_control];
uint32_t *parent = h->parentIndex;
for (uint32_t i = 0; i < h->num_objects; i++) {
if (down_depend
&& (cont[i].semantic_x == imui_size_fitchildren
|| cont[i].semantic_x == imui_size_expand)) {
down_depend[i].x = true;
} else if ((!down_depend
|| !(i > 0
&& (down_depend[i].x = down_depend[parent[i]].x)))
&& cont[i].semantic_x == imui_size_percent) {
int *percent = Ent_GetComponent (ent[i], c_percent_x, reg);
int x = (len[parent[i]].x * *percent) / 100;
len[i].x = x;
}
if (down_depend
&& (cont[i].semantic_y == imui_size_fitchildren
|| cont[i].semantic_y == imui_size_expand)) {
down_depend[i].y = true;
} else if ((!down_depend
|| !(i > 0
&& (down_depend[i].y = down_depend[parent[i]].y)))
&& cont[i].semantic_y == imui_size_percent) {
int *percent = Ent_GetComponent (ent[i], c_percent_y, reg);
int y = (len[parent[i]].y * *percent) / 100;
len[i].y = y;
}
}
}
static void
calc_downwards_dependent (hierarchy_t *h)
{
view_pos_t *len = h->components[view_len];
viewcont_t *cont = h->components[view_control];
for (uint32_t i = h->num_objects; i-- > 0; ) {
view_pos_t clen = len[i];
if (cont[i].semantic_x == imui_size_fitchildren
|| cont[i].semantic_x == imui_size_expand) {
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_size_fitchildren
|| cont[i].semantic_y == imui_size_expand) {
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;
}
}
}
len[i] = clen;
}
}
static void
calc_expansions (imui_ctx_t *ctx, hierarchy_t *h)
{
auto reg = ctx->csys.reg;
uint32_t *ent = h->ent;
view_pos_t *len = h->components[view_len];
viewcont_t *cont = h->components[view_control];
for (uint32_t i = 0; i < h->num_objects; i++) {
view_pos_t tlen = {};
view_pos_t elen = {};
view_pos_t ecount = {};
for (uint32_t j = 0; j < h->childCount[i]; j++) {
uint32_t child = h->childIndex[i] + j;
tlen.x += len[child].x;
tlen.y += len[child].y;
if (cont[child].semantic_x == imui_size_expand) {
int *p = Ent_GetComponent (ent[child], c_percent_x, reg);
elen.x += *p;
ecount.x++;
}
if (cont[child].semantic_y == imui_size_expand) {
int *p = Ent_GetComponent (ent[child], c_percent_y, reg);
elen.y += *p;
ecount.y++;
}
}
if (!ecount.x && !ecount.y) {
continue;
}
if (cont[i].vertical) {
int space = len[i].y - tlen.y;
int filled = 0;
for (uint32_t j = 0; j < h->childCount[i]; j++) {
uint32_t child = h->childIndex[i] + j;
if (cont[child].semantic_x == imui_size_expand) {
len[child].x = len[i].x;
}
if (cont[child].semantic_y == imui_size_expand) {
int *p = Ent_GetComponent (ent[child], c_percent_y, reg);
int delta = *p * space / elen.y;
len[child].y += max (delta, 0);
filled += max (delta, 0);
}
}
if (ecount.y && filled < space) {
space -= filled;
for (uint32_t j = 0; space && j < h->childCount[i]; j++) {
uint32_t child = h->childIndex[i] + j;
if (cont[child].semantic_y == imui_size_expand) {
len[child].y++;
space--;
}
}
}
} else {
int space = len[i].x - tlen.x;
int filled = 0;
for (uint32_t j = 0; j < h->childCount[i]; j++) {
uint32_t child = h->childIndex[i] + j;
if (cont[child].semantic_x == imui_size_expand) {
int *p = Ent_GetComponent (ent[child], c_percent_x, reg);
int delta = *p * space / elen.x;
len[child].x += max (delta, 0);
filled += max (delta, 0);
}
if (cont[child].semantic_y == imui_size_expand) {
len[child].y = len[i].y;
}
}
if (ecount.x && filled < space) {
space -= filled;
for (uint32_t j = 0; space && j < h->childCount[i]; j++) {
uint32_t child = h->childIndex[i] + j;
if (cont[child].semantic_x == imui_size_expand) {
len[child].x++;
space--;
}
}
}
}
}
}
//FIXME currently works properly only for grav_northwest
static void
layout_objects (imui_ctx_t *ctx, view_t root_view)
{
auto ref = View_GetRef (root_view);
auto h = ref->hierarchy;
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;
boolpair_t down_depend[h->num_objects];
// the root view size is always explicit
down_depend[0] = (boolpair_t) { false, false };
calc_upwards_dependent (ctx, h, down_depend);
calc_downwards_dependent (h);
calc_expansions (ctx, h);
//dump_tree (h, 0, 0, ctx);
// resolve conflicts
//fflush (stdout);
if (Ent_HasComponent (root_view.id, c_reference, ctx->vsys.reg)) {
auto ent = root_view.id;
auto reg = ctx->vsys.reg;
imui_reference_t *reference = Ent_GetComponent (ent, c_reference, reg);
auto anchor = View_FromEntity (ctx->vsys, reference->ref_id);
pos[0] = View_GetAbs (anchor);
}
view_pos_t cpos = {};
uint32_t cur_parent = 0;
for (uint32_t i = 0; i < h->num_objects; i++) {
if (parent[i] != cur_parent) {
cur_parent = parent[i];
cpos = (view_pos_t) {};
}
if (!cont[i].free_x && !cont[i].free_y) {
pos[i] = cpos;
} else if (!cont[i].free_x) {
pos[i].x = cpos.x;
} else if (!cont[i].free_y) {
pos[i].y = cpos.y;
}
if (i > 0 && cont[parent[i]].vertical) {
cpos.y += cont[i].semantic_y == imui_size_none ? 0 : len[i].y;
} else {
cpos.x += cont[i].semantic_x == imui_size_none ? 0 : len[i].x;
}
}
View_UpdateHierarchy (root_view);
}
static void
check_inside (imui_ctx_t *ctx, view_t root_view)
{
auto ref = View_GetRef (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;
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);
}
static int
imui_window_cmp (const void *a, const void *b)
{
auto windowa = *(imui_state_t **) a;
auto windowb = *(imui_state_t **) b;
return windowa->draw_order - windowb->draw_order;
}
static void
sort_windows (imui_ctx_t *ctx)
{
heapsort (ctx->windows.a, ctx->windows.size, sizeof (imui_state_t *),
imui_window_cmp);
for (uint32_t i = 0; i < ctx->windows.size; i++) {
auto window = ctx->windows.a[i];
window->draw_order = i + 1;
*Canvas_DrawOrder (ctx->csys, window->entity) = window->draw_order;
}
}
void
IMUI_Draw (imui_ctx_t *ctx)
{
ctx->frame_draw = Sys_LongTime ();
sort_windows (ctx);
auto ref = View_GetRef (ctx->root_view);
Hierarchy_SetTreeMode (ref->hierarchy, false);
for (uint32_t i = 0; i < ctx->windows.size; i++) {
auto window = View_FromEntity (ctx->vsys, ctx->windows.a[i]->entity);
auto ref = View_GetRef (window);
Hierarchy_SetTreeMode (ref->hierarchy, false);
}
Canvas_DrawSort (ctx->csys);
prune_objects (ctx);
layout_objects (ctx, ctx->root_view);
for (uint32_t i = 0; i < ctx->windows.size; i++) {
auto window = View_FromEntity (ctx->vsys, ctx->windows.a[i]->entity);
layout_objects (ctx, window);
}
ctx->hot = nullent;
check_inside (ctx, ctx->root_view);
for (uint32_t i = 0; i < ctx->windows.size; i++) {
auto window = View_FromEntity (ctx->vsys, ctx->windows.a[i]->entity);
check_inside (ctx, window);
}
ctx->frame_end = Sys_LongTime ();
}
int
IMUI_PushLayout (imui_ctx_t *ctx, bool vertical)
{
DARRAY_APPEND (&ctx->parent_stack, ctx->current_parent);
auto view = View_New (ctx->vsys, ctx->current_parent);
auto pcont = View_Control (ctx->current_parent);
ctx->current_parent = view;
auto x_size = pcont->vertical ? imui_size_expand
: imui_size_fitchildren;
auto y_size = pcont->vertical ? imui_size_fitchildren
: imui_size_expand;
*View_Control (view) = (viewcont_t) {
.gravity = grav_northwest,
.visible = 1,
.semantic_x = x_size,
.semantic_y = y_size,
.vertical = vertical,
};
View_SetLen (view, 0, 0);
if (x_size == imui_size_expand) {
*(int*) Ent_AddComponent (view.id, c_percent_x, ctx->csys.reg) = 100;
}
if (y_size == imui_size_expand) {
*(int*) Ent_AddComponent (view.id, c_percent_y, ctx->csys.reg) = 100;
}
return 0;
}
void
IMUI_PopLayout (imui_ctx_t *ctx)
{
ctx->current_parent = DARRAY_REMOVE (&ctx->parent_stack);
}
void
IMUI_Layout_SetXSize (imui_ctx_t *ctx, imui_size_t size, int value)
{
auto pcont = View_Control (ctx->current_parent);
uint32_t id = ctx->current_parent.id;
pcont->semantic_x = size;
if (size == imui_size_percent || imui_size_expand) {
*(int *) Ent_AddComponent(id, c_percent_x, ctx->csys.reg) = value;
}
}
void
IMUI_Layout_SetYSize (imui_ctx_t *ctx, imui_size_t size, int value)
{
auto pcont = View_Control (ctx->current_parent);
uint32_t id = ctx->current_parent.id;
pcont->semantic_y = size;
if (size == imui_size_percent || imui_size_expand) {
*(int *) Ent_AddComponent(id, c_percent_y, ctx->csys.reg) = value;
}
}
int
IMUI_PushStyle (imui_ctx_t *ctx, const imui_style_t *style)
{
DARRAY_APPEND (&ctx->style_stack, ctx->style);
if (style) {
ctx->style = *style;
}
return 0;
}
void
IMUI_PopStyle (imui_ctx_t *ctx)
{
ctx->style = DARRAY_REMOVE (&ctx->style_stack);
}
void
IMUI_Style_Update (imui_ctx_t *ctx, const imui_style_t *style)
{
ctx->style = *style;
}
void
IMUI_Style_Fetch (const imui_ctx_t *ctx, imui_style_t *style)
{
*style = ctx->style;
}
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_pos_t
check_drag_delta (imui_ctx_t *ctx, uint32_t entity)
{
view_pos_t delta = {};
if (ctx->active == entity) {
delta.x = ctx->mouse_position.x - ctx->mouse_active.x;
delta.y = ctx->mouse_position.y - ctx->mouse_active.y;
ctx->mouse_active = ctx->mouse_position;
if (ctx->mouse_released) {
ctx->active = nullent;
}
} else if (ctx->hot == entity) {
if (ctx->mouse_pressed) {
ctx->mouse_active = ctx->mouse_position;
ctx->active = entity;
}
}
return delta;
}
static view_t
add_text (imui_ctx_t *ctx, view_t view, imui_state_t *state, int mode)
{
auto reg = ctx->csys.reg;
auto text = Text_StringView (ctx->tsys, view, ctx->font,
state->label, state->label_len, 0, 0,
ctx->shaper);
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);
// prevent the layout system from repositioning the text view
View_Control (text)->free_x = 1;
View_Control (text)->free_y = 1;
View_SetVisible (text, 1);
Ent_SetComponent (text.id, c_glyphs, reg,
Ent_GetComponent (text.id, c_passage_glyphs, reg));
len = View_GetLen (text);
View_SetLen (view, len.x, len.y);
uint32_t color = ctx->style.text.color[mode];
*(uint32_t *) Ent_AddComponent (text.id, c_color, ctx->tsys.reg) = color;
return text;
}
static int
update_hot_active (imui_ctx_t *ctx, uint32_t old_entity, uint32_t new_entity)
{
int mode = 0;
if (old_entity != nullent) {
if (ctx->hot == old_entity) {
ctx->hot = new_entity;
mode = 1;
}
if (ctx->active == old_entity) {
ctx->active = new_entity;
mode = 2;
}
}
return mode;
}
static void
set_fill (imui_ctx_t *ctx, view_t view, byte color)
{
*(byte*) Ent_AddComponent (view.id, c_fill, ctx->csys.reg) = color;
}
static void
set_control (imui_ctx_t *ctx, view_t view, bool active)
{
*View_Control (view) = (viewcont_t) {
.gravity = grav_northwest,
.visible = 1,
.semantic_x = imui_size_pixels,
.semantic_y = imui_size_pixels,
.active = active,
};
}
static void
set_expand_x (imui_ctx_t *ctx, view_t view, int weight)
{
View_Control (view)->semantic_x = imui_size_expand;
*(int *) Ent_AddComponent(view.id, c_percent_x, ctx->csys.reg) = weight;
}
void
IMUI_Label (imui_ctx_t *ctx, const char *label)
{
auto state = imui_get_state (ctx, label);
auto view = View_New (ctx->vsys, ctx->current_parent);
state->entity = view.id;
set_control (ctx, view, true);
set_fill (ctx, view, ctx->style.background.normal);
add_text (ctx, view, state, 0);
}
void
IMUI_Labelf (imui_ctx_t *ctx, const char *fmt, ...)
{
va_list args;
va_start (args, fmt);
dvsprintf (ctx->dstr, fmt, args);
va_end (args);
IMUI_Label (ctx, ctx->dstr->str);
}
bool
IMUI_Button (imui_ctx_t *ctx, const char *label)
{
auto state = imui_get_state (ctx, label);
uint32_t old_entity = state->entity;
auto view = View_New (ctx->vsys, ctx->current_parent);
state->entity = view.id;
int mode = update_hot_active (ctx, old_entity, state->entity);
set_control (ctx, view, true);
set_fill (ctx, view, ctx->style.foreground.color[mode]);
add_text (ctx, view, state, mode);
return check_button_state (ctx, state->entity);
}
bool
IMUI_Checkbox (imui_ctx_t *ctx, bool *flag, const char *label)
{
auto state = imui_get_state (ctx, label);
uint32_t old_entity = state->entity;
auto view = View_New (ctx->vsys, ctx->current_parent);
state->entity = view.id;
int mode = update_hot_active (ctx, old_entity, state->entity);
set_control (ctx, view, true);
View_Control (view)->semantic_x = imui_size_fitchildren;
View_Control (view)->semantic_y = imui_size_fitchildren;
set_fill (ctx, view, ctx->style.background.color[mode]);
auto checkbox = View_New (ctx->vsys, view);
set_control (ctx, checkbox, false);
View_SetLen (checkbox, 20, 20);
set_fill (ctx, checkbox, ctx->style.foreground.color[mode]);
if (!*flag) {
auto punch = View_New (ctx->vsys, checkbox);
set_control (ctx, punch, false);
View_SetGravity (punch, grav_center);
View_SetLen (punch, 14, 14);
set_fill (ctx, punch, ctx->style.background.color[mode]);
}
auto text = View_New (ctx->vsys, view);
set_control (ctx, text, false);
add_text (ctx, text, state, mode);
if (check_button_state (ctx, state->entity)) {
*flag = !*flag;
}
return *flag;
}
void
IMUI_Radio (imui_ctx_t *ctx, int *curvalue, int value, const char *label)
{
auto state = imui_get_state (ctx, label);
uint32_t old_entity = state->entity;
auto view = View_New (ctx->vsys, ctx->current_parent);
state->entity = view.id;
int mode = update_hot_active (ctx, old_entity, state->entity);
set_control (ctx, view, true);
View_Control (view)->semantic_x = imui_size_fitchildren;
View_Control (view)->semantic_y = imui_size_fitchildren;
set_fill (ctx, view, ctx->style.background.color[mode]);
auto checkbox = View_New (ctx->vsys, view);
set_control (ctx, checkbox, false);
View_SetLen (checkbox, 20, 20);
set_fill (ctx, checkbox, ctx->style.foreground.color[mode]);
if (*curvalue != value) {
auto punch = View_New (ctx->vsys, checkbox);
set_control (ctx, punch, false);
View_SetGravity (punch, grav_center);
View_SetLen (punch, 14, 14);
set_fill (ctx, punch, ctx->style.background.color[mode]);
}
if (state->label_len) {
auto text = View_New (ctx->vsys, view);
set_control (ctx, text, false);
add_text (ctx, text, state, mode);
}
if (check_button_state (ctx, state->entity)) {
*curvalue = value;
}
}
void
IMUI_Slider (imui_ctx_t *ctx, float *value, float minval, float maxval,
const char *label)
{
}
void
IMUI_Spacer (imui_ctx_t *ctx,
imui_size_t xsize, int xvalue,
imui_size_t ysize, int yvalue)
{
auto view = View_New (ctx->vsys, ctx->current_parent);
View_SetLen (ctx->current_parent, 0, 0);
int xlen = 0;
int ylen = 0;
if (xsize == imui_size_pixels) {
xlen = xvalue;
}
if (ysize == imui_size_pixels) {
ylen = yvalue;
}
set_control (ctx, view, false);
View_SetLen (view, xlen, ylen);
View_Control (view)->semantic_x = xsize;
View_Control (view)->semantic_y = ysize;
auto reg = ctx->csys.reg;
if (xsize == imui_size_percent || xsize == imui_size_expand) {
*(int*) Ent_AddComponent (view.id, c_percent_x, reg) = xvalue;
}
if (ysize == imui_size_percent || ysize == imui_size_expand) {
*(int*) Ent_AddComponent (view.id, c_percent_y, reg) = yvalue;
}
set_fill (ctx, view, ctx->style.background.normal);
}
static void
create_reference_anchor (imui_ctx_t *ctx, uint32_t ent, imui_window_t *panel)
{
auto state = imui_find_state (ctx, panel->reference);
if (!state) {
Sys_Printf ("IMUI: unknown widget '%s' for '%s'\n", panel->reference,
panel->name);
return;
}
if (!ECS_EntValid (state->entity, ctx->csys.reg)) {
Sys_Printf ("IMUI: invalid widget reference '%s' for '%s'\n",
panel->reference, panel->name);
return;
}
auto refview = View_FromEntity (ctx->vsys, state->entity);
auto anchor = View_New (ctx->vsys, refview);
View_SetPos (anchor, 0, 0);
View_SetLen (anchor, 0, 0);
View_SetGravity (anchor, panel->anchor_gravity);
imui_reference_t reference = {
.ref_id = anchor.id,
};
Ent_SetComponent (ent, c_reference, ctx->vsys.reg, &reference);
}
int
IMUI_StartPanel (imui_ctx_t *ctx, imui_window_t *panel)
{
if (!panel->is_open) {
return 1;
}
auto state = imui_get_state (ctx, panel->name);
uint32_t old_entity = state->entity;
DARRAY_APPEND (&ctx->parent_stack, ctx->current_parent);
auto canvas = Canvas_New (ctx->csys);
int draw_group = imui_draw_group + panel->group_offset;
*Canvas_DrawGroup (ctx->csys, canvas) = draw_group;
auto panel_view = Canvas_GetRootView (ctx->csys, canvas);
state->entity = panel_view.id;
panel->mode = update_hot_active (ctx, old_entity, state->entity);
auto ref = View_GetRef (panel_view);
Hierarchy_SetTreeMode (ref->hierarchy, true);
grav_t gravity = grav_northwest;
if (panel->reference) {
create_reference_anchor (ctx, canvas, panel);
gravity = panel->reference_gravity;
}
DARRAY_APPEND (&ctx->windows, state);
if (!state->draw_order) {
state->draw_order = ++ctx->draw_order;
}
ctx->current_parent = panel_view;
*View_Control (panel_view) = (viewcont_t) {
.gravity = gravity,
.visible = 1,
.semantic_x = imui_size_fitchildren,
.semantic_y = imui_size_fitchildren,
.free_x = 1,
.free_y = 1,
.vertical = true,
.active = 1,
};
View_SetPos (panel_view, panel->xpos, panel->ypos);
View_SetLen (panel_view, panel->xlen, panel->ylen);
auto bg = ctx->style.background.normal;
#define IMUI_context ctx
UI_Vertical {
ctx->style.background.normal = 0;//FIXME style
IMUI_Spacer (ctx, imui_size_expand, 100, imui_size_pixels, 2);
UI_Horizontal {
IMUI_Spacer (ctx, imui_size_pixels, 2, imui_size_expand, 100);
UI_Vertical {
IMUI_Layout_SetXSize (ctx, imui_size_expand, 100);
panel_view = ctx->current_parent;
}
IMUI_Spacer (ctx, imui_size_pixels, 2, imui_size_expand, 100);
}
IMUI_Spacer (ctx, imui_size_expand, 100, imui_size_pixels, 2);
}
#undef IMUI_context
ctx->style.background.normal = bg;
ctx->current_parent = panel_view;
state->content = panel_view.id;
return 0;
}
int
IMUI_ExtendPanel (imui_ctx_t *ctx, const char *panel_name)
{
auto state = imui_find_state (ctx, panel_name);
if (!state || !ECS_EntValid (state->entity, ctx->vsys.reg)) {
return 1;
}
DARRAY_APPEND (&ctx->parent_stack, ctx->current_parent);
ctx->current_parent = View_FromEntity (ctx->vsys, state->content);
return 0;
}
void
IMUI_EndPanel (imui_ctx_t *ctx)
{
IMUI_PopLayout (ctx);
}
int
IMUI_StartWindow (imui_ctx_t *ctx, imui_window_t *window)
{
if (!window->is_open) {
return 1;
}
IMUI_StartPanel (ctx, window);
auto state = ctx->windows.a[ctx->windows.size - 1];
#define IMUI_context ctx
UI_Horizontal {
char cbutton = window->is_collapsed ? '>' : 'v';
if (UI_Button (va (0, "%c##collapse_%s", cbutton, window->name))) {
window->is_collapsed = !window->is_collapsed;
}
auto tb_state = imui_get_state (ctx, va (0, "%s##title_bar",
window->name));
uint32_t tb_old_entity = tb_state->entity;
auto title_bar = View_New (ctx->vsys, ctx->current_parent);
tb_state->entity = title_bar.id;
int tb_mode = update_hot_active (ctx, tb_old_entity, tb_state->entity);
auto delta = check_drag_delta (ctx, tb_state->entity);
if (ctx->active == tb_state->entity) {
state->draw_order = imui_ontop;
window->xpos += delta.x;
window->ypos += delta.y;
}
set_control (ctx, title_bar, true);
set_expand_x (ctx, title_bar, 100);
set_fill (ctx, title_bar, ctx->style.foreground.color[tb_mode]);
auto title = add_text (ctx, title_bar, state, window->mode);
View_Control (title)->gravity = grav_center;
if (UI_Button (va (0, "X##close_%s", window->name))) {
window->is_open = false;
}
}
#undef IMUI_context
return 0;
}
void
IMUI_EndWindow (imui_ctx_t *ctx)
{
IMUI_PopLayout (ctx);
}