From 0655ade456b8bfff48b1526c6528a2b7c51dee42 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sun, 30 Jun 2024 13:37:24 -0400 Subject: [PATCH] Support for rounded rectangles & borders (not the best implementation...) --- code/sectr/app/screen.odin | 19 +-- code/sectr/app/ui_theme.odin | 20 ++-- code/sectr/colors.odin | 2 +- code/sectr/engine/client_api.odin | 2 +- code/sectr/engine/render.odin | 186 ++++++++++++++++++++++++++---- code/sectr/ui/core/base.odin | 8 +- code/sectr/ui/core/style.odin | 5 +- code/sectr/ui/widgets.odin | 2 +- 8 files changed, 195 insertions(+), 49 deletions(-) diff --git a/code/sectr/app/screen.odin b/code/sectr/app/screen.odin index 3e56cfe..3d31ed6 100644 --- a/code/sectr/app/screen.odin +++ b/code/sectr/app/screen.odin @@ -182,7 +182,7 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b using title layout.anchor.ratio.x = 1.0 layout.margins = { 0, 0, 15, 0} - layout.font_size = 16 + layout.font_size = 14 } scope(theme_window_bar_btn) @@ -200,6 +200,7 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b text = str_intern("close") if close_btn.hot do style.bg_color = app_color.window_btn_close_bg_hot if close_btn.pressed do settings_menu.is_open = false + style.corner_radii = { 0, 0, 0, 0 } } } if frame_bar.active { @@ -208,13 +209,15 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b } } - if ui_drop_down( & cfg_drop_down, "settings_menu.config", str_intern("App Config"), vb_compute_layout = true).is_open + app_config := ui_drop_down( & cfg_drop_down, "settings_menu.config", str_intern("App Config"), vb_compute_layout = true) + app_config.title.layout.font_size = 12 + if app_config.is_open { Engien_Refresh_Hz: { scope(theme_table_row(is_even = false)) hb := ui_hbox(.Left_To_Right, "settings_menu.engine_refresh_hz.hb"); { using hb - layout.size.min = {0, 30} + layout.size.min = {0, 25} layout.flags = {.Fixed_Height} layout.padding = to_ui_layout_side(4) } @@ -225,7 +228,7 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b using title layout.anchor.ratio.x = 1.0 layout.margins.left = 10 - layout.text_alignment = {0, 0.0} + title.layout.font_size = 12 } input_box := ui_widget("settings_menu.engine_refresh.input_box", {.Mouse_Clickable, .Focusable, .Click_To_Focus}); { @@ -234,7 +237,7 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b layout.margins.left = 5 layout.padding.right = 5 layout.size.min.x = 80 - style.corner_radii[0] = 0.35 + style.corner_radii = { 3, 3, 3, 3 } if input_box.active do style.bg_color = app_color.input_box_bg_active else if input_box.hot do style.bg_color = app_color.input_box_bg_hot @@ -287,7 +290,7 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b scope( theme_table_row(is_even = true)) hb := ui_hbox(.Left_To_Right, "settings_menu.cam_min_zoom.hb"); { using hb - layout.size.min = {0, 30} + layout.size.min = {0, 25} layout.flags = {.Fixed_Height} layout.padding = to_ui_layout_side(4) } @@ -296,6 +299,7 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b using title layout.anchor.ratio.x = 1.0 layout.margins.left = 10 + layout.font_size = 12 } } @@ -304,7 +308,7 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b scope( theme_table_row(is_even = false)) hb := ui_hbox(.Left_To_Right, "settings_menu.cam_max_zoom.hb"); { using hb - layout.size.min = {0, 30} + layout.size.min = {0, 25} layout.flags = {.Fixed_Height} layout.padding = to_ui_layout_side(4) } @@ -313,6 +317,7 @@ ui_screen_settings_menu :: proc( captures : rawptr = nil ) -> ( should_raise : b using title layout.anchor.ratio.x = 1.0 layout.margins.left = 10 + layout.font_size = 12 } } } diff --git a/code/sectr/app/ui_theme.odin b/code/sectr/app/ui_theme.odin index 61bbbf2..183b3bc 100644 --- a/code/sectr/app/ui_theme.odin +++ b/code/sectr/app/ui_theme.odin @@ -71,12 +71,12 @@ theme_drop_down_btn :: proc() -> UI_Theme anchor = range2({0, 0},{}), alignment = {0, 0}, text_alignment = {0.5, 0.5}, - font_size = 14, + font_size = 12, margins = {0, 0, 0, 0}, padding = {0, 0, 0, 0}, border_width = 1, pos = {0, 0}, - size = range2({0,20},{}) + size = range2({0,25},{}) } style := UI_Style { bg_color = app_color.btn_bg_default, @@ -112,8 +112,8 @@ theme_drop_down_btn :: proc() -> UI_Theme theme_table_row :: proc( is_even : bool ) -> UI_Theme { @static theme : UI_Theme - @static loaded : b32 = false - if ! loaded || true + // @static loaded : b32 = false + // if ! loaded || true { app_color := app_color_theme() table_bg : RGBA8 @@ -128,7 +128,7 @@ theme_table_row :: proc( is_even : bool ) -> UI_Theme anchor = range2({},{}), alignment = {0, 0}, text_alignment = {0.5, 0.0}, - font_size = 16, + font_size = 10, margins = {0, 0, 0, 0}, padding = {0, 0, 0, 0}, border_width = 0, @@ -159,7 +159,7 @@ theme_table_row :: proc( is_even : bool ) -> UI_Theme using style_combo.active } theme = UI_Theme { layout_combo, style_combo } - loaded = true + // loaded = true } return theme } @@ -186,7 +186,7 @@ theme_window_bar :: proc() -> UI_Theme style := UI_Style { bg_color = app_color.window_bar_bg, border_color = Color_Transparent, - corner_radii = {}, + corner_radii = {0, 0, 0, 0 }, blur_size = 0, font = get_state().default_font, text_color = app_color.text_default, @@ -238,7 +238,7 @@ theme_window_bar_title :: proc() -> UI_Theme style := UI_Style { bg_color = Color_Transparent, border_color = Color_Transparent, - corner_radii = {}, + corner_radii = {0, 0, 0, 0}, blur_size = 0, font = get_state().default_font, text_color = app_color.text_default, @@ -335,7 +335,7 @@ theme_window_panel :: proc() -> UI_Theme style := UI_Style { bg_color = app_color.window_panel_bg, border_color = app_color.window_panel_border, - corner_radii = {}, + corner_radii = { 0, 0, 0, 0 }, blur_size = 0, font = get_state().default_font, text_color = app_color.text_default, @@ -421,7 +421,7 @@ theme_text :: proc() -> UI_Theme anchor = range2({},{}), alignment = {0, 0}, text_alignment = {0.0, 0.5}, - font_size = 14, + font_size = 12, margins = {0, 0, 0, 0}, padding = {0, 0, 0, 0}, border_width = 0, diff --git a/code/sectr/colors.odin b/code/sectr/colors.odin index ef01176..115c5dc 100644 --- a/code/sectr/colors.odin +++ b/code/sectr/colors.odin @@ -189,6 +189,6 @@ App_Thm_Light :: AppColorTheme { window_bar_bg = RGBA8{ 155, 155, 155, 255}, window_btn_close_bg_hot = RGBA8{ 145, 135, 135, 255}, - window_panel_bg = RGBA8 {135, 135, 135, 50}, // translucent_panel + window_panel_bg = RGBA8 {155, 155, 155, 50}, // translucent_panel window_panel_border = RGBA8{184, 184, 184, 255}, } diff --git a/code/sectr/engine/client_api.odin b/code/sectr/engine/client_api.odin index 9b96362..9a952b9 100644 --- a/code/sectr/engine/client_api.odin +++ b/code/sectr/engine/client_api.odin @@ -300,7 +300,7 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem // menu_bar.pos = Vec2(app_window.extent) * { -1, 1 } menu_bar.size = {140, 40} - settings_menu.min_size = {250, 200} + settings_menu.min_size = {260, 200} } // Demo project setup diff --git a/code/sectr/engine/render.odin b/code/sectr/engine/render.odin index 94e670f..637279f 100644 --- a/code/sectr/engine/render.odin +++ b/code/sectr/engine/render.odin @@ -169,36 +169,38 @@ render_mode_screenspace :: proc( screen_extent : Extents2, screen_ui : ^UI_State screen_corners := screen_get_corners() position := screen_corners.top_left - position.x += 2 position.y -= debug.draw_debug_text_y content := str_fmt( format, ..args ) text_size := measure_text_size( content, default_font, 14.0, 0.0 ) - debug_draw_text( content, position, 14.0 ) - debug.draw_debug_text_y += text_size.y + 3 + debug_draw_text( content, position, 10.0 ) + debug.draw_debug_text_y += text_size.y } profile("debug_text_vis") - fps_size : f32 = 14.0 - fps_msg := str_fmt( "FPS: %0.2f", fps_avg) - fps_msg_size := measure_text_size( fps_msg, default_font, fps_size, 0.0 ) - fps_msg_pos := screen_get_corners().top_right - { fps_msg_size.x, fps_msg_size.y } - debug_draw_text( fps_msg, fps_msg_pos, fps_size, color = Color_Red ) + if true { + fps_size : f32 = 14.0 + fps_msg := str_fmt( "FPS: %0.2f", fps_avg) + fps_msg_size := measure_text_size( fps_msg, default_font, fps_size, 0.0 ) + fps_msg_pos := screen_get_corners().top_right - { fps_msg_size.x, fps_msg_size.y } + debug_draw_text( fps_msg, fps_msg_pos, fps_size, color = Color_Red ) + } - debug_text( "Screen Width : %v", screen_size.x ) - debug_text( "Screen Height: %v", screen_size.y ) - debug_text( "frametime_target_ms : %f ms", frametime_target_ms ) - debug_text( "frametime (work) : %0.3f ms", frametime_delta_ms ) - debug_text( "frametime_last_elapsed_ms : %f ms", frametime_elapsed_ms ) + if false { + debug_text( "Screen Width : %v", screen_size.x ) + debug_text( "Screen Height: %v", screen_size.y ) + debug_text( "frametime_target_ms : %f ms", frametime_target_ms ) + debug_text( "frametime (work) : %0.3f ms", frametime_delta_ms ) + debug_text( "frametime_last_elapsed_ms : %f ms", frametime_elapsed_ms ) + } if replay.mode == ReplayMode.Record { debug_text( "Recording Input") } if replay.mode == ReplayMode.Playback { debug_text( "Replaying Input") } - debug_text("Zoom Target: %v", project.workspace.zoom_target) - if true + if false { using input_events @@ -221,11 +223,12 @@ render_mode_screenspace :: proc( screen_extent : Extents2, screen_ui : ^UI_State debug_text("Mouse Position (Workspace View): %0.2f", screen_to_ws_view_pos(input.mouse.pos) ) } - if true + if false { ui := & project.workspace.ui debug_text("Workspace Cam : %v", project.workspace.cam) + debug_text("Zoom Target : %v", project.workspace.zoom_target) debug_text("Box Count (Workspace): %v", ui.built_box_count ) @@ -240,7 +243,7 @@ render_mode_screenspace :: proc( screen_extent : Extents2, screen_ui : ^UI_State } } - if true + if false { ui := & screen_ui @@ -257,7 +260,7 @@ render_mode_screenspace :: proc( screen_extent : Extents2, screen_ui : ^UI_State } } - if true { + if false { state.config.font_size_canvas_scalar = 1.5 zoom_adjust_size := 16 * state.project.workspace.cam.zoom over_sample := f32(state.config.font_size_canvas_scalar) @@ -498,16 +501,23 @@ render_ui_via_box_tree :: proc( ui : ^UI_State, screen_extent : Vec2, ve_ctx : ^ GP_Render: { + corner_radii_total : f32 = 0 + for radius in style.corner_radii do corner_radii_total += radius + profile("draw_shapes") if style.bg_color.a != 0 { - draw_rect( bounds, style.bg_color ) + render_set_color( style.bg_color ) + if corner_radii_total > 0 do draw_rect_rounded( bounds, style.corner_radii, 16 ) + else do draw_rect( bounds) shape_enqueued = true } if style.border_color.a != 0 && border_width > 0 { render_set_color( style.border_color ) - draw_rect_border( bounds, border_width ) + + if corner_radii_total > 0 do draw_rect_rounded_border( bounds, style.corner_radii, border_width, 16 ) + else do draw_rect_border( bounds, border_width ) shape_enqueued = true } @@ -517,10 +527,12 @@ render_ui_via_box_tree :: proc( ui : ^UI_State, screen_extent : Vec2, ve_ctx : ^ { render_set_color( RGBA8_Debug_UI_Padding_Bounds ) draw_rect_border( computed.padding, line_thickness ) + shape_enqueued = true } else if debug.draw_ui_content_bounds { render_set_color( RGBA8_Debug_UI_Content_Bounds ) draw_rect_border( computed.content, line_thickness ) + shape_enqueued = true } if debug.draw_ui_box_bounds_points @@ -578,7 +590,8 @@ render_ui_via_box_list :: proc( render_list : []UI_RenderBoxInfo, screen_extent // profile("draw_shapes") if style.bg_color.a != 0 { - draw_rect( bounds, style.bg_color ) + render_set_color( style.bg_color ) + draw_rect( bounds ) shape_enqueued = true } @@ -643,15 +656,72 @@ draw_filled_circle :: proc(x, y, radius: f32, edges: int) gp.draw_filled_triangles(raw_data(triangles), u32(len(triangles))) } -draw_rect :: proc( rect : Range2, color : RGBA8 ) { +draw_rect :: proc( rect : Range2 ) { using rect - render_set_color( color ) - size := max - min position := min gp.draw_filled_rect( position.x, position.y, size.x, size.y ) } +// Note(Ed): This is an inefficint solution to rendering rounded rectangles +// Eventually when sokoL_gp is ported to Odin it would be best to implement these using a custom shader +// Uses triangulation from the center. (UVs are problably weird but wont matter for my use case) +draw_rect_rounded :: proc(rect: Range2, radii: [4]f32, segments: u32) +{ + segments := i32(segments) + width := rect.max.x - rect.min.x + height := rect.max.y - rect.min.y + + using Corner + + max_radius := min(width, height) * 0.5 + corner_radii := [4]f32{ + min(radii[ Top_Left ], max_radius), + min(radii[ Top_Right ], max_radius), + min(radii[ Bottom_Right], max_radius), + min(radii[ Bottom_Left ], max_radius), + } + top_left := corner_radii[ Top_Left ] + top_right := corner_radii[ Top_Right ] + bottom_left := corner_radii[ Bottom_Left ] + bottom_right := corner_radii[ Bottom_Right ] + + total_vertices := (segments + 1) * 4 + total_triangles := total_vertices + + vertices := make( []gp.Point, total_vertices ) + triangles := make( []gp.Triangle, total_triangles) + + add_corner_vertices :: proc(vertices : []gp.Point, offset : i32, cx, cy, radius : f32, start_angle : f32, segments : i32) + { + half_pi :: math.PI / 2 + for segment in i32(0) ..= segments { + angle := start_angle + half_pi * (f32(segment) / f32(segments)) + x := cx + radius * math.cos(angle) + y := cy + radius * math.sin(angle) + vertices[ offset + segment ] = gp.Point{x, y} + } + } + + half_pi :: math.PI / 2 + + // Add vertices for each corner + add_corner_vertices( vertices, 0 * (segments + 1), rect.min.x + top_left, rect.min.y + top_left, top_left, math.PI, segments ) + add_corner_vertices( vertices, 1 * (segments + 1), rect.max.x - top_right, rect.min.y + top_right, top_right, 3 * half_pi, segments ) + add_corner_vertices( vertices, 2 * (segments + 1), rect.max.x - bottom_left, rect.max.y - bottom_left, bottom_left, 0, segments ) + add_corner_vertices( vertices, 3 * (segments + 1), rect.min.x + bottom_right, rect.max.y - bottom_right, bottom_right, half_pi, segments ) + + // Create triangles using fan triangulation + center := gp.Point{ (rect.min.x + rect.max.x) * 0.5, (rect.min.y + rect.max.y) * 0.5 } + for vertex in 0 ..< total_vertices { + next := (vertex + 1) % total_vertices + triangles[vertex] = gp.Triangle { center, vertices[vertex], vertices[next] } + } + + // Draw the filled triangles + gp.draw_filled_triangles(raw_data(triangles), cast(u32)len(triangles)) +} + draw_rect_border :: proc( rect : Range2, border_width: f32) { rect_size := rect.max - rect.min @@ -666,6 +736,74 @@ draw_rect_border :: proc( rect : Range2, border_width: f32) gp.draw_filled_rects( raw_data(borders), u32(len(borders)) ) } +draw_rect_rounded_border :: proc(rect: Range2, radii: [4]f32, border_width: f32, segments: u32) +{ + width := rect.max.x - rect.min.x + height := rect.max.y - rect.min.y + + using Corner + + // Ensure radii are not too large + max_radius := min(width, height) * 0.5 + corner_radii := [4]f32{ + min(radii[0], max_radius), + min(radii[1], max_radius), + min(radii[2], max_radius), + min(radii[3], max_radius), + } + top_left := corner_radii[ Top_Left ] + top_right := corner_radii[ Top_Right ] + bottom_left := corner_radii[ Bottom_Left ] + bottom_right := corner_radii[ Bottom_Right ] + + // Ensure border width is not too large + border_width := min(border_width, max_radius) + + // Calculate the extents of the border rectangles + left := rect.min.x + max(top_left, bottom_left) + right := rect.max.x - max(top_right, bottom_right) + top := rect.min.y + max(top_left, top_right) + bottom := rect.max.y - max(bottom_left, bottom_right) + + // Draw border rectangles + gp.draw_filled_rect(left, rect.min.y, right - left, border_width) // Top + gp.draw_filled_rect(left, rect.max.y - border_width, right - left, border_width) // Bottom + gp.draw_filled_rect(rect.min.x, top, border_width, bottom - top) // Left + gp.draw_filled_rect(rect.max.x - border_width, top, border_width, bottom - top) // Right + + draw_corner_border :: proc( x, y : f32, outer_radius, inner_radius : f32, start_angle : f32, segments : u32 ) + { + if outer_radius <= inner_radius do return + triangles := make( []gp.Triangle, int(segments) * 2 ) + + half_pi :: math.PI / 2 + segment_quo := 1.0 / f32(segments) + for segment in 0 ..< segments + { + angle1 := start_angle + half_pi * f32(segment) * segment_quo + angle2 := start_angle + half_pi * f32(segment + 1) * segment_quo + + outer1 := gp.Vec2{x + outer_radius * math.cos(angle1), y + outer_radius * math.sin(angle1)} + outer2 := gp.Vec2{x + outer_radius * math.cos(angle2), y + outer_radius * math.sin(angle2)} + inner1 := gp.Vec2{x + inner_radius * math.cos(angle1), y + inner_radius * math.sin(angle1)} + inner2 := gp.Vec2{x + inner_radius * math.cos(angle2), y + inner_radius * math.sin(angle2)} + + triangles[segment * 2 ] = gp.Triangle { outer1, outer2, inner1 } + triangles[segment * 2 + 1] = gp.Triangle { inner1, outer2, inner2 } + } + + gp.draw_filled_triangles(raw_data(triangles), u32(len(triangles))) + } + + half_pi :: math.PI / 2 + + // Draw corner borders + draw_corner_border(rect.min.x + top_left, rect.min.y + top_left, top_left, max(top_left - border_width, 0), math.PI, segments) + draw_corner_border(rect.max.x - top_right, rect.min.y + top_right, top_right, max(top_right - border_width, 0), 3 * half_pi, segments) + draw_corner_border(rect.min.x + bottom_left, rect.max.y - bottom_left, bottom_left, max(bottom_left - border_width, 0), half_pi, segments) + draw_corner_border(rect.max.x - bottom_right, rect.max.y - bottom_right, bottom_right, max(bottom_right - border_width, 0), 0, segments) +} + // Draw text using a string and normalized render coordinates draw_text_string_pos_norm :: proc( content : string, id : FontID, size : f32, pos : Vec2, color := Color_White, scale : f32 = 1.0 ) { diff --git a/code/sectr/ui/core/base.odin b/code/sectr/ui/core/base.odin index 1c37c84..18b8332 100644 --- a/code/sectr/ui/core/base.odin +++ b/code/sectr/ui/core/base.odin @@ -10,10 +10,10 @@ Corner :: enum i32 { _01, _10, _11, - TopLeft = _00, - TopRight = _01, - BottomLeft = _10, - BottomRight = _11, + Top_Left = _00, + Top_Right = _01, + Bottom_Left = _10, + Bottom_Right = _11, Count = 4, } diff --git a/code/sectr/ui/core/style.odin b/code/sectr/ui/core/style.odin index 0073375..372b4ff 100644 --- a/code/sectr/ui/core/style.odin +++ b/code/sectr/ui/core/style.odin @@ -1,5 +1,9 @@ package sectr +BoxCorners :: struct { + top_left, top_right, bottom_right, bottom_left : f32, +} + // TODO(Ed): We problably can embedd this info into the UI_Layout with the regular text_alignment UI_TextAlign :: enum u32 { Left, @@ -20,7 +24,6 @@ UI_Style :: struct { bg_color : RGBA8, border_color : RGBA8, - // TODO(Ed): We cannot support individual corners unless we add it to raylib (or finally change the rendering backend) corner_radii : [Corner.Count]f32, // TODO(Ed) : Add support for this eventually diff --git a/code/sectr/ui/widgets.odin b/code/sectr/ui/widgets.odin index ce71cf6..fb39afe 100644 --- a/code/sectr/ui/widgets.odin +++ b/code/sectr/ui/widgets.odin @@ -277,7 +277,7 @@ ui_resizable_handles :: proc( parent : ^UI_Widget, pos : ^Vec2, size : ^Vec2, style := UI_Style { bg_color = Color_Transparent, border_color = Color_Transparent, - corner_radii = {5, 0, 0, 0}, + corner_radii = {5, 5, 5, 5}, blur_size = 0, font = get_state().default_font, text_color = app_color.text_default,