From ba19d468dd1bde49fa04d9b13f7069ddb4dd19f9 Mon Sep 17 00:00:00 2001 From: Emil Kosz Date: Thu, 19 Jun 2025 14:22:00 +0200 Subject: [PATCH] Entry cursor --- game/res/shaders/uicursor.frag | 12 ++ game/res/shaders/uicursor.vert | 16 +++ game/res/textures/font_9x20.png | Bin 2948 -> 2824 bytes game/res/textures/font_9x20.xcf | Bin 29084 -> 29084 bytes game/src/event/evtman.c | 2 +- game/src/resources.c | 1 + game/src/ui/uiman.c | 71 +++++++++-- game/src/ui/widget_composites.c | 6 +- game/src/ui/widget_composites.h | 2 +- game/src/ui/widget_core.c | 52 +++++--- game/src/ui/widget_core.h | 33 ++++-- game/src/ui/widget_ops.c | 48 ++++++-- game/src/ui/widget_ops.h | 22 +++- game/src/ui/widget_render.c | 202 ++++++++++++++++---------------- game/src/ui/widget_render.h | 1 + game/src/ui/widget_style.h | 1 + 16 files changed, 318 insertions(+), 151 deletions(-) create mode 100644 game/res/shaders/uicursor.frag create mode 100644 game/res/shaders/uicursor.vert diff --git a/game/res/shaders/uicursor.frag b/game/res/shaders/uicursor.frag new file mode 100644 index 0000000..cce44d3 --- /dev/null +++ b/game/res/shaders/uicursor.frag @@ -0,0 +1,12 @@ +#version 100 + +precision mediump float; + +uniform vec4 cursor_color; + +varying vec2 f_uv; + +void main () +{ + gl_FragColor = cursor_color; +} diff --git a/game/res/shaders/uicursor.vert b/game/res/shaders/uicursor.vert new file mode 100644 index 0000000..68109e9 --- /dev/null +++ b/game/res/shaders/uicursor.vert @@ -0,0 +1,16 @@ +#version 100 + +attribute vec3 position; +attribute vec2 uv; + +uniform mat3 model; +uniform mat4 projection; + +varying vec2 f_uv; + +void +main () +{ + f_uv = uv; + gl_Position = projection * vec4 (model * vec3 (position.xy, 1.0), 1.0); +} diff --git a/game/res/textures/font_9x20.png b/game/res/textures/font_9x20.png index a005c6765ba3a00ca7336606225a74ef1162c955..3860f5f3fb13b5508264d8d30b54d70ba8ac69a9 100644 GIT binary patch delta 2744 zcmZn>?+}}iQ_t1r>EaktaqI2e+eJ^bcwCcD{r`WuIL>cb3{Quy;)c6>uX#`ElP(r? zYH+Z(XCM*GKREyYLbZ9`C-biSs$aW*{rs<&R?iRolvulrW3kejy<7IYuU}EWZ)@`O zH!a3mo`NeLTl8JDzH)xbo~kVizrHDyIsP?v|I>=eA6iT=cxb1jeTdq)bwk9|`@i2z zzq#jnX0P4TV<>u#B?{&ee9^56Q;zA>L`*7@wYx?H z`tjA+w7$#i{E6G&6m=%GW$}O6w_NIE{uP$;Z29eMS(y{2&YUx|toqF&b+dCmw&~k@ zL-SM9SzUWyOmv78HQO{b!O!4yH1}d1C8LnFSM3xTXGOkV!Tai7RDb%4pR!@@!Ov$V zp4nYDrQYyN|0~OEi?GWtpQ_esC%68+JbP!Pv3BT!MNj+lQd7)+z1cnG>i0mYewjly z+GnE!0;e6_|LpVvnbZNr3a*J zgV*bQHF1OwP7k^rBw8r**$o}N1tG}$N|8?Tkl1B#X?H2Db-xMwX zu`=w^$y3EC)241!lGUtUsj9vB`jwYkj3)lo%YMD+`?`+RuU}`H{u%zuyR|?>hT%qxYV72b6#N)_yOPai6)hI_=oC4F0T4hNrO?i?+o0 zuHjo~aop83d)l*3zVyZVoc zn(tOc_RjdKr#e?Q_*>{K57Bw|G78+nRn{JRv39ddTG?7|(X88l(kC}xAD>%qX>R z*{ypkXVs=Zk(xbolj~{APl@)rz5<24AAa17dLMM@V}{Vy7{P}dO^)?A*kuN8y#8)^ z(f7XkrIu4_=l-%we?PUj)GBhX$*lR3y$kt{NA@MZ`WO2}Hg)L{`@%@OUEik#PCH?` zcXPQ^Do^7y{VOj`os`ozhUH$X~@1<$p zo44+VZaS-3VEba7?GdNN&hRbUl>H-|-+38VK-#nUS={w|9j)K2+kWDASEyl@fpCQS z0!hPDMxqY6%G0iCWlmcaR@;76qod0_KBX}4``pv3TVC+3wffRI@8gI0Gac9NIGPk1 z6&YA&lzw=3+x8^MC~dJTO79~x_pIWsljNPCyX0o&wnesO|BpSoald)-s6)qo_?-AZXC9AN>$Ijw>evnp9yl>Xk@@O zNjg&ZU9G-0*ZuAKoM)B@dw%Z8UBTjhkcbiHe20ZGEaK3bM4yb%VGCLB_)fdC>o`>y#Eq=F6?P!(>CLj z?_RvSC#P+gDp|dF>)-V@<;w-mo^w$;d~nt2~7=cySi1Kf{#Rc7@G3>vwOxU>PlVO=m&*tBbpv z*4}xuZ&&(tC$r*N#+#bf&s%l&%7jzjygzzWoi`6VoAZn*@k!jXHM{4`XLDZz3QPtD z28RbTb|!38GhS3bFQ7BlE{#Ts~}tEuexbo0&by-s4h z&+o4GKD{?BWZLWWqpS0#Rxh}^yYp(vlV%bb+T3ej=KRh3xo#`FK4x%bldDc* ze5!tIdivg96Z`AGy3|h4tzJI8?>I+%<_SAb?z@%Noi~r^{C)fK?5SI4q%8$!{I&f( zS?{6~^WQh6{WtdR>_#B{>- z>5(5J<^IZfmWTU?oc`?TR^?LReZDAsP4(;I_5EwrWxcJJt}hO;-L~|t&GsAH=G}R%#nsjQEl>UmX7Bn7Q#9)1bgfQBpJzRCNzZoo;No+D#9C-gEl$&$Rao`)=6>OPzRq+Gh8jcUukDl)L{I|Eu%4V4>0l$y@)ny?nju zech*~JLXrtXRh4fA0-~SGohWqF=G`!D9EtldaR-h3=EY!m|Id@7r%7q;I`@Ov5Z(C$UXIAUHwhfDHtJ}6*d_S{EPH@xQZI8mw9TM?1xSg@8 zU}af!nnI#Uz@;9or0@vmd0%6eTnx+D`ufd>%a-9PQRfdW3XXnVJ=4}E`_*4Nw=w^GxOFi3SPW{|{?x#O(m3m>ddxMs1;N!5zk(*b_W;@)l<2PM2|JVxQ pl#KiN8(VRO62;<%xJ?lIAb;+NtDoS8C!axzJYD@<);T3K0RR!?3>E+Y literal 2948 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 BFev~4 diff --git a/game/res/textures/font_9x20.xcf b/game/res/textures/font_9x20.xcf index ba88e6201f29d6a5bb5b36b36165b65b46ef073c..3c00cd12f0e79848aac66b72f34764ebc92cf440 100644 GIT binary patch delta 23 fcmbR9m~qZy#tG{fA8uU#G?bZ{fob#fFji>*g*6F- delta 23 fcmbR9m~qZy#tG{f?`~ZGG?bZzfpPQnFji>*g#QVD diff --git a/game/src/event/evtman.c b/game/src/event/evtman.c index 5d39bc0..aab9780 100644 --- a/game/src/event/evtman.c +++ b/game/src/event/evtman.c @@ -73,6 +73,7 @@ evtman_init () evtman_add_handler (2, SDL_EVENT_WINDOW_RESIZED, NULL, NULL, uiman_cb_window_resize, NULL); /* The UI effectively eats most SDL events, as it needs it for it's own calculations. */ + evtman_add_handler (2, SDL_EVENT_KEY_DOWN, evt_pred_key, KEY (SDLK_TAB), uiman_cb_switch_focus, NULL); evtman_add_handler (1, SDL_EVENT_KEY_DOWN, evt_pred_flag, "ui_has_focus", uiman_cb_key_down, NULL); evtman_add_handler (1, SDL_EVENT_KEY_UP, evt_pred_flag, "ui_has_focus", uiman_cb_key_up, NULL); evtman_add_handler (1, SDL_EVENT_MOUSE_MOTION, evt_pred_flag, "ui_has_focus", uiman_cb_mouse_motion, NULL); @@ -89,7 +90,6 @@ evtman_init () evtman_add_handler (0, SDL_EVENT_KEY_UP, evt_pred_key, KEY (SDLK_A), evt_cb_flag_off, "moving_left"); evtman_add_handler (0, SDL_EVENT_KEY_DOWN, evt_pred_key, KEY (SDLK_D), evt_cb_flag_on, "moving_right"); evtman_add_handler (0, SDL_EVENT_KEY_UP, evt_pred_key, KEY (SDLK_D), evt_cb_flag_off, "moving_right"); - evtman_add_handler (0, SDL_EVENT_KEY_DOWN, evt_pred_key, KEY (SDLK_TAB), uiman_cb_switch_focus, NULL); evtman_add_handler (0, SDL_EVENT_MOUSE_MOTION, NULL, NULL, evt_cb_rotate_camera, NULL); evtman_add_handler (0, SDL_EVENT_KEY_DOWN, evt_pred_key, KEY (SDLK_SPACE), evt_cb_flag_on, "flying_up"); diff --git a/game/src/resources.c b/game/src/resources.c index 677ab34..e410743 100644 --- a/game/src/resources.c +++ b/game/src/resources.c @@ -151,6 +151,7 @@ resources_init () RAP ("clouds", RLP ("res/shaders/clouds.vert", "res/shaders/clouds.frag")); RAP ("uiwidget", RLP ("res/shaders/uiwidget.vert", "res/shaders/uiwidget.frag")); RAP ("uitext", RLP ("res/shaders/uitext.vert", "res/shaders/uitext.frag")); + RAP ("uicursor", RLP ("res/shaders/uicursor.vert", "res/shaders/uicursor.frag")); RAT ("terrain0", RLT ("res/textures/dirt.png")); RAT ("terrain1", RLT ("res/textures/drygrass.png")); diff --git a/game/src/ui/uiman.c b/game/src/ui/uiman.c index 1d0c7e5..7ac477d 100644 --- a/game/src/ui/uiman.c +++ b/game/src/ui/uiman.c @@ -15,6 +15,7 @@ static Widget *ui_root; static mat4 projection; static Widget *hovered_widget = NULL; +static Widget *focused_widget = NULL; const char *lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut " @@ -70,6 +71,13 @@ uiman_init () Widget_add_child_weighed (window2, Widget_test_new (100), 1); Widget_add_child_weighed (window2, Widget_test_new (100), 1); + Widget *entry = Widget_entry_new ("Input here"); + entry->style->background_image = RESTEX ("terrain4"); + entry->size[0] = 150; + entry->size[1] = 20; + Widget_add_child_weighed (window2, entry, 1); + Widget_add_child_weighed (window2, Widget_test_new (100), 1); + Widget_add_child (ui_root, window2); } @@ -92,11 +100,11 @@ uiman_free () } static inline gboolean -_uiman_call_on_event (Widget *widget, WidgetEvent event) +_uiman_call_on_event (Widget *widget, SDL_Event *sdl_event, WidgetEvent event) { if (widget && widget->on_event[event]) { - widget->on_event[event] (widget, widget->event_data[event]); + widget->on_event[event] (widget, sdl_event, widget->event_data[event]); return TRUE; } @@ -121,12 +129,29 @@ _uiman_set_hovered (Widget *widget, gboolean state) } } +static inline void +_uiman_set_focused (Widget *widget, gboolean state) +{ + if (widget) + { + widget->focused = state; + } +} + gboolean uiman_cb_switch_focus (SDL_Event *event, void *data) { UNUSED (event, data); flags_switch ("ui_has_focus"); SDL_SetWindowRelativeMouseMode (sdl_window, !flags_get ("ui_has_focus")); + + /* We reset the global states of the UI. */ + SDL_StopTextInput (sdl_window); + _uiman_set_hovered (hovered_widget, FALSE); + hovered_widget = NULL; + _uiman_set_focused (focused_widget, FALSE); + focused_widget = NULL; + return TRUE; } @@ -143,6 +168,19 @@ gboolean uiman_cb_key_down (SDL_Event *event, void *data) { UNUSED (event, data); + + SDL_Scancode sc = event->key.scancode; + if (focused_widget && focused_widget->on_key[sc]) + { + focused_widget->on_key[sc] (focused_widget, event, focused_widget->key_data[sc]); + return TRUE; + } + + if (focused_widget && SDL_TextInputActive (sdl_window)) + { + return TRUE; + } + return FALSE; } @@ -162,15 +200,15 @@ uiman_cb_mouse_motion (SDL_Event *event, void *data) if (new_hover != hovered_widget) { - _uiman_call_on_event (hovered_widget, EVENT_MOUSE_LEAVE); + _uiman_call_on_event (hovered_widget, event, EVENT_MOUSE_LEAVE); _uiman_set_hovered (hovered_widget, FALSE); _uiman_set_clicked (hovered_widget, FALSE); - _uiman_call_on_event (new_hover, EVENT_MOUSE_ENTER); + _uiman_call_on_event (new_hover, event, EVENT_MOUSE_ENTER); _uiman_set_hovered (new_hover, TRUE); hovered_widget = new_hover; } - _uiman_call_on_event (hovered_widget, EVENT_MOUSE_MOVE); + _uiman_call_on_event (hovered_widget, event, EVENT_MOUSE_MOVE); /* This callback always eats the event, because why would the game need mouse motion if the UI has stolen the focus? */ @@ -185,7 +223,17 @@ uiman_cb_button_down (SDL_Event *event, void *data) WidgetEvent e = event->button.button == SDL_BUTTON_LEFT ? EVENT_LEFT_DOWN : EVENT_RIGHT_DOWN; Widget *widget = Widget_find_xy (ui_root, (vec2) { event->button.x, event->button.y }); _uiman_set_clicked (widget, TRUE); - return _uiman_call_on_event (widget, e); + + if (widget != focused_widget) + { + _uiman_set_focused (focused_widget, FALSE); + _uiman_call_on_event (focused_widget, event, EVENT_FOCUS_LOST); + _uiman_set_focused (widget, TRUE); + _uiman_call_on_event (widget, event, EVENT_FOCUS_GAINED); + focused_widget = widget; + } + + return _uiman_call_on_event (widget, event, e); } gboolean @@ -196,7 +244,7 @@ uiman_cb_button_up (SDL_Event *event, void *data) WidgetEvent e = event->button.button == SDL_BUTTON_LEFT ? EVENT_LEFT_UP : EVENT_RIGHT_UP; Widget *widget = Widget_find_xy (ui_root, (vec2) { event->button.x, event->button.y }); _uiman_set_clicked (widget, FALSE); - return _uiman_call_on_event (widget, e); + return _uiman_call_on_event (widget, event, e); } gboolean @@ -206,12 +254,19 @@ uiman_cb_mouse_wheel (SDL_Event *event, void *data) WidgetEvent e = event->wheel.y > 0 ? EVENT_SCROLL_UP : EVENT_SCROLL_DOWN; Widget *widget = Widget_find_xy_event (ui_root, (vec2) { event->wheel.mouse_x, event->wheel.mouse_y }, e); - return _uiman_call_on_event (widget, e); + return _uiman_call_on_event (widget, event, e); } gboolean uiman_cb_text_input (SDL_Event *event, void *data) { UNUSED (event, data); + + if (focused_widget) + { + _uiman_call_on_event (focused_widget, event, EVENT_TEXT_INPUT); + return TRUE; + } + return FALSE; } diff --git a/game/src/ui/widget_composites.c b/game/src/ui/widget_composites.c index 378d15e..29f2801 100644 --- a/game/src/ui/widget_composites.c +++ b/game/src/ui/widget_composites.c @@ -29,7 +29,7 @@ Widget * Widget_label_make_int (const char *fmt, int *ptr, TextAlignment alignment) { Widget *w = Widget_label_new (NULL, alignment); - Widget_set_on_tick (w, uicb_widget_op, WidgetOpData_new (OP_LABEL_SHOW_INT, ptr, fmt)); + Widget_set_on_event (w, EVENT_TICK, uicb_widget_op, WidgetOpData_new (OP_LABEL_SHOW_INT, ptr, fmt)); return w; } @@ -37,7 +37,7 @@ Widget * Widget_label_make_float (const char *fmt, float *ptr, TextAlignment alignment) { Widget *w = Widget_label_new (NULL, alignment); - Widget_set_on_tick (w, uicb_widget_op, WidgetOpData_new (OP_LABEL_SHOW_FLOAT, ptr, fmt)); + Widget_set_on_event (w, EVENT_TICK, uicb_widget_op, WidgetOpData_new (OP_LABEL_SHOW_FLOAT, ptr, fmt)); return w; } @@ -78,7 +78,7 @@ Widget_composite_checkbox_make (const char *label, char *flag) box->icon = ICON_CHECKBOX_UNCHECKED; box->aspect_ratio = 1.0; - Widget_set_on_tick (box, uicb_widget_op, WidgetOpData_new (OP_CHECKBOX_SHOW_FLAG, flag)); + Widget_set_on_event (box, EVENT_TICK, uicb_widget_op, WidgetOpData_new (OP_CHECKBOX_SHOW_FLAG, flag)); Widget_set_on_event (box, EVENT_LEFT_UP, uicb_widget_op, WidgetOpData_new (OP_SWITCH_FLAG, flag)); Widget_add_child_weighed (cont, box, 1); diff --git a/game/src/ui/widget_composites.h b/game/src/ui/widget_composites.h index eea0d12..99c1bb1 100644 --- a/game/src/ui/widget_composites.h +++ b/game/src/ui/widget_composites.h @@ -1,6 +1,6 @@ #pragma once -/* #include "widget_fw.h" */ +#include "widget_fw.h" #include "widget_core.h" #include "widget_ops.h" diff --git a/game/src/ui/widget_core.c b/game/src/ui/widget_core.c index 6a60cd7..5c2c10c 100644 --- a/game/src/ui/widget_core.c +++ b/game/src/ui/widget_core.c @@ -20,6 +20,7 @@ WidgetStyle widget_style_default = .text_color = { 1.0, 1.0, 1.0, 1.0 }, .text_color_hovered = { 1.0, 1.0, 1.0, 1.0 }, .text_color_clicked = { 1.0, 1.0, 1.0, 1.0 }, + .ghost_text_color = { 1.0, 1.0, 1.0, 0.5 }, .icon_color = { 1.0, 1.0, 1.0, 1.0 }, .icon_color_hovered = { 1.0, 1.0, 1.0, 1.0 }, @@ -123,6 +124,22 @@ Widget_multiline_text_new (const char *text, float line_height, TextAlignment al return e; } +Widget * +Widget_entry_new (const char *ghost_text) +{ + Widget *e = ui_el_new_internal (WIDGET_ENTRY); + e->do_render = Widget_render_entry; + e->entry.ghost_text = g_string_new (ghost_text); + e->entry.content = g_string_new (NULL); + Widget_set_on_event (e, EVENT_FOCUS_GAINED, uicb_widget_op, WidgetOpData_new (OP_START_TEXT_INPUT, NULL)); + Widget_set_on_event (e, EVENT_FOCUS_LOST, uicb_widget_op, WidgetOpData_new (OP_STOP_TEXT_INPUT, NULL)); + Widget_set_on_event (e, EVENT_TEXT_INPUT, uicb_widget_op, WidgetOpData_new (OP_ENTRY_INPUT_TEXT, NULL)); + Widget_set_on_key (e, SDL_SCANCODE_BACKSPACE, uicb_widget_op, WidgetOpData_new (OP_ENTRY_DELETE_CHAR, NULL)); + Widget_set_on_key (e, SDL_SCANCODE_LEFT, uicb_widget_op, WidgetOpData_new (OP_ENTRY_MOVE_LEFT, NULL)); + Widget_set_on_key (e, SDL_SCANCODE_RIGHT, uicb_widget_op, WidgetOpData_new (OP_ENTRY_MOVE_RIGHT, NULL)); + return e; +} + Widget * Widget_container_new (gboolean horizontal) { @@ -174,22 +191,23 @@ Widget_add_child_weighed (Widget *parent, Widget *child, float layout_weight) Widget_add_child (parent, child); } -void -Widget_set_on_tick (Widget *widget, - void (*on_tick) (Widget *, void *), void *userdata) -{ - widget->on_tick = on_tick; - widget->tick_data = userdata; -} - void Widget_set_on_event (Widget *widget, WidgetEvent event, - void (*on_event) (Widget *, void *), void *userdata) + void (*on_event) (Widget *, SDL_Event *, void *), + void *userdata) { widget->on_event[event] = on_event; widget->event_data[event] = userdata; } +void +Widget_set_on_key (Widget *widget, SDL_Scancode key, + void (*on_key) (Widget *, SDL_Event *, void *), void *userdata) +{ + widget->on_key[key] = on_key; + widget->key_data[key] = userdata; +} + void Widget_set_style (Widget *widget, WidgetStyle style) { @@ -211,6 +229,13 @@ Widget_label_printf (Widget *widget, const char *format, ...) va_end (args); } +void +Widget_entry_input_text (Widget *widget, const char *text) +{ + g_string_insert (widget->entry.content, widget->entry.cursor_position, text); + widget->entry.cursor_position += strlen (text); +} + void Widget_scrollable_scroll (Widget *widget, vec2 direction) { @@ -288,9 +313,9 @@ Widget_find_xy (Widget *widget, vec2 pos) void Widget_tick (Widget *widget) { - if (widget->on_tick) + if (widget->on_event[EVENT_TICK]) { - widget->on_tick (widget, widget->tick_data); + widget->on_event[EVENT_TICK] (widget, NULL, widget->event_data[EVENT_TICK]); } for (unsigned int i = 0; i < widget->children->len; ++i) @@ -341,11 +366,6 @@ Widget_free (Widget *widget) g_string_free (widget->label.text, TRUE); } - if (widget->on_tick && widget->tick_data) - { - g_free (widget->tick_data); - } - for (int ev = 0; ev < EVENT_COUNT; ++ev) { if (widget->on_event[ev] && widget->event_data[ev]) diff --git a/game/src/ui/widget_core.h b/game/src/ui/widget_core.h index a50a2e7..0dfe346 100644 --- a/game/src/ui/widget_core.h +++ b/game/src/ui/widget_core.h @@ -3,6 +3,7 @@ #include #include +#include "widget_fw.h" #include "widget_render.h" #include "widget_layout.h" #include "widget_style.h" @@ -24,6 +25,7 @@ typedef enum WidgetKind WIDGET_LABEL, WIDGET_SCROLLBAR, WIDGET_PROGRESS, + WIDGET_ENTRY, WIDGET_MULTILINE_TEXT, @@ -48,7 +50,8 @@ typedef enum WidgetIcon typedef enum WidgetEvent { EVENT_NONE = -1, /* Don't use. */ - EVENT_LEFT_DOWN = 0, + EVENT_TICK = 0, + EVENT_LEFT_DOWN, EVENT_LEFT_UP, EVENT_RIGHT_DOWN, EVENT_RIGHT_UP, @@ -57,6 +60,9 @@ typedef enum WidgetEvent EVENT_MOUSE_MOVE, EVENT_SCROLL_UP, EVENT_SCROLL_DOWN, + EVENT_FOCUS_GAINED, + EVENT_FOCUS_LOST, + EVENT_TEXT_INPUT, EVENT_COUNT } WidgetEvent; @@ -97,6 +103,13 @@ typedef struct MultilineTextWidget TextAlignment text_alignment; } MultilineTextWidget; +typedef struct EntryWidget +{ + GString *ghost_text; + GString *content; + float cursor_position; +} EntryWidget; + typedef struct ContainerWidget { gboolean horizontal; @@ -128,6 +141,7 @@ typedef struct Widget ScrollbarWidget scrollbar; ProgressWidget progress; MultilineTextWidget multiline_text; + EntryWidget entry; ContainerWidget container; ScrollableWidget scrollable; @@ -136,7 +150,7 @@ typedef struct Widget gboolean visible; gboolean layout_dirty; - gboolean hovered, clicked; + gboolean hovered, clicked, focused; vec2 position; vec2 size; @@ -149,11 +163,11 @@ typedef struct Widget void (*do_layout) (struct Widget *); void (*do_render) (struct Widget *, mat4, int); - void (*on_tick) (struct Widget *, void *); - void *tick_data; - - void (*on_event[EVENT_COUNT]) (struct Widget *, void *); + void (*on_event[EVENT_COUNT]) (struct Widget *, SDL_Event *sdl_event, void *); void *event_data[EVENT_COUNT]; + + void (*on_key[SDL_SCANCODE_COUNT]) (struct Widget *, SDL_Event *null, void *); + void *key_data[SDL_SCANCODE_COUNT]; } Widget; Widget *Widget_plain_new (); @@ -162,6 +176,7 @@ Widget *Widget_label_new (const char *text, TextAlignment alignment); Widget *Widget_scrollbar_new (Widget *target, gboolean horizontal); Widget *Widget_progress_new (gboolean horizontal, float min, float max, float *ptr); Widget *Widget_multiline_text_new (const char *text, float line_height, TextAlignment alignment); +Widget *Widget_entry_new (const char *ghost_text); Widget *Widget_container_new (gboolean horizontal); Widget *Widget_scrollable_new (); Widget *Widget_flowbox_new (gboolean horizontal); @@ -170,13 +185,15 @@ void Widget_add_child (Widget *parent, Widget *child); void Widget_add_child_weighed (Widget *parent, Widget *child, float layout_weight); -void Widget_set_on_tick (Widget *widget, void (*on_tick) (Widget *, void *), void *userdata); -void Widget_set_on_event (Widget *widget, WidgetEvent event, void (*on_event) (Widget *, void *), void *userdata); +void Widget_set_on_event (Widget *widget, WidgetEvent event, void (*on_event) (Widget *, SDL_Event *, void *), void *userdata); +void Widget_set_on_key (Widget *widget, SDL_Scancode key, void (*on_key) (Widget *, SDL_Event *, void *), void *userdata); void Widget_set_style (Widget *widget, WidgetStyle style); void Widget_label_set_text (Widget *widget, const char *text); void Widget_label_printf (Widget *widget, const char *format, ...); +void Widget_entry_input_text (Widget *widget, const char *text); + void Widget_scrollable_scroll (Widget *widget, vec2 direction); Widget *Widget_find_xy_event (Widget *widget, vec2 pos, WidgetEvent event); diff --git a/game/src/ui/widget_ops.c b/game/src/ui/widget_ops.c index 621f36f..03d2bb9 100644 --- a/game/src/ui/widget_ops.c +++ b/game/src/ui/widget_ops.c @@ -1,5 +1,7 @@ #include "widget_ops.h" +extern SDL_Window *sdl_window; + WidgetOpData * WidgetOpData_new (WidgetOp op, void *ptr, ...) { @@ -31,8 +33,9 @@ WidgetOpData_new (WidgetOp op, void *ptr, ...) } void -uicb_widget_op (Widget *self, void *userdata) +uicb_widget_op (Widget *self, SDL_Event *sdl_event, void *userdata) { + UNUSED (sdl_event); WidgetOpData *data = userdata; switch (data->op) @@ -55,6 +58,34 @@ uicb_widget_op (Widget *self, void *userdata) case OP_SCROLLABLE_SCROLL: Widget_scrollable_scroll ((Widget *) data->ptr, data->scroll_dir); break; + case OP_START_TEXT_INPUT: + SDL_StartTextInput (sdl_window); + break; + case OP_STOP_TEXT_INPUT: + SDL_StopTextInput (sdl_window); + break; + case OP_ENTRY_INPUT_TEXT: + Widget_entry_input_text (self, sdl_event->text.text); + break; + case OP_ENTRY_DELETE_CHAR: + if (self->entry.cursor_position > 0 + && self->entry.content->len > 0) { + g_string_erase (self->entry.content, self->entry.cursor_position - 1, 1); + self->entry.cursor_position--; + } + break; + case OP_ENTRY_MOVE_LEFT: + if (self->entry.cursor_position > 0) + { + self->entry.cursor_position--; + } + break; + case OP_ENTRY_MOVE_RIGHT: + if (self->entry.cursor_position < self->entry.content->len) + { + self->entry.cursor_position++; + } + break; default: break; } @@ -63,14 +94,12 @@ uicb_widget_op (Widget *self, void *userdata) /* Scrollbar boilerplate. */ void -uicb_scrollbar_start_scroll (Widget *self, void *userdata) +uicb_scrollbar_start_scroll (Widget *self, SDL_Event *sdl_event, void *userdata) { UNUSED (userdata); Widget *scr = self->scrollbar.target; - vec2 mouse_pos; - SDL_GetMouseState ((float*) &mouse_pos[0], (float*) &mouse_pos[1]); - + vec2 mouse_pos = { sdl_event->button.x, sdl_event->button.y }; unsigned int idx = self->scrollbar.horizontal ? 0 : 1; float content_size = scr->scrollable.content_size[idx]; @@ -97,7 +126,7 @@ uicb_scrollbar_start_scroll (Widget *self, void *userdata) } void -uicb_scrollbar_scroll (Widget *self, void *userdata) +uicb_scrollbar_scroll (Widget *self, SDL_Event *sdl_event, void *userdata) { UNUSED (userdata); @@ -107,8 +136,7 @@ uicb_scrollbar_scroll (Widget *self, void *userdata) } Widget *scr = self->scrollbar.target; - vec2 mouse_pos; - SDL_GetMouseState((float*) &mouse_pos[0], (float*) &mouse_pos[1]); + vec2 mouse_pos = { sdl_event->motion.x, sdl_event->motion.y }; unsigned int idx = self->scrollbar.horizontal ? 0 : 1; float content_size = scr->scrollable.content_size[idx]; @@ -129,8 +157,8 @@ uicb_scrollbar_scroll (Widget *self, void *userdata) } void -uicb_scrollbar_stop_scroll (Widget *self, void *userdata) +uicb_scrollbar_stop_scroll (Widget *self, SDL_Event *sdl_event, void *userdata) { - UNUSED (userdata); + UNUSED (sdl_event, userdata); self->scrollbar.dragging = FALSE; } diff --git a/game/src/ui/widget_ops.h b/game/src/ui/widget_ops.h index 2519be5..7e041ff 100644 --- a/game/src/ui/widget_ops.h +++ b/game/src/ui/widget_ops.h @@ -1,5 +1,7 @@ #pragma once +#include + #include "widget_fw.h" #include "widget_core.h" @@ -7,10 +9,20 @@ typedef enum WidgetOp { OP_SWITCH_FLAG, OP_MODIFY_INT, + OP_LABEL_SHOW_INT, OP_LABEL_SHOW_FLOAT, + OP_CHECKBOX_SHOW_FLAG, - OP_SCROLLABLE_SCROLL + + OP_SCROLLABLE_SCROLL, + + OP_START_TEXT_INPUT, + OP_STOP_TEXT_INPUT, + OP_ENTRY_INPUT_TEXT, + OP_ENTRY_DELETE_CHAR, + OP_ENTRY_MOVE_LEFT, + OP_ENTRY_MOVE_RIGHT } WidgetOp; typedef struct WidgetOpData @@ -25,8 +37,8 @@ typedef struct WidgetOpData } WidgetOpData; WidgetOpData *WidgetOpData_new (WidgetOp op, void *ptr, ...); -void uicb_widget_op (Widget *self, void *userdata); +void uicb_widget_op (Widget *self, SDL_Event *sdl_event, void *userdata); -void uicb_scrollbar_start_scroll (Widget *self, void *userdata); -void uicb_scrollbar_scroll (Widget *self, void *userdata); -void uicb_scrollbar_stop_scroll (Widget *self, void *userdata); +void uicb_scrollbar_start_scroll (Widget *self, SDL_Event *sdl_event, void *userdata); +void uicb_scrollbar_scroll (Widget *self, SDL_Event *sdl_event, void *userdata); +void uicb_scrollbar_stop_scroll (Widget *self, SDL_Event *sdl_event, void *userdata); diff --git a/game/src/ui/widget_render.c b/game/src/ui/widget_render.c index 2dba062..d9e56c6 100644 --- a/game/src/ui/widget_render.c +++ b/game/src/ui/widget_render.c @@ -15,6 +15,50 @@ _Widget_get_aabb (Widget *self, vec2 aabb[2]) glm_vec2_copy ((vec2) { self->position[0] + self->size[0], self->position[1] + self->size[1] }, aabb[1]); } +static inline void +_Widget_render_text_line(const char *text, TextAlignment alignment, vec2 position, + vec2 size, float max_width, vec4 color, mat4 projection) +{ + program_use (RESPRG ("uitext")); + program_set_mat4(RESPRG ("uitext"), "projection", projection); + program_set_vec4(RESPRG ("uitext"), "color", color); + + int nchars = g_utf8_strlen (text, -1); + float total_w = nchars * size[0]; + + float xoff = 0.0; + + switch (alignment) + { + case TEXT_ALIGN_CENTER: + xoff = (max_width - total_w) * 0.5; + break; + case TEXT_ALIGN_RIGHT: + xoff = max_width - total_w; + break; + case TEXT_ALIGN_LEFT: + default: + xoff = 0.0; + } + + const char *p = text; + while (*p) + { + gunichar ch = g_utf8_get_char (p); + program_set_float (RESPRG ("uitext"), "character", ch); + + mat3 model; + glm_translate2d_make (model, (vec2) { position[0] + xoff, position[1] }); + glm_scale2d (model, size); + program_set_mat3 (RESPRG ("uitext"), "model", model); + + SimpleMesh_render (&quad, FALSE); + + xoff += size[0]; + p = g_utf8_next_char (p); + } +} + void Widget_render_base (Widget *self, mat4 projection, int window_height) { @@ -94,58 +138,15 @@ Widget_render_label (Widget *self, mat4 projection, int window_height) { Widget_render_base (self, projection, window_height); + float char_w = self->size[1] * (9.0f / 20.0f); + vec2 pos = { self->position[0], self->position[1] }; + WidgetStyle *style = self->style; - - program_use (RESPRG ("uitext")); - program_set_mat4 (RESPRG ("uitext"), "projection", projection); texture_use (style->font_bitmap, 0); - - program_set_vec4 (RESPRG ("uitext"), "color", style->text_color); - - if (self->clicked) - { - program_set_vec4 (RESPRG ("uitext"), "color", style->text_color_clicked); - } - else if (self->hovered) - { - program_set_vec4 (RESPRG ("uitext"), "color", style->text_color_hovered); - } - - float char_width = self->size[1] * (9.0 / 20.0); - float total_width = g_utf8_strlen (self->label.text->str, -1) * char_width; - float offset = 0.0; - - switch (self->label.text_alignment) - { - case TEXT_ALIGN_LEFT: - offset = -char_width; - break; - case TEXT_ALIGN_CENTER: - offset = (self->size[0] / 2) - (total_width / 2.0) -char_width; - break; - case TEXT_ALIGN_RIGHT: - offset = self->size[0] - total_width - char_width; - break; - } - - const char *p = self->label.text->str; - while (*p) - { - gunichar ch = g_utf8_get_char (p); - - program_set_float (RESPRG ("uitext"), "character", ch); - - mat3 model; - glm_translate2d_make (model, (vec2) { self->position[0] + offset + char_width, self->position[1] }); - glm_scale2d (model, (vec2) { char_width, self->size[1] }); - offset += char_width; - - program_set_mat3 (RESPRG ("uitext"), "model", model); - - SimpleMesh_render (&quad, FALSE); - - p = g_utf8_next_char (p); - } + _Widget_render_text_line (self->label.text->str, self->label.text_alignment, + pos, (vec2) { char_w, self->size[1] }, self->size[0], + self->clicked ? style->text_color_clicked : self->hovered + ? style->text_color_hovered : style->text_color, projection); } void @@ -212,67 +213,70 @@ Widget_render_progress (Widget *self, mat4 projection, int window_height) void Widget_render_multiline_text (Widget *self, mat4 projection, int window_height) { - Widget_render_base(self, projection, window_height); + Widget_render_base (self, projection, window_height); WidgetStyle *style = self->style; - - program_use (RESPRG ("uitext")); - program_set_mat4 (RESPRG ("uitext"), "projection", projection); texture_use (style->font_bitmap, 0); - program_set_vec4 (RESPRG ("uitext"), "color", style->text_color); float pad = style->layout_padding; - guint lines = self->multiline_text.lines->len; - - if (lines == 0) - { - return; - } - float row_h = self->multiline_text.line_height; float char_w = row_h * (9.0 / 20.0); - float x0 = self->position[0] + pad; - float y0 = self->position[1] + pad; - float usable = self->size[0] - 2 * pad; + vec2 pos0 = { self->position[0] + pad, self->position[1] + pad }; - for (guint i = 0; i < lines; ++i) + for (unsigned int i = 0; i < self->multiline_text.lines->len; ++i) { GString *line = g_array_index (self->multiline_text.lines, GString *, i); - guint nchars = g_utf8_strlen (line->str, -1); - float line_w = nchars * char_w; + vec2 pos = { pos0[0], pos0[1] + i * row_h }; + _Widget_render_text_line(line->str, self->multiline_text.text_alignment, pos, + (vec2) { char_w, row_h }, self->size[0] - 2 * pad, + style->text_color, projection); + } +} + +void +Widget_render_entry (Widget *self, mat4 projection, int window_height) +{ + Widget_render_base (self, projection, window_height); + + WidgetStyle *style = self->style; + texture_use (style->font_bitmap, 0); + + float char_w = self->size[1] * (9.0f / 20.0f); + float cursor_in_px = self->entry.cursor_position * char_w; + /* -char_w because we leave space for the cursor at the end of the + text. */ + float offset = glm_max (0, cursor_in_px - (self->size[0] - char_w)); + + if (!self->focused && self->entry.content->len == 0) + { + _Widget_render_text_line (self->entry.ghost_text->str, TEXT_ALIGN_LEFT, + self->position, (vec2) { char_w, self->size[1] }, self->size[0], + style->ghost_text_color, projection); + } + else + { + vec2 pos = { self->position[0] - offset, self->position[1] }; + _Widget_render_text_line (self->entry.content->str, TEXT_ALIGN_LEFT, + pos, (vec2) { char_w, self->size[1] }, self->size[0], + style->text_color, projection); + } + + if (self->focused) + { + float x = self->position[0] + cursor_in_px - offset; + float y = self->position[1]; + float w = 2.0; + float h = self->size[1]; - float y = y0 + i * row_h; - float x; + mat3 model; + glm_translate2d_make(model, (vec2) { x, y }); + glm_scale2d(model, (vec2) { w, h }); - switch (self->multiline_text.text_alignment) - { - case TEXT_ALIGN_CENTER: - x = x0 + (usable - line_w) * 0.5; - break; - case TEXT_ALIGN_RIGHT: - x = x0 + (usable - line_w); - break; - case TEXT_ALIGN_LEFT: - default: - x = x0; - break; - } + program_use (RESPRG ("uicursor")); + program_set_mat3 (RESPRG ("uicursor"), "model", model); + program_set_mat4 (RESPRG ("uicursor"), "projection", projection); + program_set_vec4 (RESPRG ("uicursor"), "cursor_color", style->text_color); - const char *p = line->str; - while (*p) - { - gunichar ch = g_utf8_get_char (p); - program_set_float (RESPRG ("uitext"), "character", ch); - - mat3 model; - glm_translate2d_make (model, (vec2) { x, y }); - glm_scale2d (model, (vec2) { char_w, row_h }); - program_set_mat3 (RESPRG ("uitext"), "model", model); - - SimpleMesh_render (&quad, FALSE); - - x += char_w; - p = g_utf8_next_char (p); - } + SimpleMesh_render(&quad, FALSE); } } diff --git a/game/src/ui/widget_render.h b/game/src/ui/widget_render.h index a4f2dac..687f8bc 100644 --- a/game/src/ui/widget_render.h +++ b/game/src/ui/widget_render.h @@ -12,3 +12,4 @@ void Widget_render_label (Widget *self, mat4 projection, int window_height); void Widget_render_scrollbar (Widget *self, mat4 projection, int window_height); void Widget_render_progress (Widget *self, mat4 projection, int window_height); void Widget_render_multiline_text (Widget *self, mat4 projection, int window_height); +void Widget_render_entry (Widget *self, mat4 projection, int window_height); diff --git a/game/src/ui/widget_style.h b/game/src/ui/widget_style.h index f329916..f94cbb6 100644 --- a/game/src/ui/widget_style.h +++ b/game/src/ui/widget_style.h @@ -12,6 +12,7 @@ typedef struct WidgetStyle vec4 text_color; vec4 text_color_hovered; vec4 text_color_clicked; + vec4 ghost_text_color; vec4 icon_color; vec4 icon_color_hovered;