const int U_GrowthRule_Down = 1; const int U_GrowthRule_Up = 2; const int U_SizeRule_Exact = 4; const int U_SizeRule_MatchText = 8; const int U_AlignRule_Left = 16; const int U_AlignRule_Center = 32; const int U_AnimationRule_Off = 64; const int U_LayoutRule_Absolute = 128; struct U_Layout { int rule_flags; Vec2 pos; Vec2 size; Vec2 pos_iter; uint64_t di; }; const int U_Action_Button = 1; const int U_Action_Slider = 2; struct U_Action { int flags; bool pressed; }; struct U_Widget { S8_String string; Vec2 pos; Vec2 size; Vec2 string_pos; U_Action action; float t; float running_t; // @retained float max_t; // @retained U_Layout layout; // @absolute uint64_t di; uint64_t hash; uint64_t last_touched_frame_index; }; enum U_EventKind { U_EventKind_None, U_EventKind_Widget, U_EventKind_PushLayout, U_EventKind_PopLayout, }; struct U_Event { U_EventKind kind; union { U_Widget *widget; U_Layout *layout; }; }; MA_Arena U_Arena; Array U_FreeLayouts; Array U_LayoutStack; Array U_RetainedWidgets; Array U_Events; Array U_WidgetCache; Array U_FreeWidgets; Array U_WidgetsToDraw; U_Widget *U_Hot; U_Widget *U_InteractingWith; uint64_t U_DebugID; // One frame delayed utility variables bool U_InteractionEnded; bool U_ClickedUnpress; bool U_ClickedOutsideUI; bool U_DrawDebug; U_Widget U_NullWidget; U_Widget *U_CreateWidget(S8_String string, int action_flags) { // Hash the string up to '::' so that varying numbers are ok int64_t string_to_hash_len = string.len; for (int64_t i = 0; i < string.len; i += 1) { if (string.str[i] == ':' && string.str[i + 1] == ':') { string_to_hash_len = i; break; } } uint64_t hash = HashBytes(string.str, string_to_hash_len); // string = S8_ReplaceAll(G_Frame, string, S8_Lit("##"), S8_Lit(""), 0); U_Widget *widget = 0; bool found_in_cache = false; For(U_WidgetCache) { if (it->hash == hash) { widget = it; found_in_cache = true; break; } } if (widget && widget->last_touched_frame_index == Mu->frame) { return &U_NullWidget; } if (widget == 0 && U_FreeWidgets.len) widget = U_FreeWidgets.pop(); if (widget == 0) widget = MA_PushStruct(&U_Arena, U_Widget); if (!found_in_cache) { U_WidgetCache.add(widget); *widget = {}; } // @string_lifetime: // I think we dont need to worry about the string lifetime because // it should get updated on every frame // // Will see how this plays out when adding notifications // maybe that should also be called on every frame etc. widget->string = string; widget->hash = hash; widget->last_touched_frame_index = Mu->frame; widget->di = ++U_DebugID; widget->action.flags = action_flags; U_Events.add({U_EventKind_Widget, widget}); return widget; } void U_PushLayout(U_Layout layout) { layout.di = ++U_DebugID; U_Layout *l = 0; if (U_FreeLayouts.len) { l = U_FreeLayouts.pop(); } else { l = MA_PushStruct(&U_Arena, U_Layout); } *l = layout; U_LayoutStack.add(l); auto e = U_Events.alloc(); e->kind = U_EventKind_PushLayout; e->layout = l; } void U_PushLayoutIndent(float indent) { U_Layout **l = U_LayoutStack.last(); U_Layout layout = **l; layout.pos.x += indent; U_PushLayout(layout); } void U_PopLayout() { U_Layout *l = U_LayoutStack.pop(); auto e = U_Events.alloc(); e->kind = U_EventKind_PopLayout; e->layout = l; } bool U_Button(char *str, ...) { S8_FORMAT(G_Frame, str, string_result); U_Widget *w = U_CreateWidget(string_result, U_Action_Button); return w->action.pressed; } bool U_Checkbox(bool *result, char *str, ...) { S8_FORMAT(G_Frame, str, string_result); U_Widget *w = U_CreateWidget(string_result, U_Action_Button); if (w->action.pressed) *result = !*result; return *result; } void U_Popup(Vec2 pos, char *str, ...) { S8_FORMAT(G_Frame, str, string_result); U_Widget *w = U_CreateWidget(string_result, 0); w->layout = {}; w->layout.pos = pos; w->layout.rule_flags = U_LayoutRule_Absolute | U_AlignRule_Center | U_GrowthRule_Down | U_SizeRule_MatchText; } void U_Notification(float max_t, char *str, ...) { S8_FORMAT(G_Frame, str, string_result); U_Widget *w = U_RetainedWidgets.alloc(); w->max_t = max_t; w->string = string_result; w->string.str = (char *)malloc(string_result.len + 1); w->di = ++U_DebugID; MA_MemoryCopy(w->string.str, string_result.str, string_result.len); } Vec2 UI_RectPadding = {32, 8.f}; float UI_DrawBox(float t, Rect2 animated_popup_rect) { float bounce_t = EaseOutElastic(Clamp01(t)); animated_popup_rect = ShrinkByHalfSize(animated_popup_rect, (1 - bounce_t) * CalcSize(animated_popup_rect) / 2); Rect2 popup_rect = ExpandByHalfSize(animated_popup_rect, UI_RectPadding); Rect2 outline_popup_rect = ExpandByHalfSize(popup_rect, {1.f, 1.f}); R2_DrawRectRounded(popup_rect, COLOR_on_hover_rect, 0.1f); return bounce_t; } void U_EndFrame() { R2.font = &R2.font_medium; R2.vertex_list = &R2.ui_vertex_list; IO_Assert(U_LayoutStack.len == 0); // We move all of the "inline" function logic into the event list // and then afterwards we only render those things that got updated // in this frame (which got it's index updated meaning that they dont // get evicted from cache. For(U_Events) { if (it.kind == U_EventKind_PushLayout) { U_LayoutStack.add(it.layout); continue; } else if (it.kind == U_EventKind_PopLayout) { U_LayoutStack.pop(); continue; } else { IO_Assert(it.kind == U_EventKind_Widget); } U_Widget *widget = it.widget; U_Layout *layout = 0; if (U_LayoutStack.len) layout = *U_LayoutStack.last(); if (widget->layout.rule_flags & U_LayoutRule_Absolute) layout = &widget->layout; Vec2 string_size = R2_GetStringSize(widget->string); if (layout->rule_flags & U_SizeRule_Exact) { widget->size = layout->size; } else if (layout->rule_flags & U_SizeRule_MatchText) { widget->size = string_size; } else { IO_InvalidCodepath(); } widget->pos = layout->pos + layout->pos_iter; if (layout->rule_flags & U_GrowthRule_Up) { layout->pos_iter.y += widget->size.y; } else if (layout->rule_flags & U_GrowthRule_Down) { layout->pos_iter.y -= widget->size.y; } else { IO_InvalidCodepath(); } if (layout->rule_flags & U_AnimationRule_Off) { widget->t = 1.0f; } Rect2 string_rect = R2_GetStringRect(widget->pos, widget->string); widget->string_pos = widget->pos; // align font (aligned to baseline) to top left of rectangle widget->string_pos.y = widget->pos.y + (widget->pos.y - string_rect.min.y); if (layout->rule_flags & U_AlignRule_Left) { // widget->string_pos = widget->pos + (widget->size - string_size) / 2; widget->string_pos.y += (widget->size.y - string_size.y) / 2; widget->string_pos.x += 32; } else if (layout->rule_flags & U_AlignRule_Center) { widget->string_pos += (widget->size - string_size) / 2; } else { IO_InvalidCodepath(); } } float retained_y_iter = 0; ForArrayRemovable(U_RetainedWidgets) { ForArrayRemovablePrepare(U_RetainedWidgets); it.t = Clamp01(it.running_t); float completion_rate = it.running_t / it.max_t; if (completion_rate > 1.05) { free(it.string.str); ForArrayRemovableDeclare(); continue; } if (completion_rate > 0.8f) { float eigth = 0.8f * it.max_t; it.t = it.t - (it.running_t - eigth); } it.running_t += Mu->time.deltaf; Vec2 string_size = R2_GetStringSize(it.string); it.size = string_size + UI_RectPadding; it.pos = UI_RectPadding; it.pos.y += retained_y_iter; retained_y_iter += string_size.y + UI_RectPadding.y * 2; // @copy_paste: from layouting, might need to collapse it in the future Rect2 string_rect = R2_GetStringRect(it.pos, it.string); it.string_pos = it.pos; // align font (aligned to baseline) to top left of rectangle it.string_pos.y = it.pos.y + (it.pos.y - string_rect.min.y); // align center it.string_pos += (it.size - string_size) / 2; U_WidgetsToDraw.add(&it); } ForArrayRemovable(U_WidgetCache) { ForArrayRemovablePrepare(U_WidgetCache); if (it->last_touched_frame_index != Mu->frame) { ForArrayRemovableDeclare(); U_FreeWidgets.add(it); } else { U_WidgetsToDraw.add(it); } } U_InteractionEnded = false; U_ClickedUnpress = false; U_ClickedOutsideUI = false; U_Hot = 0; For(U_WidgetsToDraw) { bool activated = false; Rect2 rect = Rect2_Size(it->pos, it->size); if (AreColliding(rect, Mu->window->mouse.posf)) { U_Hot = it; } if (U_Hot == it) { if (Mu->window->mouse.left.press) { U_InteractingWith = it; } } if (U_InteractingWith) { if (Mu->window->mouse.left.unpress) { if (U_Hot == it) { activated = true; } U_InteractionEnded = true; } } if (it->action.flags & U_Action_Button) { it->action.pressed = false; if (activated) it->action.pressed = true; } Vec4 text_color = COLOR_text; if (it->action.flags & U_Action_Button) { if (U_Hot == it) text_color = COLOR_highlighted_text; if (U_InteractingWith == it) text_color = {1, 0, 0, 1}; if (activated) text_color = {0, 1, 0, 1}; } text_color.a = UI_DrawBox(it->t, rect); R2_DrawString(it->string_pos, it->string, text_color); it->t += Mu->time.deltaf; } if (Mu->window->mouse.left.unpress) { U_ClickedUnpress = true; U_InteractingWith = 0; } if (Mu->window->mouse.left.press && U_InteractingWith == 0) { U_ClickedOutsideUI = true; } if (U_DrawDebug) { R2_DebugString("FreeWidgets: %d", U_FreeWidgets.len); R2_DebugString("CachedWidgets: %d", U_WidgetCache.len); R2_DebugString("Events: %d", U_Events.len); R2_DebugString("FreeLayouts: %d", U_FreeLayouts.len); R2_DebugString("RetainedElements: %d", U_RetainedWidgets.len); } For(U_Events) { if (it.kind == U_EventKind_PushLayout) { U_FreeLayouts.add(it.layout); } } U_Events.reset(); U_WidgetsToDraw.reset(); R2.vertex_list = &R2.base_vertex_list; }