From f9c3dc8aba97b7360733446f120e274e8fe46411 Mon Sep 17 00:00:00 2001 From: Emil Kosz Date: Sat, 5 Jul 2025 16:35:50 +0200 Subject: [PATCH] Tooltip rendering --- game/Makefile | 2 + game/src/ui/uiman.c | 49 ++++++++++++++++++++---- game/src/ui/widget_core.c | 78 ++++++++++++++++++++++++++++++++++----- game/src/ui/widget_core.h | 13 +++++-- 4 files changed, 122 insertions(+), 20 deletions(-) diff --git a/game/Makefile b/game/Makefile index 05762dc..cbbc429 100644 --- a/game/Makefile +++ b/game/Makefile @@ -1,3 +1,5 @@ +# To launch with debug messages: G_MESSAGES_DEBUG=all ./wschod + SRC := $(shell find src -name '*.c') OBJ := $(patsubst src/%.c, build/%.o, $(SRC)) DEP := $(OBJ:.o=.d) diff --git a/game/src/ui/uiman.c b/game/src/ui/uiman.c index b96ab02..8e63f7b 100644 --- a/game/src/ui/uiman.c +++ b/game/src/ui/uiman.c @@ -18,14 +18,28 @@ static Widget *hovered_widget = NULL; static Widget *clicked_widget = NULL; static Widget *focused_widget = NULL; +static Widget *tooltip_widget = NULL; +static Widget *tooltip_text = NULL; + +const char *lorem_ipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."; + void uiman_init () { - ui_root = Widget_plain_new (); - VEC2_SETI (ui_root->size, sdl_window_w, sdl_window_h); ui_widget_mesh_init (); glm_ortho (0.0, (float) sdl_window_w, (float) sdl_window_h, 0.0, -1.0, 1.0, projection); + tooltip_widget = Widget_flowbox_new (TRUE); + tooltip_widget->visible = FALSE; + tooltip_widget->size[0] = 200; + tooltip_widget->style->layout_padding = 4; + tooltip_widget->style->background_image = RESTEX ("terrain4"); + tooltip_text = Widget_multiline_text_new (NULL, 20, TEXT_ALIGN_CENTER); + Widget_add_child_flowbox (tooltip_widget, tooltip_text, 1, 0); + + ui_root = Widget_plain_new (); + VEC2_SETI (ui_root->size, sdl_window_w, sdl_window_h); + Widget *debug_menu = Widget_flowbox_new (TRUE); debug_menu->style->layout_padding = 8; debug_menu->style->background_image = RESTEX ("terrain4"); @@ -40,12 +54,16 @@ uiman_init () Widget_add_child_flowbox (debug_menu, Widget_label_make_float ("Y: %.1f", &camera_position[1], TEXT_ALIGN_CENTER), 1./3., 20); Widget_add_child_flowbox (debug_menu, Widget_label_make_float ("Z: %.1f", &camera_position[2], TEXT_ALIGN_RIGHT), 1./3., 20); - Widget_add_child_flowbox (debug_menu, Widget_composite_spinner_make_int ("Render distance: %d", &render_distance), 1, 20); + Widget *spinner = Widget_composite_spinner_make_int ("Render distance: %d", &render_distance); + spinner->tooltip = g_string_new (lorem_ipsum); + Widget_add_child_flowbox (debug_menu, spinner, 1, 20); Widget_add_child_flowbox (debug_menu, Widget_composite_checkbox_make ("Wireframe", "wireframe_enabled"), 1./2., 20); Widget_add_child_flowbox (debug_menu, Widget_composite_checkbox_make ("Flight", "flight_enabled"), 1./2., 20); Widget_add_child (ui_root, debug_menu); + + Widget_dump (ui_root, 0); } void @@ -58,6 +76,7 @@ void uiman_render () { Widget_render (ui_root, projection, sdl_window_h); + Widget_render (tooltip_widget, projection, sdl_window_h); } void @@ -121,6 +140,8 @@ uiman_cb_switch_focus (SDL_Event *event, void *data) _uiman_set_clicked (clicked_widget, FALSE); clicked_widget = NULL; + tooltip_widget->visible = FALSE; + return TRUE; } @@ -164,8 +185,9 @@ gboolean uiman_cb_mouse_motion (SDL_Event *event, void *data) { UNUSED (event, data); - - Widget *new_hover = Widget_find_xy (ui_root, (vec2) { event->motion.x, event->motion.y }); + vec2 pos = { event->motion.x, event->motion.y }; + + Widget *new_hover = Widget_find_xy_cond (ui_root, pos, wcond_any, NULL); if (new_hover != hovered_widget) { @@ -180,6 +202,19 @@ uiman_cb_mouse_motion (SDL_Event *event, void *data) /* Even if it isn't the hovered widget, this is useful for stuff like scrollbars. */ _uiman_call_on_event (clicked_widget, event, EVENT_MOUSE_MOVE); + + Widget *tooltip_source = Widget_find_xy_cond (ui_root, pos, wcond_tooltip, NULL); + + if (tooltip_source && tooltip_source->tooltip && tooltip_source->tooltip->len > 0) + { + Widget_multiline_text_set_text (tooltip_text, tooltip_source->tooltip->str); + tooltip_widget->visible = TRUE; + VEC2_SETI (tooltip_widget->position, pos[0] + 12, pos[1] + 12); + } + else + { + tooltip_widget->visible = FALSE; + } /* This callback always eats the event, because why would the game need mouse motion if the UI has stolen the focus? */ @@ -192,7 +227,7 @@ uiman_cb_button_down (SDL_Event *event, void *data) UNUSED (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 }); + Widget *widget = Widget_find_xy_cond (ui_root, (vec2) { event->button.x, event->button.y }, wcond_any, NULL); _uiman_set_clicked (widget, TRUE); clicked_widget = widget; @@ -227,7 +262,7 @@ uiman_cb_mouse_wheel (SDL_Event *event, void *data) UNUSED (event, 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); + Widget *widget = Widget_find_xy_cond (ui_root, (vec2) { event->wheel.mouse_x, event->wheel.mouse_y }, wcond_event, GINT_TO_POINTER (e)); return _uiman_call_on_event (widget, event, e); } diff --git a/game/src/ui/widget_core.c b/game/src/ui/widget_core.c index ec32eae..2a3272a 100644 --- a/game/src/ui/widget_core.c +++ b/game/src/ui/widget_core.c @@ -236,6 +236,13 @@ Widget_label_printf (Widget *widget, const char *format, ...) va_end (args); } +void +Widget_multiline_text_set_text (Widget *widget, const char *text) +{ + g_string_overwrite (widget->multiline_text.text, 0, text); + widget->layout_dirty = TRUE; +} + void Widget_entry_input_text (Widget *widget, const char *text) { @@ -274,8 +281,29 @@ Widget_hit_test (Widget *widget, vec2 point) && y >= top && y <= bottom); } +gboolean +wcond_tooltip (Widget *widget, void *userdata) +{ + UNUSED (userdata); + return widget->tooltip != NULL; +} + +gboolean +wcond_event (Widget *widget, void *event) +{ + WidgetEvent e = GPOINTER_TO_INT (event); + return widget->on_event[e] != NULL; +} + +gboolean +wcond_any (Widget *widget, void *userdata) +{ + UNUSED (widget, userdata); + return TRUE; +} + Widget * -Widget_find_xy_event (Widget *widget, vec2 pos, WidgetEvent event) +Widget_find_xy_cond (Widget *widget, vec2 pos, gboolean (*cond_func) (Widget *, void *), void *userdata) { if (!widget->visible) { @@ -298,7 +326,7 @@ Widget_find_xy_event (Widget *widget, vec2 pos, WidgetEvent event) for (int i = widget->children->len - 1; i >= 0; --i) { Widget *child = g_array_index (widget->children, Widget *, i); - Widget *found = Widget_find_xy_event (child, pos_scrolled, event); + Widget *found = Widget_find_xy_cond (child, pos_scrolled, cond_func, userdata); if (found != NULL) { @@ -306,18 +334,12 @@ Widget_find_xy_event (Widget *widget, vec2 pos, WidgetEvent event) } } - if (event == EVENT_NONE || widget->on_event[event]) + if (cond_func (widget, userdata)) { return widget; } - return NULL; -} - -Widget * -Widget_find_xy (Widget *widget, vec2 pos) -{ - return Widget_find_xy_event (widget, pos, EVENT_NONE); + return NULL; } void @@ -343,6 +365,14 @@ Widget_render (Widget *widget, mat4 projection, int window_height) widget->do_layout (widget); } + if (!widget->visible) + { + /* If the children need layouting, it doesn't matter, as we're + not visible anyway. They'll layout when the widget becomes + visible again. */ + return; + } + if (widget->do_render) { widget->do_render (widget, projection, window_height); @@ -386,3 +416,31 @@ Widget_free (Widget *widget) g_free (widget); } + +void +Widget_dump (Widget *widget, int depth) +{ + if (!widget) return; + + GString *prefix = g_string_new (NULL); + for (int i = 0; i < depth; ++i) + { + g_string_append (prefix, " "); + } + + g_debug ("%sWidget %p kind=%d visible=%d", prefix->str, widget, widget->kind, widget->visible); + + for (int i = 0; i < EVENT_COUNT; ++i) + { + if (widget->on_event[i]) + { + g_debug ("%s Has handler for event %d", prefix->str, i); + } + } + + for (unsigned int i = 0; i < widget->children->len; ++i) + { + Widget *child = g_array_index (widget->children, Widget *, i); + Widget_dump (child, depth + 1); + } +} diff --git a/game/src/ui/widget_core.h b/game/src/ui/widget_core.h index eee2662..0271d3e 100644 --- a/game/src/ui/widget_core.h +++ b/game/src/ui/widget_core.h @@ -49,7 +49,6 @@ typedef enum WidgetIcon typedef enum WidgetEvent { - EVENT_NONE = -1, /* Don't use. */ EVENT_TICK = 0, EVENT_LEFT_DOWN, EVENT_LEFT_UP, @@ -162,6 +161,8 @@ typedef struct Widget WidgetStyle *style; WidgetIcon icon; + GString *tooltip; + void (*do_layout) (struct Widget *); void (*do_render) (struct Widget *, mat4, int); @@ -196,13 +197,19 @@ 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_multiline_text_set_text (Widget *widget, const char *text); + 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); -Widget *Widget_find_xy (Widget *widget, vec2 point); +gboolean wcond_tooltip (Widget *widget, void *userdata); +gboolean wcond_event (Widget *widget, void *event); +gboolean wcond_any (Widget *widget, void *userdata); +Widget *Widget_find_xy_cond (Widget *widget, vec2 point, gboolean (*cond_func) (Widget *, void *), void *userdata); void Widget_tick (Widget *widget); void Widget_render (Widget *widget, mat4 projection, int window_height); void Widget_free (Widget *widget); + +void Widget_dump (Widget *widget, int depth);