Ui: UI_State; UI_TextAlign :: typedef int; UI_TextAlign_Center :: 0; UI_TextAlign_CenterLeft :: ^; UI_Layout :: struct { rect: R2P; } UI_Widget :: struct { next: *UI_Widget; id: u64; last_touched_frame_index: u64; } UI_Cut :: typedef int; UI_Cut_Left :: 0; UI_Cut_Right :: ^; UI_Cut_Top :: ^; UI_Cut_Bottom :: ^; UI_Style :: struct { cut_size: V2; text_align: UI_TextAlign; scroll: V2; absolute: bool; absolute_rect: R2P; button_background_color: V4; hot_button_background_color: V4; interacting_with_button_background_color: V4; active_button_background_color: V4; text_color: V4; button_border_color: V4; checked_checkbox_button_background_color: V4; draw_text_shadow: bool; } UI_BaseButtonDesc :: struct { text: S8_String; draw_text: bool; draw_border: bool; draw_background: bool; use_checked_checkbox_button_background_colors: bool; } UI_Result :: struct { pressed: bool; interacting_with: bool; hot: bool; drag: V2; } UI_LayoutStack :: struct { data: [256]UI_Layout; len : i32; } UI_State :: struct { cut: UI_Cut; s: UI_Style; // current style base_style: UI_Style; // @todo: add stack id concept cached_widgets: LC_Map; // @todo: add removing widgets at end of frame layouts: UI_LayoutStack; font: *R_Font; hot: u64; interacting_with: u64; id: u64; inside_begin_end_pair: bool; // next cut output text_width: f32; text_size: V2; } UI_AddLayout :: proc(s: *UI_LayoutStack, layout: UI_Layout) { if s.len + 1 > lengthof(s.data) { IO_FatalErrorf("layout stack overflow reached max item count: %d", lengthof(s.data)); } s.data[s.len] = layout; s.len += 1; } UI_GetLastLayout :: proc(s: *UI_LayoutStack): *UI_Layout { if (s.len == 0) IO_FatalErrorf("error, trying to get last layout but there are none"); return &s.data[s.len - 1]; } UI_Begin :: proc() { IO_Assert(Ui.inside_begin_end_pair == false); Ui.inside_begin_end_pair = true; IO_Assert(Ui.layouts.len == 0 || Ui.layouts.len == 1); Ui.layouts.len = 0; UI_AddLayout(&Ui.layouts, {R2P_SizeF(0, 0, :f32(Mu.window.size.x), :f32(Mu.window.size.y))}); UI_SetStyle(); } UI_SetStyle :: proc() { Ui.font = R.font; Ui.base_style = { cut_size = :V2{-1, Ui.font.size + 4}, button_background_color = :V4{0, 0, 0, 1.0}, text_color = :V4{1, 1, 1, 1}, button_border_color = :V4{1, 1, 1, 1}, checked_checkbox_button_background_color = :V4{0.5, 0.5, 0.5, 1.0}, hot_button_background_color = :V4{0.5, 0, 0, 1}, interacting_with_button_background_color = :V4{1.0, 0, 0, 1}, active_button_background_color = :V4{1, 0, 0, 1}, }; Ui.s = Ui.base_style; } UI_End :: proc() { IO_Assert(Ui.inside_begin_end_pair); Ui.inside_begin_end_pair = false; if (Mu.window.mouse.left.unpress) { Ui.interacting_with = NULL; } Ui.hot = NULL; } R_DrawBorder :: proc(r: R2P, color: V4) { r = R2P_Shrink(r, 1); R_Rect2D(R2P_CutLeft(&r, 1), R.atlas.white_texture_bounding_box, color); R_Rect2D(R2P_CutRight(&r, 1), R.atlas.white_texture_bounding_box, color); R_Rect2D(R2P_CutTop(&r, 1), R.atlas.white_texture_bounding_box, color); R_Rect2D(R2P_CutBottom(&r, 1), R.atlas.white_texture_bounding_box, color); } UI_GetNextRect :: proc(text: S8_String): R2P { if (Ui.s.absolute) return Ui.s.absolute_rect; l := UI_GetLastLayout(&Ui.layouts); cut := Ui.s.cut_size; font := Ui.font; if (text.len) { Ui.text_width = R_GetTextSize(text); Ui.text_size = :V2{Ui.text_width, font.ascent}; } if (cut.x < 0) { cut.x = Ui.text_width + 32; if (cut.x < 0) { cut.x = 32; } } if (cut.y < 0) { cut.y = font.ascent; } if (Ui.cut == UI_Cut_Top || Ui.cut == UI_Cut_Bottom) { scroll_y := F32_Clamp(Ui.s.scroll.y, -cut.y, cut.y); cut.y -= scroll_y; Ui.s.scroll.y -= scroll_y; } if (Ui.cut == UI_Cut_Left || Ui.cut == UI_Cut_Right) { scrollx := F32_Clamp(Ui.s.scroll.x, -cut.x, cut.x); cut.x -= scrollx; Ui.s.scroll.x -= scrollx; } if (Ui.cut == UI_Cut_Left) { return R2P_CutLeft(&l.rect, cut.x); } if (Ui.cut == UI_Cut_Right) { return R2P_CutRight(&l.rect, cut.x); } if (Ui.cut == UI_Cut_Top) { return R2P_CutTop(&l.rect, cut.y); } if (Ui.cut == UI_Cut_Bottom) { return R2P_CutBottom(&l.rect, cut.y); } IO_InvalidCodepath(); return :R2P{}; } UI_GetWidget :: proc(text: S8_String): *UI_Widget { if (Ui.cached_widgets.allocator.p == NULL) { Ui.cached_widgets.allocator = Perm.allocator; } hash := HashBytes(text.str, :u64(text.len)); widget: *UI_Widget = LC_MapGetU64(&Ui.cached_widgets, hash); if (!widget) { widget = MA_PushSize(Perm, :usize(sizeof(:UI_Widget))); widget.id = hash; LC_MapInsertU64(&Ui.cached_widgets, hash, widget); } IO_Assert(widget.id == hash); widget.last_touched_frame_index = :u64(Mu.frame); return widget; } UI_BaseButton :: proc(desc: UI_BaseButtonDesc): UI_Result { result: UI_Result; rect := UI_GetNextRect(desc.text); mouse_pos: V2 = {:f32(Mu.window.mouse.pos.x), :f32(Mu.window.mouse.pos.y)}; delta_mouse_pos: V2 = {:f32(Mu.window.mouse.delta_pos.x), :f32(Mu.window.mouse.delta_pos.y)}; if (rect.min.x == rect.max.x || rect.min.y == rect.max.y) { return result; } widget := UI_GetWidget(desc.text); if (R2P_CollidesV2(rect, mouse_pos)) { Ui.hot = widget.id; } if (Ui.hot == widget.id) { result.hot = true; if (Mu.window.mouse.left.press) { Ui.interacting_with = widget.id; } } if (Ui.interacting_with == widget.id) { result.interacting_with = true; result.drag = delta_mouse_pos; if (Ui.hot == widget.id && Mu.window.mouse.left.unpress) { result.pressed = true; } } text_pos := rect.min; rect_size := R2P_GetSize(rect); centered_text_pos := V2_Add(text_pos, V2_DivF(V2_Sub(rect_size, Ui.text_size), 2.0)); if (Ui.s.text_align == UI_TextAlign_Center) { text_pos = centered_text_pos; } if (Ui.s.text_align == UI_TextAlign_CenterLeft) { text_pos.y = centered_text_pos.y; text_pos.x += 4; } button_background_color: V4 = Ui.s.button_background_color; text_color: V4 = Ui.s.text_color; button_border_color: V4 = Ui.s.button_border_color; if (desc.use_checked_checkbox_button_background_colors) { button_background_color = Ui.s.checked_checkbox_button_background_color; } if (Ui.hot == widget.id) { button_background_color = Ui.s.hot_button_background_color; } if (Ui.interacting_with == widget.id) { button_background_color = Ui.s.interacting_with_button_background_color; } if (result.pressed) { button_background_color = Ui.s.active_button_background_color; } if (desc.draw_background) { R_Rect2D(rect, R.atlas.white_texture_bounding_box, button_background_color); } if (desc.draw_border) { R_DrawBorder(rect, button_border_color); } if (desc.draw_text) { if (Ui.s.draw_text_shadow) { R_Text2D({ text = desc.text, color = R_ColorWhite, pos = :V2{text_pos.x + 2, text_pos.y - 2}, do_draw = true, scale = 1.0, }); } R_DrawText(desc.text, text_pos, text_color); } return result; } UI_Button :: proc(str: *char): bool { result := UI_BaseButton({ text = S8_MakeFromChar(str), draw_text = true, draw_border = true, draw_background = true, }); return result.pressed; } UI_Checkbox :: proc(val: *bool, str: *char): bool { result: UI_Result = UI_BaseButton({ text = S8_MakeFromChar(str), draw_text = true, draw_border = true, draw_background = true, use_checked_checkbox_button_background_colors = *val, }); if (result.pressed) { *val = !*val; } return *val; } UI_Fill :: proc() { rect := UI_GetNextRect(S8_MakeEmpty()); R_Rect2D(rect, R.atlas.white_texture_bounding_box, Ui.s.button_background_color); R_DrawBorder(rect, Ui.s.button_border_color); } UI_PopStyle :: proc() { Ui.s = Ui.base_style; } UI_PopLayout :: proc() { if Ui.layouts.len == 0 { IO_FatalError("tryign to pop a layout but layout stack is empty"); } Ui.layouts.len -= 1; } UI_PushLayout :: proc(rect_override: R2P): R2P { rect: R2P; if (rect_override.min.x != 0 || rect_override.min.y != 0 || rect_override.max.x != 0 || rect_override.max.y != 0) { rect = rect_override; } else { rect = UI_GetNextRect(S8_MakeEmpty()); } UI_AddLayout(&Ui.layouts, {rect}); return rect; }