mirror of
https://github.com/kichikuou/xsystem35-sdl2.git
synced 2026-09-22 22:48:08 +03:00
microui: Add keyboard navigation
ctx->focus is a transient "pressed with the mouse" state, so it cannot serve as a keyboard focus. Add a persistent ctx->kb_focus alongside it. Controls enter the focus cycle from mu_update_control(), which every widget already calls, so this needs no per-widget code. Tab and Down move the focus forward, Shift+Tab and Up backward, Enter or Space activates the focused control, and Left/Right adjust a focused slider. The focused control is outlined with the new MU_COLOR_FOCUSRING; window chrome is excluded via MU_OPT_NOKBNAV.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
- The popup menu and dialogs can now be operated with the keyboard.
|
||||
|
||||
## 2.19.1 - 2026-08-02
|
||||
- Fixed an issue that prevented moving upward during battles in Rance 4.
|
||||
|
||||
|
||||
+1
-1
@@ -453,7 +453,7 @@ static bool inputnumber_build(mu_Context *ctx, modal *modal) {
|
||||
mu_Id id = mu_get_id(ctx, "value", 5);
|
||||
mu_Rect box = mu_layout_next(ctx);
|
||||
if (st->focus_init) {
|
||||
mu_set_focus(ctx, id);
|
||||
mu_set_kb_focus(ctx, id);
|
||||
st->focus_init = false;
|
||||
}
|
||||
int res = mu_textbox_raw(ctx, st->buf, sizeof(st->buf), id, box, 0);
|
||||
|
||||
+76
-8
@@ -68,7 +68,8 @@ static mu_Style default_style = {
|
||||
{ 35, 35, 35, 255 }, /* MU_COLOR_BASEHOVER */
|
||||
{ 40, 40, 40, 255 }, /* MU_COLOR_BASEFOCUS */
|
||||
{ 43, 43, 43, 255 }, /* MU_COLOR_SCROLLBASE */
|
||||
{ 30, 30, 30, 255 } /* MU_COLOR_SCROLLTHUMB */
|
||||
{ 30, 30, 30, 255 }, /* MU_COLOR_SCROLLTHUMB */
|
||||
{ 120, 170, 255, 255 } /* MU_COLOR_FOCUSRING */
|
||||
}
|
||||
};
|
||||
|
||||
@@ -144,6 +145,10 @@ void mu_begin(mu_Context *ctx) {
|
||||
ctx->next_hover_root = NULL;
|
||||
ctx->mouse_delta.x = ctx->mouse_pos.x - ctx->last_mouse_pos.x;
|
||||
ctx->mouse_delta.y = ctx->mouse_pos.y - ctx->last_mouse_pos.y;
|
||||
/* restart the keyboard-navigation scan */
|
||||
ctx->nav_first = ctx->nav_last = 0;
|
||||
ctx->nav_prev = ctx->nav_next = ctx->nav_cursor = 0;
|
||||
ctx->kb_focus_seen = 0;
|
||||
ctx->frame++;
|
||||
}
|
||||
|
||||
@@ -171,6 +176,20 @@ void mu_end(mu_Context *ctx) {
|
||||
if (!ctx->updated_focus) { ctx->focus = 0; }
|
||||
ctx->updated_focus = 0;
|
||||
|
||||
/* move the keyboard focus. The new focus takes effect on the next frame,
|
||||
** which is fine for a UI that is rebuilt every frame. */
|
||||
if (!ctx->kb_focus_seen) { ctx->kb_focus = 0; }
|
||||
if (ctx->key_pressed & (MU_KEY_TAB | MU_KEY_UP | MU_KEY_DOWN)) {
|
||||
int back = (ctx->key_pressed & MU_KEY_UP) ||
|
||||
((ctx->key_pressed & MU_KEY_TAB) && (ctx->key_down & MU_KEY_SHIFT));
|
||||
if (!ctx->kb_focus) {
|
||||
ctx->kb_focus = back ? ctx->nav_last : ctx->nav_first;
|
||||
} else {
|
||||
ctx->kb_focus = back ? (ctx->nav_prev ? ctx->nav_prev : ctx->nav_last)
|
||||
: (ctx->nav_next ? ctx->nav_next : ctx->nav_first);
|
||||
}
|
||||
}
|
||||
|
||||
/* bring hover root to front if mouse was pressed */
|
||||
if (ctx->mouse_pressed && ctx->next_hover_root &&
|
||||
ctx->next_hover_root->zindex < ctx->last_zindex &&
|
||||
@@ -216,6 +235,12 @@ void mu_set_focus(mu_Context *ctx, mu_Id id) {
|
||||
}
|
||||
|
||||
|
||||
void mu_set_kb_focus(mu_Context *ctx, mu_Id id) {
|
||||
ctx->kb_focus = id;
|
||||
ctx->kb_focus_seen = 1;
|
||||
}
|
||||
|
||||
|
||||
/* 32bit fnv-1a hash */
|
||||
#define HASH_INITIAL 2166136261
|
||||
|
||||
@@ -652,6 +677,17 @@ void mu_draw_control_frame(mu_Context *ctx, mu_Id id, mu_Rect rect,
|
||||
if (opt & MU_OPT_NOFRAME) { return; }
|
||||
colorid += (ctx->focus == id) ? 2 : (ctx->hover == id) ? 1 : 0;
|
||||
ctx->draw_frame(ctx, rect, colorid);
|
||||
if (ctx->kb_focus == id) {
|
||||
mu_draw_box(ctx, expand_rect(rect, 1), ctx->style->colors[MU_COLOR_FOCUSRING]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static int control_activated(mu_Context *ctx, mu_Id id) {
|
||||
if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) { return 1; }
|
||||
if (ctx->kb_focus == id &&
|
||||
ctx->key_pressed & (MU_KEY_RETURN | MU_KEY_SPACE)) { return 1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -689,6 +725,22 @@ void mu_update_control(mu_Context *ctx, mu_Id id, mu_Rect rect, int opt) {
|
||||
if (opt & MU_OPT_NOINTERACT) { return; }
|
||||
if (mouseover && !ctx->mouse_down) { ctx->hover = id; }
|
||||
|
||||
/* record this control in the keyboard focus cycle. Controls are visited in
|
||||
** submission order, so tracking the previous one is enough to know both
|
||||
** neighbours of kb_focus. */
|
||||
if (~opt & MU_OPT_NOKBNAV && mu_check_clip(ctx, rect) != MU_CLIP_ALL) {
|
||||
if (!ctx->nav_first) { ctx->nav_first = id; }
|
||||
ctx->nav_last = id;
|
||||
if (id == ctx->kb_focus) {
|
||||
ctx->kb_focus_seen = 1;
|
||||
ctx->nav_prev = ctx->nav_cursor;
|
||||
} else if (ctx->kb_focus && !ctx->nav_next &&
|
||||
ctx->nav_cursor == ctx->kb_focus) {
|
||||
ctx->nav_next = id;
|
||||
}
|
||||
ctx->nav_cursor = id;
|
||||
}
|
||||
|
||||
if (ctx->focus == id) {
|
||||
if (ctx->mouse_pressed && !mouseover) { mu_set_focus(ctx, 0); }
|
||||
if (!ctx->mouse_down && ~opt & MU_OPT_HOLDFOCUS) { mu_set_focus(ctx, 0); }
|
||||
@@ -697,6 +749,8 @@ void mu_update_control(mu_Context *ctx, mu_Id id, mu_Rect rect, int opt) {
|
||||
if (ctx->hover == id) {
|
||||
if (ctx->mouse_pressed) {
|
||||
mu_set_focus(ctx, id);
|
||||
ctx->kb_focus = id; /* clicking moves the keyboard focus too */
|
||||
ctx->kb_focus_seen = 1;
|
||||
} else if (!mouseover) {
|
||||
ctx->hover = 0;
|
||||
}
|
||||
@@ -742,7 +796,7 @@ int mu_button_ex(mu_Context *ctx, const char *label, int icon, int opt) {
|
||||
mu_Rect r = mu_layout_next(ctx);
|
||||
mu_update_control(ctx, id, r, opt);
|
||||
/* handle click */
|
||||
if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) {
|
||||
if (control_activated(ctx, id)) {
|
||||
res |= MU_RES_SUBMIT;
|
||||
}
|
||||
/* draw */
|
||||
@@ -760,7 +814,7 @@ int mu_checkbox(mu_Context *ctx, const char *label, int *state) {
|
||||
mu_Rect box = mu_rect(r.x, r.y, r.h, r.h);
|
||||
mu_update_control(ctx, id, r, 0);
|
||||
/* handle click */
|
||||
if (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id) {
|
||||
if (control_activated(ctx, id)) {
|
||||
res |= MU_RES_CHANGE;
|
||||
*state = !*state;
|
||||
}
|
||||
@@ -779,6 +833,11 @@ int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, mu_Id id, mu_Rect r,
|
||||
int opt)
|
||||
{
|
||||
int res = 0;
|
||||
/* the keyboard focus decides which textbox is editable, so that Tab moves the
|
||||
** caret out of a MU_OPT_HOLDFOCUS textbox. A click reaches this through
|
||||
** mu_update_control(), which also sets kb_focus. */
|
||||
if (ctx->kb_focus == id) { mu_set_focus(ctx, id); }
|
||||
else if (ctx->focus == id) { mu_set_focus(ctx, 0); }
|
||||
mu_update_control(ctx, id, r, opt | MU_OPT_HOLDFOCUS);
|
||||
|
||||
if (ctx->focus == id) {
|
||||
@@ -878,6 +937,12 @@ int mu_slider_ex(mu_Context *ctx, mu_Real *value, mu_Real low, mu_Real high,
|
||||
v = low + (ctx->mouse_pos.x - base.x) * (high - low) / base.w;
|
||||
if (step) { v = ((long long)((v + step / 2) / step)) * step; }
|
||||
}
|
||||
/* left/right are not navigation keys, so they need no arbitration with the
|
||||
** focus movement in mu_end() */
|
||||
if (ctx->kb_focus == id && ctx->key_pressed & (MU_KEY_LEFT | MU_KEY_RIGHT)) {
|
||||
mu_Real d = step ? step : (high - low) / 100;
|
||||
v += (ctx->key_pressed & MU_KEY_RIGHT) ? d : -d;
|
||||
}
|
||||
/* clamp and store value, update res */
|
||||
*value = v = mu_clamp(v, low, high);
|
||||
if (last != v) { res |= MU_RES_CHANGE; }
|
||||
@@ -916,6 +981,9 @@ int mu_number_ex(mu_Context *ctx, mu_Real *value, mu_Real step,
|
||||
if (ctx->focus == id && ctx->mouse_down == MU_MOUSE_LEFT) {
|
||||
*value += ctx->mouse_delta.x * step;
|
||||
}
|
||||
if (ctx->kb_focus == id && ctx->key_pressed & (MU_KEY_LEFT | MU_KEY_RIGHT)) {
|
||||
*value += (ctx->key_pressed & MU_KEY_RIGHT) ? step : -step;
|
||||
}
|
||||
/* set flag if value changed */
|
||||
if (*value != last) { res |= MU_RES_CHANGE; }
|
||||
|
||||
@@ -943,7 +1011,7 @@ static int header(mu_Context *ctx, const char *label, int istreenode, int opt) {
|
||||
mu_update_control(ctx, id, r, 0);
|
||||
|
||||
/* handle click */
|
||||
active ^= (ctx->mouse_pressed == MU_MOUSE_LEFT && ctx->focus == id);
|
||||
active ^= control_activated(ctx, id);
|
||||
|
||||
/* update pool ref */
|
||||
if (idx >= 0) {
|
||||
@@ -1006,7 +1074,7 @@ void mu_end_treenode(mu_Context *ctx) {
|
||||
base.w = ctx->style->scrollbar_size; \
|
||||
\
|
||||
/* handle input */ \
|
||||
mu_update_control(ctx, id, base, 0); \
|
||||
mu_update_control(ctx, id, base, MU_OPT_NOKBNAV); \
|
||||
if (ctx->focus == id && ctx->mouse_down == MU_MOUSE_LEFT) { \
|
||||
cnt->scroll.y += ctx->mouse_delta.y * cs.y / base.h; \
|
||||
} \
|
||||
@@ -1111,7 +1179,7 @@ int mu_begin_window_ex(mu_Context *ctx, const char *title, mu_Rect rect, int opt
|
||||
/* do title text */
|
||||
if (~opt & MU_OPT_NOTITLE) {
|
||||
mu_Id id = mu_get_id(ctx, "!title", 6);
|
||||
mu_update_control(ctx, id, tr, opt);
|
||||
mu_update_control(ctx, id, tr, opt | MU_OPT_NOKBNAV);
|
||||
mu_draw_control_text(ctx, title, tr, MU_COLOR_TITLETEXT, opt);
|
||||
if (id == ctx->focus && ctx->mouse_down == MU_MOUSE_LEFT) {
|
||||
cnt->rect.x += ctx->mouse_delta.x;
|
||||
@@ -1127,7 +1195,7 @@ int mu_begin_window_ex(mu_Context *ctx, const char *title, mu_Rect rect, int opt
|
||||
mu_Rect r = mu_rect(tr.x + tr.w - tr.h, tr.y, tr.h, tr.h);
|
||||
tr.w -= r.w;
|
||||
mu_draw_icon(ctx, MU_ICON_CLOSE, r, ctx->style->colors[MU_COLOR_TITLETEXT]);
|
||||
mu_update_control(ctx, id, r, opt);
|
||||
mu_update_control(ctx, id, r, opt | MU_OPT_NOKBNAV);
|
||||
if (ctx->mouse_pressed == MU_MOUSE_LEFT && id == ctx->focus) {
|
||||
cnt->open = 0;
|
||||
}
|
||||
@@ -1141,7 +1209,7 @@ int mu_begin_window_ex(mu_Context *ctx, const char *title, mu_Rect rect, int opt
|
||||
int sz = ctx->style->title_height;
|
||||
mu_Id id = mu_get_id(ctx, "!resize", 7);
|
||||
mu_Rect r = mu_rect(rect.x + rect.w - sz, rect.y + rect.h - sz, sz, sz);
|
||||
mu_update_control(ctx, id, r, opt);
|
||||
mu_update_control(ctx, id, r, opt | MU_OPT_NOKBNAV);
|
||||
if (id == ctx->focus && ctx->mouse_down == MU_MOUSE_LEFT) {
|
||||
cnt->rect.w = mu_max(96, cnt->rect.w + ctx->mouse_delta.x);
|
||||
cnt->rect.h = mu_max(64, cnt->rect.h + ctx->mouse_delta.y);
|
||||
|
||||
+22
-2
@@ -58,6 +58,7 @@ enum {
|
||||
MU_COLOR_BASEFOCUS,
|
||||
MU_COLOR_SCROLLBASE,
|
||||
MU_COLOR_SCROLLTHUMB,
|
||||
MU_COLOR_FOCUSRING,
|
||||
MU_COLOR_MAX
|
||||
};
|
||||
|
||||
@@ -88,7 +89,8 @@ enum {
|
||||
MU_OPT_AUTOSIZE = (1 << 9),
|
||||
MU_OPT_POPUP = (1 << 10),
|
||||
MU_OPT_CLOSED = (1 << 11),
|
||||
MU_OPT_EXPANDED = (1 << 12)
|
||||
MU_OPT_EXPANDED = (1 << 12),
|
||||
MU_OPT_NOKBNAV = (1 << 13) /* exclude from the keyboard focus cycle */
|
||||
};
|
||||
|
||||
enum {
|
||||
@@ -102,7 +104,13 @@ enum {
|
||||
MU_KEY_CTRL = (1 << 1),
|
||||
MU_KEY_ALT = (1 << 2),
|
||||
MU_KEY_BACKSPACE = (1 << 3),
|
||||
MU_KEY_RETURN = (1 << 4)
|
||||
MU_KEY_RETURN = (1 << 4),
|
||||
MU_KEY_TAB = (1 << 5),
|
||||
MU_KEY_UP = (1 << 6),
|
||||
MU_KEY_DOWN = (1 << 7),
|
||||
MU_KEY_LEFT = (1 << 8),
|
||||
MU_KEY_RIGHT = (1 << 9),
|
||||
MU_KEY_SPACE = (1 << 10)
|
||||
};
|
||||
|
||||
|
||||
@@ -179,6 +187,17 @@ struct mu_Context {
|
||||
mu_Style *style;
|
||||
mu_Id hover;
|
||||
mu_Id focus;
|
||||
/* Unlike `focus`, which only lives for as long as a mouse button is held,
|
||||
** `kb_focus` persists across frames and is moved by Tab / arrow keys. The
|
||||
** nav_* fields are rebuilt every frame as mu_update_control() walks the
|
||||
** controls in submission order. */
|
||||
mu_Id kb_focus;
|
||||
mu_Id nav_first; /* first navigable control of this frame */
|
||||
mu_Id nav_last; /* last navigable control of this frame */
|
||||
mu_Id nav_prev; /* control registered just before kb_focus */
|
||||
mu_Id nav_next; /* control registered just after kb_focus */
|
||||
mu_Id nav_cursor; /* scan state: last registered control */
|
||||
int kb_focus_seen; /* kb_focus was registered this frame */
|
||||
mu_Id last_id;
|
||||
mu_Rect last_rect;
|
||||
int last_zindex;
|
||||
@@ -221,6 +240,7 @@ void mu_init(mu_Context *ctx);
|
||||
void mu_begin(mu_Context *ctx);
|
||||
void mu_end(mu_Context *ctx);
|
||||
void mu_set_focus(mu_Context *ctx, mu_Id id);
|
||||
void mu_set_kb_focus(mu_Context *ctx, mu_Id id);
|
||||
mu_Id mu_get_id(mu_Context *ctx, const void *data, int size);
|
||||
void mu_push_id(mu_Context *ctx, const void *data, int size);
|
||||
void mu_pop_id(mu_Context *ctx);
|
||||
|
||||
+12
-5
@@ -89,6 +89,7 @@ static void set_light_style(mu_Style *style) {
|
||||
[MU_COLOR_BASEFOCUS] = { 235, 235, 242, 255 },
|
||||
[MU_COLOR_SCROLLBASE] = { 200, 200, 200, 255 },
|
||||
[MU_COLOR_SCROLLTHUMB] = { 170, 170, 170, 255 },
|
||||
[MU_COLOR_FOCUSRING] = { 40, 90, 200, 255 },
|
||||
};
|
||||
memcpy(style->colors, light, sizeof(light));
|
||||
}
|
||||
@@ -164,11 +165,17 @@ bool modal_default_handler(const SDL_Event *e, modal *modal) {
|
||||
modal->cancelled = true;
|
||||
int c = 0;
|
||||
switch (e->key.keysym.sym) {
|
||||
case SDLK_LSHIFT: case SDLK_RSHIFT: c = MU_KEY_SHIFT; break;
|
||||
case SDLK_LCTRL: case SDLK_RCTRL: c = MU_KEY_CTRL; break;
|
||||
case SDLK_LALT: case SDLK_RALT: c = MU_KEY_ALT; break;
|
||||
case SDLK_RETURN: c = MU_KEY_RETURN; break;
|
||||
case SDLK_BACKSPACE: c = MU_KEY_BACKSPACE; break;
|
||||
case SDLK_LSHIFT: case SDLK_RSHIFT: c = MU_KEY_SHIFT; break;
|
||||
case SDLK_LCTRL: case SDLK_RCTRL: c = MU_KEY_CTRL; break;
|
||||
case SDLK_LALT: case SDLK_RALT: c = MU_KEY_ALT; break;
|
||||
case SDLK_RETURN: case SDLK_KP_ENTER: c = MU_KEY_RETURN; break;
|
||||
case SDLK_BACKSPACE: c = MU_KEY_BACKSPACE; break;
|
||||
case SDLK_TAB: c = MU_KEY_TAB; break;
|
||||
case SDLK_UP: c = MU_KEY_UP; break;
|
||||
case SDLK_DOWN: c = MU_KEY_DOWN; break;
|
||||
case SDLK_LEFT: c = MU_KEY_LEFT; break;
|
||||
case SDLK_RIGHT: c = MU_KEY_RIGHT; break;
|
||||
case SDLK_SPACE: c = MU_KEY_SPACE; break;
|
||||
}
|
||||
if (!c)
|
||||
break;
|
||||
|
||||
Reference in New Issue
Block a user