From a2b6325b5b101fa90e363d91b041443f75628de5 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 9 May 2024 04:02:33 -0400 Subject: [PATCH] coordinate space math fixes, got resize handles working in settings menu prototype will eventually lift to its own generic widget I still need to implement the corner resize.. --- Readme.md | 1 + code/colors.odin | 26 +- code/env.odin | 3 + code/grime.odin | 6 +- code/grime_memory_tracker.odin | 9 +- code/input.odin | 9 +- code/space.odin | 64 ++++- code/text.odin | 37 ++- code/tick_render.odin | 399 ++++++++++++++++----------- code/tick_update.odin | 475 +++++++++++++++++++++------------ code/ui.odin | 187 +------------ code/ui_layout.odin | 6 +- code/ui_style.odin | 174 ++++++++++++ code/ui_tests.odin | 3 + ols.json | 12 - 15 files changed, 857 insertions(+), 554 deletions(-) create mode 100644 code/ui_style.odin diff --git a/Readme.md b/Readme.md index 03957c8..24f7d18 100644 --- a/Readme.md +++ b/Readme.md @@ -22,6 +22,7 @@ The dependencies are: * An ini parser The client(sectr) module's organization is relatively flat due to the nature of odin's package management not allowing for cyclic dependencies across modules, and modules can only be in one directory. +This makes it difficult to unflatten as the depedency chain must clear with no inter-module collisions, not something organic todo in a prototype... Even so the notatble groups are: diff --git a/code/colors.odin b/code/colors.odin index 7df434b..a3f311b 100644 --- a/code/colors.odin +++ b/code/colors.odin @@ -4,20 +4,24 @@ import rl "vendor:raylib" Color :: rl.Color Color_Blue :: rl.BLUE -Color_Green :: rl.GREEN +// Color_Green :: rl.GREEN Color_Red :: rl.RED Color_White :: rl.WHITE -Color_Transparent :: Color { 0, 0, 0, 0 } -Color_BG :: Color { 41, 41, 45, 255 } -Color_BG_TextBox :: Color { 32, 32, 32, 180 } -Color_BG_TextBox_Green :: Color { 102, 102, 110, 255 } -Color_Frame_Disabled :: Color { 22, 22, 22, 120 } -Color_Frame_Hover :: Color { 122, 122, 125, 200 } -Color_Frame_Select :: Color { 188, 188, 188, 220 } -Color_GreyRed :: Color { 220, 100, 100, 50 } -Color_White_A125 :: Color { 255, 255, 255, 165 } -Color_Black :: Color { 0, 0, 0, 255 } +Color_Transparent :: Color { 0, 0, 0, 0 } +Color_BG :: Color { 61, 61, 64, 255 } +Color_BG_TextBox :: Color { 32, 32, 32, 180 } +Color_BG_Panel :: Color { 32, 32, 32, 255 } +Color_BG_Panel_Translucent :: Color { 32, 32, 32, 220 } +Color_BG_TextBox_Green :: Color { 102, 102, 110, 255 } +Color_Frame_Disabled :: Color { 22, 22, 22, 120 } +Color_Frame_Hover :: Color { 122, 122, 125, 200 } +Color_Frame_Select :: Color { 188, 188, 188, 220 } +Color_GreyRed :: Color { 220, 100, 100, 50 } +Color_White_A125 :: Color { 255, 255, 255, 165 } +Color_Black :: Color { 0, 0, 0, 255 } +Color_Green :: Color { 0, 180, 0, 255 } +Color_ResizeHandle :: Color { 90, 90, 100, 255 } Color_3D_BG :: Color { 188, 182 , 170, 255 } diff --git a/code/env.odin b/code/env.odin index b3843f1..c13f8ee 100644 --- a/code/env.odin +++ b/code/env.odin @@ -202,6 +202,9 @@ State :: struct { // the screen-space UI and the current workspace UI. // This is used so that the ui api doesn't need to have the user pass the context every single time. ui_context : ^UI_State, + + // The camera is considered the "context" for coodrinate space operations in rendering + cam_context : Camera, } get_state :: proc "contextless" () -> ^ State { diff --git a/code/grime.odin b/code/grime.odin index c3f61d4..d128778 100644 --- a/code/grime.odin +++ b/code/grime.odin @@ -134,9 +134,9 @@ dot :: proc { dot_unitv3_vs, } -draw_text :: proc { - draw_text_string, - draw_text_string_cached, +ws_view_draw_text :: proc { + ws_view_draw_text_string, + ws_view_draw_text_StrRunesPair, } from_bytes :: proc { diff --git a/code/grime_memory_tracker.odin b/code/grime_memory_tracker.odin index ac97853..29eaa23 100644 --- a/code/grime_memory_tracker.odin +++ b/code/grime_memory_tracker.odin @@ -1,8 +1,15 @@ +/* + This was a tracking allocator made to kill off various bugs left with grime's pool & slab allocators + It doesn't perform that well on a per-frame basis and should be avoided for general memory debugging + + It only makes sure that memory allocations don't collide in the allocator and deallocations don't occur for memory never allocated. + + I'm keeping it around as an artifact & for future allocators I may make. +*/ package sectr MemoryTrackerEntry :: struct { start, end : rawptr, - // owner : string, } MemoryTracker :: struct { diff --git a/code/input.odin b/code/input.odin index 89a400e..8290115 100644 --- a/code/input.odin +++ b/code/input.odin @@ -269,14 +269,14 @@ MouseState :: struct { side, forward, back, extra : DigitalBtn } }, - pos, delta : Vec2, + raw_pos, pos, delta : Vec2, vertical_wheel, horizontal_wheel : AnalogAxis } mouse_world_delta :: #force_inline proc "contextless" () -> Vec2 { using state := get_state() cam := & state.project.workspace.cam - return { input.mouse.delta.x, -input.mouse.delta.y } * ( 1 / cam.zoom ) + return input.mouse.delta * ( 1 / cam.zoom ) } InputState :: struct { @@ -347,8 +347,9 @@ poll_input :: proc( old, new : ^ InputState ) input_process_digital_btn( old_btn, new_btn, is_down ) } - new.mouse.pos = rl.GetMousePosition() - transmute(Vec2) get_state().app_window.extent - new.mouse.delta = rl.GetMouseDelta() + new.mouse.raw_pos = rl.GetMousePosition() + new.mouse.pos = render_to_surface_pos(new.mouse.raw_pos) + new.mouse.delta = rl.GetMouseDelta() * {1, -1} new.mouse.vertical_wheel = rl.GetMouseWheelMove() } } diff --git a/code/space.odin b/code/space.odin index edc26f3..b14967d 100644 --- a/code/space.odin +++ b/code/space.odin @@ -140,9 +140,9 @@ screen_size :: proc "contextless" () -> AreaSize { screen_get_bounds :: #force_inline proc "contextless" () -> Range2 { state := get_state(); using state - screen_extent := state.app_window.extent - bottom_left := Vec2 { -screen_extent.x, -screen_extent.y} - top_right := Vec2 { screen_extent.x, screen_extent.y} + surface_extent := state.app_window.extent + bottom_left := Vec2 { -surface_extent.x, -surface_extent.y} + top_right := Vec2 { surface_extent.x, surface_extent.y} return range2( bottom_left, top_right ) } @@ -156,6 +156,7 @@ screen_get_corners :: #force_inline proc "contextless"() -> BoundsCorners2 { return { top_left, top_right, bottom_left, bottom_right } } +// TODO(Ed): Use a cam/workspace context instead (when multiple workspaces viewproting supported) view_get_bounds :: #force_inline proc "contextless"() -> Range2 { state := get_state(); using state cam := & project.workspace.cam @@ -166,6 +167,7 @@ view_get_bounds :: #force_inline proc "contextless"() -> Range2 { return range2( bottom_left, top_right ) } +// TODO(Ed): Use a cam/workspace context instead (when multiple workspace viewproting) view_get_corners :: #force_inline proc "contextless"() -> BoundsCorners2 { state := get_state(); using state cam := & project.workspace.cam @@ -178,30 +180,64 @@ view_get_corners :: #force_inline proc "contextless"() -> BoundsCorners2 { return { top_left, top_right, bottom_left, bottom_right } } -screen_to_world :: #force_inline proc "contextless" (pos: Vec2) -> Vec2 { - state := get_state(); using state - cam := & project.workspace.cam - result := Vec2 { cam.target.x, -cam.target.y} + Vec2 { pos.x, -pos.y } * (1 / cam.zoom) +render_to_surface_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 { + extent := & get_state().app_window.extent + result := Vec2 { + pos.x - extent.x, + pos.y * -1 + extent.y + } return result } -screen_to_render :: #force_inline proc "contextless"(pos: Vec2) -> Vec2 { - screen_extent := transmute(Vec2) get_state().project.workspace.cam.offset - return pos + { screen_extent.x, -screen_extent.y } +render_to_ws_view_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 { + return {} } -world_screen_extent :: #force_inline proc "contextless"() -> Extents2 { +surface_to_ws_view_pos :: #force_inline proc "contextless" (pos: Vec2) -> Vec2 { + state := get_state(); using state + cam := & project.workspace.cam + result := Vec2 { cam.target.x, -cam.target.y} + Vec2 { pos.x, pos.y } * (1 / cam.zoom) + return result +} + +// (Surface) Centered screen space to conventional screen space used for rendering +surface_to_render_pos :: #force_inline proc "contextless" (pos : Vec2) -> Vec2 { + screen_extent := transmute(Vec2) get_state().app_window.extent + return pos * {1, -1} + { screen_extent.x, screen_extent.y } +} + +// TODO(Ed): These should assume a cam_context or have the ability to provide it in params + +// Extent of workspace view (currently hardcoded to the app window's extent, eventually will be based on a viewport object's extent field) +// TODO(Ed): Support a position which would not be centered on the screen if in a viewport +ws_view_extent :: #force_inline proc "contextless"() -> Extents2 { state := get_state(); using state cam_zoom_ratio := 1.0 / project.workspace.cam.zoom return app_window.extent * cam_zoom_ratio } -world_to_screen_pos :: #force_inline proc "contextless"(position: Vec2) -> Vec2 { +// Workspace view to surface space position +// TODO(Ed): Support a position which would not be centered on the screen if in a viewport +ws_view_to_surface_pos :: #force_inline proc "contextless"(position: Vec2) -> Vec2 { + return position +} + +ws_view_to_render_pos :: #force_inline proc "contextless"(position: Vec2) -> Vec2 { return { position.x, position.y * -1 } } -world_to_screen_no_zoom :: #force_inline proc "contextless"(position: Vec2) -> Vec2 { +// Workspace view to surface space position (zoom agnostic) +// TODO(Ed): Support a position which would not be centered on the screen if in a viewport +ws_view_to_surface_pos_no_zoom :: #force_inline proc "contextless"(position: Vec2) -> Vec2 { state := get_state(); using state cam_zoom_ratio := 1.0 / state.project.workspace.cam.zoom - return { position.x, position.y * -1 } * cam_zoom_ratio + return { position.x, position.y } * cam_zoom_ratio +} + +// Workspace view to render space position (zoom agnostic) +// TODO(Ed): Support a position which would not be centered on the screen if in a viewport +ws_view_to_render_pos_no_zoom :: #force_inline proc "contextless"(position: Vec2) -> Vec2 { + state := get_state(); using state + cam_zoom_ratio := 1.0 / state.project.workspace.cam.zoom + return { position.x, position.y } * cam_zoom_ratio } diff --git a/code/text.odin b/code/text.odin index a9c418b..239d256 100644 --- a/code/text.odin +++ b/code/text.odin @@ -21,7 +21,7 @@ debug_draw_text :: proc( content : string, pos : Vec2, size : f32, color : rl.Co // if ( len(font) == 0 ) { font = default_font } - pos := screen_to_render(pos) + pos := surface_to_render_pos(pos) px_size := size @@ -34,7 +34,34 @@ debug_draw_text :: proc( content : string, pos : Vec2, size : f32, color : rl.Co tint = color ); } -draw_text_string :: proc( content : string, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default ) +draw_text_screenspace :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default ) +{ + // profile(#procedure) + state := get_state(); using state + + if len( content.str ) == 0 { + return + } + font := font + if font.key == Font_Default.key { + font = default_font + } + pos := pos + + rl_font := to_rl_Font(font, size ) + runes := content.runes + + rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.TRILINEAR) + rl.DrawTextCodepoints( rl_font, + raw_data(runes), cast(i32) len(runes), + position = transmute(rl.Vector2) pos, + fontSize = size, + spacing = 0.0, + tint = color ); + rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT) +} + +ws_view_draw_text_string :: proc( content : string, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default ) { // profile(#procedure) state := get_state(); using state @@ -50,7 +77,7 @@ draw_text_string :: proc( content : string, pos : Vec2, size : f32, color : rl.C // if len(font) == 0 { font = default_font } - pos := world_to_screen_pos(pos) + pos := ws_view_to_render_pos(pos) px_size := size zoom_adjust := px_size * project.workspace.cam.zoom @@ -66,7 +93,7 @@ draw_text_string :: proc( content : string, pos : Vec2, size : f32, color : rl.C rl.SetTextureFilter(rl_font.texture, rl.TextureFilter.POINT) } -draw_text_string_cached :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default ) +ws_view_draw_text_StrRunesPair :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default ) { // profile(#procedure) state := get_state(); using state @@ -78,7 +105,7 @@ draw_text_string_cached :: proc( content : StrRunesPair, pos : Vec2, size : f32, if font.key == Font_Default.key { font = default_font } - pos := world_to_screen_pos(pos) + pos := ws_view_to_render_pos(pos) px_size := size zoom_adjust := px_size * project.workspace.cam.zoom diff --git a/code/tick_render.odin b/code/tick_render.odin index eb35f8a..a7a19da 100644 --- a/code/tick_render.odin +++ b/code/tick_render.odin @@ -4,6 +4,23 @@ import "core:fmt" import rl "vendor:raylib" +draw_rectangle :: #force_inline proc "contextless" ( rect : rl.Rectangle, style : UI_Style ) { + if style.layout.corner_radii[0] > 0 { + rl.DrawRectangleRounded( rect, style.layout.corner_radii[0], 9, style.bg_color ) + } + else { + rl.DrawRectangleRec( rect, style.bg_color ) + } +} + +draw_rectangle_lines :: #force_inline proc "contextless" ( rect : rl.Rectangle, style : UI_Style, color : Color, thickness : f32 ) { + if style.layout.corner_radii[0] > 0 { + rl.DrawRectangleRoundedLines( rect, style.layout.corner_radii[0], 9, thickness, color ) + } + else { + rl.DrawRectangleLinesEx( rect, thickness, color ) + } +} render :: proc() { @@ -15,124 +32,34 @@ render :: proc() rl.BeginDrawing() rl.ClearBackground( Color_BG ) - render_mode_2d() + render_mode_2d_workspace() render_mode_screenspace() rl.EndDrawing() } -render_mode_screenspace :: proc () +// Experimental 3d viewport, not really the focus of this prototype +// Until we can have a native or interpreted program render to it its not very useful. +// Note(Ed): Other usecase could be 3d vis notes & math/graphical debug +render_mode_3d :: proc() { - profile("Render Screenspace") + profile(#procedure) state := get_state(); using state - replay := & Memory_App.replay - cam := & project.workspace.cam - win_extent := state.app_window.extent - //region App UI - Render_App_UI: - { - profile("App UI") - ui := & state.app_ui - root := ui.root - if root.num_children == 0 { - break Render_App_UI - } + rl.BeginDrawing() + rl.BeginTextureMode( debug.viewport_rt ) + rl.BeginMode3D( debug.cam_vp ) + rl.ClearBackground( Color_3D_BG ) - current := root.first - for ; current != nil; current = ui_box_tranverse_next( current ) - { - // profile("Box") - parent := current.parent - - style := current.style - computed := & current.computed - - computed_size := computed.bounds.p1 - computed.bounds.p0 - } - } - //endregion App UI - - screen_top_left : Vec2 = { - -win_extent.x + cam.target.x, - -win_extent.y + cam.target.y, - } - - fps_msg := str_fmt_tmp( "FPS: %f", fps_avg) - fps_msg_width := measure_text_size( fps_msg, default_font, 16.0, 0.0 ).x - fps_msg_pos := screen_get_corners().top_right - { fps_msg_width, 0 } - debug_draw_text( fps_msg, fps_msg_pos, 16.0, color = rl.GREEN ) - - debug_text :: proc( format : string, args : ..any ) - { - @static draw_text_scratch : [Kilobyte * 64]u8 - - state := get_state(); using state - if debug.draw_debug_text_y > 800 { - debug.draw_debug_text_y = 50 - } - - cam := & project.workspace.cam - screen_corners := screen_get_corners() - - position := screen_corners.top_right - position.x -= 800 - position.y += debug.draw_debug_text_y - - content := str_fmt_buffer( draw_text_scratch[:], format, ..args ) - debug_draw_text( content, position, 14.0 ) - - debug.draw_debug_text_y += 14 - } - - // Debug Text - { - // debug_text( "Screen Width : %v", rl.GetScreenWidth () ) - // debug_text( "Screen Height: %v", rl.GetScreenHeight() ) - debug_text( "frametime_target_ms : %f ms", frametime_target_ms ) - debug_text( "frametime : %f 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 debug.mouse_vis { - debug_text( "Mouse Vertical Wheel: %v", input.mouse.vertical_wheel ) - debug_text( "Mouse Position (Screen): %v", input.mouse.pos ) - debug_text("Mouse Position (World): %v", screen_to_world(input.mouse.pos) ) - cursor_pos := transmute(Vec2) state.app_window.extent + input.mouse.pos - rl.DrawCircleV( cursor_pos, 10, Color_White_A125 ) - } - - ui := & project.workspace.ui - - // debug_text("Box Count: %v", ui.built_box_count ) - - hot_box := ui_box_from_key( ui.curr_cache, ui.hot ) - active_box := ui_box_from_key( ui.curr_cache, ui.active ) - if hot_box != nil { - debug_text("Hot Box : %v", hot_box.label.str ) - // debug_text("Hot Range2: %v", hot_box.computed.bounds.pts) - } - if active_box != nil{ - // debug_text("Active Box: %v", active_box.label.str ) - } - // debug_text("Active Resizing: %v", ui.active_start_signal.resizing) - - view := view_get_bounds() - // debug_text("View Bounds (World): %v", view.pts ) - - debug.draw_debug_text_y = 50 + rl.EndMode3D() + rl.EndTextureMode() + rl.EndDrawing() } -render_mode_2d :: proc() +// TODO(Ed): Eventually this needs to become a 'viewport within a UI' +// This would allow the user to have more than one workspace open at the same time +render_mode_2d_workspace :: proc() { profile(#procedure) state := get_state(); using state @@ -164,8 +91,8 @@ render_mode_2d :: proc() when false { render_view := Range2 { pts = { - world_to_screen_pos(view_bounds.min), - world_to_screen_pos(view_bounds.max), + world_to_screen_pos( view_bounds.min), + world_to_screen_pos( view_bounds.max), }} view_rect := rl.Rectangle { render_view.min.x, @@ -205,24 +132,24 @@ render_mode_2d :: proc() // profile_begin("Calculating Raylib rectangles") render_anchors := range2( - world_to_screen_pos(computed.anchors.min), - world_to_screen_pos(computed.anchors.max), + ws_view_to_render_pos(computed.anchors.min), + ws_view_to_render_pos(computed.anchors.max), ) render_margins := range2( - world_to_screen_pos(computed.margins.min), - world_to_screen_pos(computed.margins.max), + ws_view_to_render_pos(computed.margins.min), + ws_view_to_render_pos(computed.margins.max), ) render_bounds := range2( - world_to_screen_pos(computed.bounds.min), - world_to_screen_pos(computed.bounds.max), + ws_view_to_render_pos(computed.bounds.min), + ws_view_to_render_pos(computed.bounds.max), ) render_padding := range2( - world_to_screen_pos(computed.padding.min), - world_to_screen_pos(computed.padding.max), + ws_view_to_render_pos(computed.padding.min), + ws_view_to_render_pos(computed.padding.max), ) render_content := range2( - world_to_screen_pos(computed.content.min), - world_to_screen_pos(computed.content.max), + ws_view_to_render_pos(computed.content.min), + ws_view_to_render_pos(computed.content.max), ) rect_anchors := range2_to_rl_rect( render_anchors ) @@ -232,24 +159,6 @@ render_mode_2d :: proc() rect_content := range2_to_rl_rect( render_content ) // profile_end() - draw_rectangle :: #force_inline proc "contextless" ( rect : rl.Rectangle, style : UI_Style ) { - if style.layout.corner_radii[0] > 0 { - rl.DrawRectangleRounded( rect, style.layout.corner_radii[0], 9, style.bg_color ) - } - else { - rl.DrawRectangleRec( rect, style.bg_color ) - } - } - - draw_rectangle_lines :: #force_inline proc "contextless" ( rect : rl.Rectangle, style : UI_Style, color : Color, thickness : f32 ) { - if style.layout.corner_radii[0] > 0 { - rl.DrawRectangleRoundedLines( rect, style.layout.corner_radii[0], 9, thickness, color ) - } - else { - rl.DrawRectangleLinesEx( rect, thickness, color ) - } - } - // profile_begin("rl.DrawRectangleRounded( rect_bounds, style.layout.corner_radii[0], 9, style.bg_color )") if style.bg_color.a != 0 { @@ -281,8 +190,8 @@ render_mode_2d :: proc() { -resize_percent_width.x, resize_percent_width.x })) render_resize := range2( - world_to_screen_pos(resize_border_non_range.min), - world_to_screen_pos(resize_border_non_range.max), + ws_view_to_render_pos(resize_border_non_range.min), + ws_view_to_render_pos(resize_border_non_range.max), ) rect_resize := rl.Rectangle { render_resize.min.x, @@ -307,7 +216,7 @@ render_mode_2d :: proc() // profile_end() if len(current.text.str) > 0 { - draw_text( current.text, world_to_screen_pos(computed.text_pos), style.font_size, style.text_color ) + ws_view_draw_text( current.text, ws_view_to_render_pos(computed.text_pos * {1, -1}), style.font_size, style.text_color ) } } } @@ -315,8 +224,8 @@ render_mode_2d :: proc() if debug.mouse_vis { - cursor_world_pos := screen_to_world(input.mouse.pos) - rl.DrawCircleV( world_to_screen_pos(cursor_world_pos), 5, Color_GreyRed ) + cursor_world_pos := surface_to_ws_view_pos(input.mouse.pos) + rl.DrawCircleV( ws_view_to_render_pos(cursor_world_pos), 5, Color_GreyRed ) } rl.DrawCircleV( { 0, 0 }, 1 * cam_zoom_ratio, Color_White ) @@ -324,18 +233,208 @@ render_mode_2d :: proc() rl.EndMode2D() } -render_mode_3d :: proc() +render_mode_screenspace :: proc () +{ + profile("Render Screenspace") + + state := get_state(); using state + replay := & Memory_App.replay + cam := & project.workspace.cam + win_extent := state.app_window.extent + + render_app_ui() + + fps_msg := str_fmt_tmp( "FPS: %f", fps_avg) + fps_msg_width := measure_text_size( fps_msg, default_font, 16.0, 0.0 ).x + fps_msg_pos := screen_get_corners().top_right - { fps_msg_width, 0 } - { 5, 5 } + debug_draw_text( fps_msg, fps_msg_pos, 16.0, color = rl.GREEN ) + + debug_text :: proc( format : string, args : ..any ) + { + @static draw_text_scratch : [Kilobyte * 64]u8 + + state := get_state(); using state + if debug.draw_debug_text_y > 800 { + debug.draw_debug_text_y = 0 + } + + cam := & project.workspace.cam + screen_corners := screen_get_corners() + + position := screen_corners.top_right + position.x -= app_window.extent.x + position.y -= debug.draw_debug_text_y + + content := str_fmt_buffer( draw_text_scratch[:], format, ..args ) + debug_draw_text( content, position, 14.0 ) + + debug.draw_debug_text_y += 14 + } + + // Debug Text + { + // debug_text( "Screen Width : %v", rl.GetScreenWidth () ) + // debug_text( "Screen Height: %v", rl.GetScreenHeight() ) + debug_text( "frametime_target_ms : %f ms", frametime_target_ms ) + debug_text( "frametime : %f 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 debug.mouse_vis { + debug_text("Mouse Vertical Wheel: %v", input.mouse.vertical_wheel ) + debug_text("Mouse Position (Render) : %v", input.mouse.raw_pos ) + debug_text("Mouse Position (Surface) : %v", input.mouse.pos ) + debug_text("Mouse Position (Workspace View): %v", surface_to_ws_view_pos(input.mouse.pos) ) + rl.DrawCircleV( input.mouse.raw_pos, 10, Color_White_A125 ) + rl.DrawCircleV( surface_to_render_pos(input.mouse.pos), 2, Color_BG ) + } + + ui := & project.workspace.ui + + // debug_text("Box Count: %v", ui.built_box_count ) + + hot_box := ui_box_from_key( ui.curr_cache, ui.hot ) + active_box := ui_box_from_key( ui.curr_cache, ui.active ) + if hot_box != nil { + debug_text("Hot Box : %v", hot_box.label.str ) + // debug_text("Hot Range2: %v", hot_box.computed.bounds.pts) + } + if active_box != nil{ + // debug_text("Active Box: %v", active_box.label.str ) + } + // debug_text("Active Resizing: %v", ui.active_start_signal.resizing) + + view := view_get_bounds() + // debug_text("View Bounds (World): %v", view.pts ) + + debug.draw_debug_text_y = 14 +} + +// A non-zoomable static-view for ui +// Only a scalar factor may be applied to the size of widgets & fonts +// 'Window tiled' panels reside here +render_app_ui :: proc() { profile(#procedure) - state := get_state(); using state + using state := get_state() - rl.BeginDrawing() - rl.BeginTextureMode( debug.viewport_rt ) - rl.BeginMode3D( debug.cam_vp ) - rl.ClearBackground( Color_3D_BG ) + //region App UI + Render_App_UI: + { + profile("App UI") + ui := & state.app_ui + root := ui.root + if root.num_children == 0 { + break Render_App_UI + } - rl.EndMode3D() - rl.EndTextureMode() - rl.EndDrawing() + current := root.first + for ; current != nil; current = ui_box_tranverse_next( current ) + { + // profile("Box") + parent := current.parent + + style := current.style + computed := & current.computed + + computed_size := computed.bounds.p1 - computed.bounds.p0 + + render_anchors := range2( + surface_to_render_pos(computed.anchors.min), + surface_to_render_pos(computed.anchors.max), + ) + render_margins := range2( + surface_to_render_pos(computed.margins.min), + surface_to_render_pos(computed.margins.max), + ) + render_bounds := range2( + surface_to_render_pos(computed.bounds.min), + surface_to_render_pos(computed.bounds.max), + ) + render_padding := range2( + surface_to_render_pos(computed.padding.min), + surface_to_render_pos(computed.padding.max), + ) + render_content := range2( + surface_to_render_pos(computed.content.min), + surface_to_render_pos(computed.content.max), + ) + rect_anchors := range2_to_rl_rect( render_anchors ) + rect_margins := range2_to_rl_rect( render_margins ) + rect_bounds := range2_to_rl_rect( render_bounds ) + rect_padding := range2_to_rl_rect( render_padding ) + rect_content := range2_to_rl_rect( render_content ) + // profile_end() + + // profile_begin("rl.DrawRectangleRounded( rect_bounds, style.layout.corner_radii[0], 9, style.bg_color )") + if style.bg_color.a != 0 + { + draw_rectangle( rect_bounds, style ) + } + if style.border_width > 0 { + draw_rectangle_lines( rect_bounds, style, style.border_color, style.border_width ) + } + // profile_end() + + line_thickness : f32 = 1 + + // profile_begin("rl.DrawRectangleRoundedLines: padding & content") + if equal_range2(computed.content, computed.padding) { + // draw_rectangle_lines( rect_padding, style, Color_Debug_UI_Padding_Bounds, line_thickness ) + } + else { + // draw_rectangle_lines( rect_content, style, Color_Debug_UI_Content_Bounds, line_thickness ) + } + // profile_end() + + if .Mouse_Resizable in current.flags + { + // profile("Resize Bounds") + resize_border_width := cast(f32) get_state().config.ui_resize_border_width + resize_percent_width := computed_size * (resize_border_width * 1.0/ 200.0) + resize_border_non_range := add(current.computed.bounds, range2( + { resize_percent_width.x, -resize_percent_width.x }, + { -resize_percent_width.x, resize_percent_width.x })) + + render_resize := range2( + resize_border_non_range.min, + resize_border_non_range.max, + ) + rect_resize := rl.Rectangle { + render_resize.min.x, + render_resize.min.y, + render_resize.max.x - render_resize.min.x, + render_resize.max.y - render_resize.min.y, + } + draw_rectangle_lines( rect_padding, style, Color_Red, line_thickness ) + } + + point_radius : f32 = 3 + + // profile_begin("circles") + // center := Vec2 { + // render_bounds.p0.x + computed_size.x * 0.5, + // render_bounds.p0.y - computed_size.y * 0.5, + // } + // rl.DrawCircleV( center, point_radius, Color_White ) + + // rl.DrawCircleV( render_bounds.p0, point_radius, Color_Red ) + // rl.DrawCircleV( render_bounds.p1, point_radius, Color_Blue ) + // profile_end() + + if len(current.text.str) > 0 { + draw_text_screenspace( current.text, surface_to_render_pos(computed.text_pos), style.font_size, style.text_color ) + } + } + } + //endregion App UI } diff --git a/code/tick_update.odin b/code/tick_update.odin index afdb519..e95b5af 100644 --- a/code/tick_update.odin +++ b/code/tick_update.odin @@ -149,6 +149,7 @@ update :: proc( delta_time : f64 ) -> b32 } //region 2D Camera Manual Nav + // TODO(Ed): This should be per workspace view { // profile("Camera Manual Nav") digital_move_speed : f32 = 200.0 @@ -184,13 +185,314 @@ update :: proc( delta_time : f64 ) -> b32 if debug_actions.cam_mouse_pan { if is_within_screenspace(input.mouse.pos) { - pan_velocity := input.mouse.delta * ( 1 / cam.zoom ) + pan_velocity := input.mouse.delta * vec2(1, -1) * ( 1 / cam.zoom ) cam.target -= pan_velocity } } } //endregion 2D Camera Manual Nav + // TODO(Ed): We need input buffer so that we can consume input actions based on the UI with priority + + //region App UI Tick + { + profile("App Screenspace Imgui") + + ui_graph_build( & state.app_ui ) + ui := ui_context + + /* + Prototype app menu + This is a menu bar for the app for now inside the same ui as the workspace's UI state + Eventually this will get moved out to its own UI state for the app itself. + */ + if true + { + fmt :: str_fmt_alloc + + @static bar_pos := Vec2{} + bar_size := vec2( 400, 40 ) + + menu_bar : UI_Widget + { + theme := UI_Style { + flags = { + }, + bg_color = { 0, 0, 0, 30 }, + border_color = { 0, 0, 0, 200 }, + + font = default_font, + font_size = 12, + text_color = Color_White, + + layout = UI_Layout { + anchor = {}, + border_width = 1.0, + pos = bar_pos, + size = range2( bar_size, {}), + // padding = { 10, 10, 10, 10 }, + }, + } + ui_theme_via_style(theme) + menu_bar = ui_widget("App Menu Bar", UI_BoxFlags {} ) + menu_bar.text = { fmt("%v", bar_pos), {} } + menu_bar.text.runes = to_runes(menu_bar.text.str) + + if (menu_bar.first_frame) { + bar_pos = screen_get_corners().top_right - vec2(0, app_window.extent.y * 0.5) + } + } + // Setup Children + settings_btn : UI_Widget + { + ui_parent(menu_bar) + + style := UI_Style { + flags = { + // .Origin_At_Anchor_Center + .Fixed_Height + }, + bg_color = Color_Frame_Disabled, + + font = default_font, + font_size = 18, + text_color = Color_White, + + layout = UI_Layout { + anchor = range2( {0, 0}, {0.0, 0} ), + alignment = { 0.0, 1.0 }, + text_alignment = { 0.5, 0.5 }, + pos = { 0, 0 }, + size = range2( {25, bar_size.y}, {0, 0}) + } + } + theme := UI_StyleTheme { styles = { + style, + style, + style, + style, + }} + theme.disabled.bg_color = Color_Frame_Disabled + theme.hot.bg_color = Color_White + theme.active.bg_color = Color_Frame_Select + ui_style_theme(theme) + + move_box : UI_Widget + { + move_box = ui_button("Move Box") + if move_box.dragging { + bar_pos += input.mouse.delta + } + } + // bar_pos = {0, 400} + + move_settings_spacer := ui_widget("Move-Settings Spacer", {}) + move_settings_spacer.text = str_intern("spacer") + move_settings_spacer.style.font_size = 10 + move_settings_spacer.style.bg_color = Color_Transparent + + // settings_btn : UI_Widget + { + settings_btn = ui_button("Settings Btn") + settings_btn.text = str_intern("Settings") + settings_btn.style.flags = { + .Scale_Width_By_Height_Ratio, + } + } + + // HBox layout calculation? + { + hb_space_ratio_move_box := 0.1 + hb_space_ratio_move_settings_spacer := 0.05 + hb_space_ratio_settings_btn := 1.0 + + style := & move_box.box.style + style.anchor.max.x = 0.9 + + style = & move_settings_spacer.box.style + style.anchor.min.x = 0.1 + style.anchor.max.x = 0.8 + + style = & settings_btn.box.style + style.anchor.min.x = 0.2 + style.anchor.max.x = 0.55 + } + } + + + @static settings_open := true + if settings_btn.left_clicked || settings_open + { + settings_open = true + + @static pos := Vec2 {0, 0} + @static size := Vec2 { 600, 800 } + resize_border_width : f32 = 10 + + // Prototype for a resize box + // Will surround one box with a resize borders + // All sides can have their borders toggled + resize_box := ui_widget("Settings Menu: Resize Box", {}) + { + using resize_box + style.pos = pos + style.alignment = { 0.5, 0.5 } + style.bg_color = {} + style.size = range2( size, {}) + } + ui_parent(resize_box) + + // Resize handles and corners + { + flags := UI_BoxFlags { .Mouse_Clickable, .Focusable } + + style_resize_width := UI_Style { + flags = { .Fixed_Width }, + size = range2({resize_border_width, 0}, {}), + bg_color = Color_ResizeHandle, + alignment = {0, 1}, + margins = { resize_border_width, resize_border_width, 0, 0 }, + } + style_resize_height := style_resize_width + style_resize_height.flags = {.Fixed_Height} + style_resize_height.size.min = {0, resize_border_width} + style_resize_height.margins = { 0, 0, resize_border_width, resize_border_width } + + ui_theme_via_style(style_resize_width) + left := ui_widget("Settings Menu: Resize Left Border", flags ) + right := ui_widget("Settings Menu: Resize Right Border", flags) + right.style.anchor.left = 1 + right.style.alignment = {1, 1} + + ui_theme_via_style(style_resize_height) + top := ui_widget("Settings Menu: Resize Top Border", flags ) + bottom := ui_widget("Settings Menu: Resize Bottom Border", flags) + bottom.style.anchor.top = 1 + bottom.style.alignment = {0, 0} + + style_corner := UI_Style { + flags = { .Fixed_Width, .Fixed_Height }, + size = range2({resize_border_width, resize_border_width}, {}), + bg_color = Color_Blue, + alignment = {0, 1} + } + ui_theme_via_style(style_corner) + corner_tl := ui_widget("Settings Menu: Corner TL", flags) + corner_tr := ui_widget("Settings Menu: Corner TR", flags) + corner_tr.style.anchor = range2({1, 0}, {}) + corner_tr.style.alignment = {1, 1} + + corner_bl := ui_widget("Settings Menu: Corner BL", flags) + corner_bl.style.anchor = range2({}, {0, 1}) + corner_bl.style.alignment = {} + corner_br := ui_widget("Settings Menu: Corner BR", flags) + corner_br.style.anchor = range2({1, 0}, {0, 1}) + corner_br.style.alignment = {1, 0} + + process_handle_drag :: #force_inline proc ( handle : ^UI_Widget, + side_scalar : f32, + size_axis : ^f32, + size_delta_axis : f32, + pos_axis : ^f32, + alignment_axis : ^f32, + alignment_while_dragging : f32 ) + { + if get_state().ui_context.last_pressed_key != handle.key { return } + + @static was_dragging := false + @static within_drag := false + + using handle.signal + if dragging + { + if ! was_dragging { + was_dragging = true + (pos_axis ^) += size_axis^ * 0.5 * side_scalar + } + (size_axis ^) += size_delta_axis * -side_scalar + (alignment_axis ^) = alignment_while_dragging + } + else if was_dragging && released + { + (pos_axis ^) += size_axis^ * 0.5 * -side_scalar + was_dragging = false + } + } + + process_handle_drag( & right , -1.0, & size.x, input.mouse.delta.x, & pos.x, & resize_box.style.alignment.x, 0) + process_handle_drag( & left, 1.0, & size.x, input.mouse.delta.x, & pos.x, & resize_box.style.alignment.x, 1) + process_handle_drag( & top, -1.0, & size.y, input.mouse.delta.y, & pos.y, & resize_box.style.alignment.y, 0) + process_handle_drag( & bottom, 1.0, & size.y, input.mouse.delta.y, & pos.y, & resize_box.style.alignment.y, 1) + + // process_corner_drag :: #force_inline proc() + { + + } + } + + settings_menu := ui_widget("Settings Menu", {}) + { + using settings_menu + style.alignment = { 0.0, 1.0 } + style.bg_color = Color_BG_Panel_Translucent + // style.border_width = 1.0 + // style.border_color = Color_Blue + style.margins = { + resize_border_width, + resize_border_width, + resize_border_width, + resize_border_width, } + } + ui_parent(settings_menu) + + ui_theme_via_style({ + bg_color = Color_Transparent, + font = default_font, + font_size = 16, + text_color = Color_White, + size = range2({0, 40}, {0, 40}) // TODO(Ed): Implment ratio scaling for height + }) + frame_bar := ui_widget("Settings Menu: Frame Bar", { .Mouse_Clickable, .Focusable, .Click_To_Focus }) + { + using frame_bar + style.bg_color = Color_BG_Panel + style.flags = {} + style.alignment = { 0, 1 } + // style.size = {} + style.anchor = range2( {0, 0.95}, {0, 0} ) + ui_parent(frame_bar) + + if dragging { + pos += input.mouse.delta + } + + title := ui_text("Settings Menu: Title", str_intern("Settings Menu")) + { + using title + style.alignment = {0, 1} + style.margins = { 0, 0, 15, 0} + style.text_alignment = {0.0 , 0.5} + } + + close_btn := ui_button("Settings Menu: Close Btn") + { + using close_btn + text = str_intern("close") + style.bg_color = Color_GreyRed + style.size.min = {50, 0} + style.anchor = range2( {1.0, 0}, {}) + style.alignment = {1, 1} + style.text_alignment = {0.5, 0.5} + if close_btn.pressed { + settings_open = false + } + } + } + } + } + } + //endregion App UI Tick + //region WorkspaceImgui Tick { profile("Workspace Imgui") @@ -240,180 +542,9 @@ update :: proc( delta_time : f64 ) -> b32 // test_text_box() // test_parenting( & default_layout, & frame_style_default ) // test_whitespace_ast( & default_layout, & frame_style_default ) - - /* - Prototype app menu - This is a menu bar for the app for now inside the same ui as the workspace's UI state - Eventually this will get moved out to its own UI state for the app itself. - */ - if true - { - fmt :: str_fmt_alloc - - @static bar_pos := Vec2 {} - bar_size := vec2( 400, 40 ) - - menu_bar : UI_Widget - { - theme := UI_Style { - flags = { - }, - bg_color = { 0, 0, 0, 30 }, - border_color = { 0, 0, 0, 200 }, - - font = default_font, - font_size = 12, - text_color = Color_White, - - layout = UI_Layout { - anchor = {}, - border_width = 1.0 * (1.0/cam.zoom), - pos = screen_to_world(bar_pos), - size = range2( bar_size * (1.0/cam.zoom), {}), - // padding = { 10, 10, 10, 10 }, - }, - } - ui_theme_via_style(theme) - menu_bar = ui_widget("App Menu Bar", UI_BoxFlags {} ) - } - // Setup Children - settings_btn : UI_Widget - { - ui_parent(menu_bar) - - style := UI_Style { - flags = { - // .Origin_At_Anchor_Center - .Fixed_Height - }, - bg_color = Color_Frame_Disabled, - - font = default_font, - font_size = 18 * (1.0/cam.zoom), - text_color = Color_White, - - layout = UI_Layout { - anchor = range2( {0, 0}, {0.0, 0} ), - alignment = { 0.0, 1.0 }, - text_alignment = { 0.5, 0.5 }, - pos = { 0, 0 }, - size = range2( {25, bar_size.y} * (1.0/cam.zoom), {0, 0}) - } - } - theme := UI_StyleTheme { styles = { - style, - style, - style, - style, - }} - theme.disabled.bg_color = Color_Frame_Disabled - theme.hot.bg_color = Color_White - theme.active.bg_color = Color_Frame_Select - ui_style_theme(theme) - - move_box : UI_Widget - { - move_box = ui_button("Move Box") - if move_box.dragging { - // bar_pos += mouse_world_delta() - bar_pos += state.input.mouse.delta - } - } - - move_settings_spacer := ui_widget("Move-Settings Spacer", {}) - move_settings_spacer.text = str_intern("") - move_settings_spacer.style.font_size = 10 * (1.0/cam.zoom) - move_settings_spacer.style.bg_color = Color_Transparent - - // settings_btn : UI_Widget - { - settings_btn = ui_button("Settings Btn") - settings_btn.text = str_intern("Settings") - settings_btn.style.flags = { - .Scale_Width_By_Height_Ratio, - } - } - - // HBox layout calculation? - { - hb_space_ratio_move_box := 0.1 - hb_space_ratio_move_settings_spacer := 0.05 - hb_space_ratio_settings_btn := 1.0 - - style := & move_box.box.style - style.anchor.max.x = 0.9 - - style = & move_settings_spacer.box.style - style.anchor.min.x = 0.1 - style.anchor.max.x = 0.8 - - style = & settings_btn.box.style - style.anchor.min.x = 0.2 - style.anchor.max.x = 0.55 - } - } - - @static settings_open := false - if settings_btn.left_clicked || settings_open - { - settings_open = true - - @static pos := Vec2 {0, 0} - - settings_menu := ui_widget("Settings Menu", { .Mouse_Clickable, .Focusable, .Click_To_Focus }) - settings_menu.style.pos = screen_to_world(pos) - settings_menu.style.size = range2( {600, 800} * (1/cam.zoom), {}) - settings_menu.style.text_alignment = {0, 0.0} - settings_menu.style.alignment = { 0.5, 0.5 } - settings_menu.style.bg_color = Color_Transparent - settings_menu.style.border_width = 1.0 * (1/cam.zoom) - settings_menu.style.border_color = Color_Blue - // settings_menu.style.padding = { 10, 10, 10, 10 } - - settings_menu.text = { fmt("%v", pos), {} } - settings_menu.text.runes = to_runes(settings_menu.text.str) - settings_menu.style.font_size = 16 * (1/cam.zoom) - - // pos.x += frametime_delta32() * 100 - if settings_menu.dragging { - pos += state.input.mouse.delta - // pos.x += frametime_delta32() * 1 - } - ui_parent(settings_menu) - - frame_bar := ui_widget("Settings Menu: Frame Bar", {}) - { - using frame_bar - // style.bg_color = Color_Red - style.flags = {} - style.alignment = { 0, 1 } - style.size = {} - style.anchor = range2( {0, 0.95}, {0, 0} ) - - // Close button - { - - } - } - } - } } //endregion Workspace Imgui Tick - //region App Screenspace Imgui Tick - { - profile("App Screenspace Imgui") - - ui_graph_build( & state.app_ui ) - ui := ui_context - - /* - Prototype app menu - TODO(Ed): Move it to here - */ - } - //endregion App Screenspace Imgui Tick - debug.last_mouse_pos = input.mouse.pos should_shutdown : b32 = ! cast(b32) rl.WindowShouldClose() diff --git a/code/ui.odin b/code/ui.odin index 822b599..767f416 100644 --- a/code/ui.odin +++ b/code/ui.odin @@ -69,6 +69,8 @@ UI_BoxFlag :: enum u64 { Pan_X, Pan_Y, + Screenspace, + Count, } UI_BoxFlags :: bit_set[UI_BoxFlag; u64] @@ -86,13 +88,6 @@ UI_Computed :: struct { text_size : Vec2, // Size of text within content } -UI_LayoutSide :: struct { - // using _ : struct { - top, bottom : UI_Scalar, - left, right : UI_Scalar, - // } -} - UI_Cursor :: struct { placeholder : int, } @@ -119,32 +114,6 @@ UI_ScalarConstraint :: struct { UI_Scalar2 :: [Axis2.Count]UI_Scalar -// Desiered constraints on the UI_Box. -UI_Layout :: struct { - anchor : Range2, - alignment : Vec2, - text_alignment : Vec2, - - border_width : UI_Scalar, - - margins : UI_LayoutSide, - padding : UI_LayoutSide, - - // TODO(Ed): We cannot support individual corners unless we add it to raylib (or finally change the rendering backend) - corner_radii : [Corner.Count]f32, - - // Position in relative coordinate space. - // If the box's flags has Fixed_Position, then this will be its aboslute position in the relative coordinate space - pos : Vec2, - - size : Range2, - - // TODO(Ed) : Should thsi just always be WS_Pos for workspace UI? - // (We can union either varient and just know based on checking if its the screenspace UI) - // If the box is a child of the root parent, its automatically in world space and thus will use the tile_pos. - // tile_pos : WS_Pos, -} - UI_Signal :: struct { cursor_pos : Vec2, drag_delta : Vec2, @@ -163,91 +132,6 @@ UI_Signal :: struct { commit : b8, } -UI_StyleFlag :: enum u32 { - - // Will perform scissor pass on children to their parent's bounds - // (Specified in the parent) - Clip_Children_To_Bounds, - - // Enforces the box will always remain in a specific position relative to the parent. - // Overriding the anchors and margins. - Fixed_Position_X, - Fixed_Position_Y, - - // Enforces box will always be within the bounds of the parent box. - Clamp_Position_X, - Clamp_Position_Y, - - // Enroces the widget will maintain its size reguardless of any constraints - // Will override parent constraints (use the size.min.xy to specify the width & height) - Fixed_Width, - Fixed_Height, - - // TODO(Ed): Implement this! - // Enforces the widget will have a width specified as a ratio of its height (use the size.min/max.x to specify the scalar) - // If you wish for the width to stay fixed couple with the Fixed_Width flag - Scale_Width_By_Height_Ratio, - // Enforces the widget will have a height specified as a ratio of its width (use the size.min/max.y to specify the scalar) - // If you wish for the height to stay fixed couple with the Fixed_Height flag - Scale_Height_By_Width_Ratio, - - // Sets the (0, 0) position of the child box to the parents anchor's center (post-margins bounds) - // By Default, the origin is at the top left of the anchor's bounds - Origin_At_Anchor_Center, - - // Will size the box to its text. (Padding & Margins will thicken ) - Size_To_Text, - Text_Wrap, - - Count, -} -UI_StyleFlags :: bit_set[UI_StyleFlag; u32] - -UI_StylePreset :: enum u32 { - Default, - Disabled, - Hot, - Active, - Count, -} - -UI_Style :: struct { - flags : UI_StyleFlags, - - bg_color : Color, - border_color : Color, - - // TODO(Ed) : Add support for this eventually - blur_size : f32, - - font : FontID, - // TODO(Ed): Should this get moved to the layout struct? Techncially font-size is mainly - font_size : f32, - text_color : Color, - - cursor : UI_Cursor, - - using layout : UI_Layout, - - // Used with style, prev_style, and style_delta to produce a simple interpolated animation - // Applied in the layout pass & the rendering pass for their associated fields. - transition_time : f32, -} - -UI_StyleTheme :: struct #raw_union { - array : [UI_StylePreset.Count] UI_Style, - using styles : struct { - default, disabled, hot, active : UI_Style, - } -} - -UI_TextAlign :: enum u32 { - Left, - Center, - Right, - Count -} - UI_Box :: struct { // Cache ID key : UI_Key, @@ -443,16 +327,16 @@ ui_box_tranverse_next :: proc "contextless" ( box : ^ UI_Box ) -> (^ UI_Box) ui_cursor_pos :: #force_inline proc "contextless" () -> Vec2 { using state := get_state() if ui_context == & state.project.workspace.ui { - return screen_to_world( input.mouse.pos ) + return surface_to_ws_view_pos( input.mouse.pos ) } else { return input.mouse.pos } } -ui_drag_delta :: #force_inline proc "contextless" () -> Vec2 { +ui_ws_drag_delta :: #force_inline proc "contextless" () -> Vec2 { using state := get_state() - return ui_cursor_pos() - state.ui_context.active_start_signal.cursor_pos + return surface_to_ws_view_pos(input.mouse.pos) - state.ui_context.active_start_signal.cursor_pos } ui_graph_build_begin :: proc( ui : ^ UI_State, bounds : Vec2 = {} ) @@ -481,19 +365,19 @@ ui_graph_build_begin :: proc( ui : ^ UI_State, bounds : Vec2 = {} ) } // TODO(Ed) :: Is this even needed? -ui_graph_build_end :: proc() +ui_graph_build_end :: proc( ui : ^UI_State ) { profile(#procedure) ui_parent_pop() // Should be ui_context.root // Regenerate the computed layout if dirty - ui_compute_layout() + ui_compute_layout( ui ) get_state().ui_context = nil } -@(deferred_none = ui_graph_build_end) +@(deferred_in = ui_graph_build_end) ui_graph_build :: proc( ui : ^ UI_State ) { ui_graph_build_begin( ui ) } @@ -520,10 +404,6 @@ ui_key_from_string :: #force_inline proc "contextless" ( value : string ) -> UI_ return key } -ui_layout_padding :: proc( pixels : f32 ) -> UI_LayoutSide { - return { pixels, pixels, pixels, pixels } -} - ui_parent_push :: proc( ui : ^ UI_Box ) { stack := & get_state().ui_context.parent_stack stack_push( & get_state().ui_context.parent_stack, ui ) @@ -542,54 +422,3 @@ ui_parent_pop :: proc() { ui_parent :: proc( ui : ^UI_Box) { ui_parent_push( ui ) } - -ui_style_peek :: proc( box_state : UI_StylePreset ) -> UI_Style { - return stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] -} - -ui_style_ref :: proc( box_state : UI_StylePreset ) -> (^ UI_Style) { - return & stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] -} - -ui_style_set :: proc ( style : UI_Style, box_state : UI_StylePreset ) { - stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] = style -} - -ui_style_set_layout :: proc ( layout : UI_Layout, preset : UI_StylePreset ) { - stack_peek_ref( & get_state().ui_context.theme_stack ).array[preset].layout = layout -} - -ui_style_theme_push :: proc( preset : UI_StyleTheme ) { - push( & get_state().ui_context.theme_stack, preset ) -} - -ui_style_theme_pop :: proc() { - pop( & get_state().ui_context.theme_stack ) -} - -@(deferred_none = ui_style_theme_pop) -ui_style_theme :: proc( preset : UI_StyleTheme ) { - ui_style_theme_push( preset ) -} - -@(deferred_none = ui_style_theme_pop) -ui_theme_via_style :: proc ( style : UI_Style ) { - ui_style_theme_push( UI_StyleTheme { styles = { style, style, style, style } }) -} - -ui_style_theme_set_layout :: proc ( layout : UI_Layout ) { - for & preset in stack_peek_ref( & get_state().ui_context.theme_stack ).array { - preset.layout = layout - } -} - -ui_style_theme_layout_push :: proc ( layout : UI_Layout ) { - ui := get_state().ui_context - ui_style_theme_push( stack_peek( & ui.theme_stack) ) - ui_style_theme_set_layout(layout) -} - -@(deferred_none = ui_style_theme_pop) -ui_style_theme_layout :: proc( layout : UI_Layout ) { - ui_style_theme_layout_push(layout) -} diff --git a/code/ui_layout.odin b/code/ui_layout.odin index 291f34e..fecc5cd 100644 --- a/code/ui_layout.odin +++ b/code/ui_layout.odin @@ -5,12 +5,12 @@ import "core:math/linalg" // Note(Ed): This is naturally pretty expensive -ui_compute_layout :: proc() +ui_compute_layout :: proc( ui : ^UI_State ) { profile(#procedure) state := get_state() - root := state.project.workspace.ui.root + root := ui.root { computed := & root.computed style := root.style @@ -172,7 +172,7 @@ ui_compute_layout :: proc() text_pos.y += ( content_size.y - text_size.y ) * layout.text_alignment.y computed.text_size = text_size - computed.text_pos = { text_pos.x, -text_pos.y } + computed.text_pos = { text_pos.x, text_pos.y } } current = ui_box_tranverse_next( current ) diff --git a/code/ui_style.odin b/code/ui_style.odin new file mode 100644 index 0000000..3d5a915 --- /dev/null +++ b/code/ui_style.odin @@ -0,0 +1,174 @@ +package sectr + +UI_LayoutSide :: struct { + // using _ : struct { + top, bottom : UI_Scalar, + left, right : UI_Scalar, + // } +} + +// Desiered constraints on the UI_Box. +UI_Layout :: struct { + anchor : Range2, + alignment : Vec2, + text_alignment : Vec2, + + border_width : UI_Scalar, + + margins : UI_LayoutSide, + padding : UI_LayoutSide, + + // TODO(Ed): We cannot support individual corners unless we add it to raylib (or finally change the rendering backend) + corner_radii : [Corner.Count]f32, + + // Position in relative coordinate space. + // If the box's flags has Fixed_Position, then this will be its aboslute position in the relative coordinate space + pos : Vec2, + + size : Range2, + + // TODO(Ed) : Should thsi just always be WS_Pos for workspace UI? + // (We can union either varient and just know based on checking if its the screenspace UI) + // If the box is a child of the root parent, its automatically in world space and thus will use the tile_pos. + // tile_pos : WS_Pos, +} + +UI_StyleFlag :: enum u32 { + + // Will perform scissor pass on children to their parent's bounds + // (Specified in the parent) + Clip_Children_To_Bounds, + + // Enforces the box will always remain in a specific position relative to the parent. + // Overriding the anchors and margins. + Fixed_Position_X, + Fixed_Position_Y, + + // Enforces box will always be within the bounds of the parent box. + Clamp_Position_X, + Clamp_Position_Y, + + // Enroces the widget will maintain its size reguardless of any constraints + // Will override parent constraints (use the size.min.xy to specify the width & height) + Fixed_Width, + Fixed_Height, + + // TODO(Ed): Implement this! + // Enforces the widget will have a width specified as a ratio of its height (use the size.min/max.x to specify the scalar) + // If you wish for the width to stay fixed couple with the Fixed_Width flag + Scale_Width_By_Height_Ratio, + // Enforces the widget will have a height specified as a ratio of its width (use the size.min/max.y to specify the scalar) + // If you wish for the height to stay fixed couple with the Fixed_Height flag + Scale_Height_By_Width_Ratio, + + // Sets the (0, 0) position of the child box to the parents anchor's center (post-margins bounds) + // By Default, the origin is at the top left of the anchor's bounds + Origin_At_Anchor_Center, + + // Will size the box to its text. (Padding & Margins will thicken ) + Size_To_Text, + Text_Wrap, + + Count, +} +UI_StyleFlags :: bit_set[UI_StyleFlag; u32] + +UI_StylePreset :: enum u32 { + Default, + Disabled, + Hot, + Active, + Count, +} + +UI_Style :: struct { + flags : UI_StyleFlags, + + bg_color : Color, + border_color : Color, + + // TODO(Ed) : Add support for this eventually + blur_size : f32, + + font : FontID, + // TODO(Ed): Should this get moved to the layout struct? Techncially font-size is mainly + font_size : f32, + text_color : Color, + + cursor : UI_Cursor, + + using layout : UI_Layout, + + // Used with style, prev_style, and style_delta to produce a simple interpolated animation + // Applied in the layout pass & the rendering pass for their associated fields. + transition_time : f32, +} + +UI_StyleTheme :: struct #raw_union { + array : [UI_StylePreset.Count] UI_Style, + using styles : struct { + default, disabled, hot, active : UI_Style, + } +} + +UI_TextAlign :: enum u32 { + Left, + Center, + Right, + Count +} + +ui_layout_padding :: proc( pixels : f32 ) -> UI_LayoutSide { + return { pixels, pixels, pixels, pixels } +} + +ui_style_peek :: proc( box_state : UI_StylePreset ) -> UI_Style { + return stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] +} + +ui_style_ref :: proc( box_state : UI_StylePreset ) -> (^ UI_Style) { + return & stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] +} + +ui_style_set :: proc ( style : UI_Style, box_state : UI_StylePreset ) { + stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] = style +} + +ui_style_set_layout :: proc ( layout : UI_Layout, preset : UI_StylePreset ) { + stack_peek_ref( & get_state().ui_context.theme_stack ).array[preset].layout = layout +} + +ui_style_theme_push :: proc( preset : UI_StyleTheme ) { + push( & get_state().ui_context.theme_stack, preset ) +} + +ui_style_theme_pop :: proc() { + pop( & get_state().ui_context.theme_stack ) +} + +@(deferred_none = ui_style_theme_pop) +ui_style_theme :: proc( preset : UI_StyleTheme ) { + ui_style_theme_push( preset ) +} + +@(deferred_none = ui_style_theme_pop) +ui_theme_via_style :: proc ( style : UI_Style ) { + ui_style_theme_push( UI_StyleTheme { styles = { style, style, style, style } }) +} + +ui_style_theme_set_layout :: proc ( layout : UI_Layout ) { + for & preset in stack_peek_ref( & get_state().ui_context.theme_stack ).array { + preset.layout = layout + } +} + +ui_style_theme_layout_push :: proc ( layout : UI_Layout ) { + ui := get_state().ui_context + ui_style_theme_push( stack_peek( & ui.theme_stack) ) + ui_style_theme_set_layout(layout) +} + +@(deferred_none = ui_style_theme_pop) +ui_style_theme_layout :: proc( layout : UI_Layout ) { + ui_style_theme_layout_push(layout) +} diff --git a/code/ui_tests.odin b/code/ui_tests.odin index f97daba..33a2b3f 100644 --- a/code/ui_tests.odin +++ b/code/ui_tests.odin @@ -66,6 +66,9 @@ test_draggable :: proc() draggable.style.layout.pos = debug.draggable_box_pos draggable.style.layout.size.min = debug.draggable_box_size + + draggable.text = { str_fmt_alloc("%v", debug.draggable_box_pos), {} } + draggable.text.runes = to_runes(draggable.text.str) } test_parenting :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_Style ) diff --git a/ols.json b/ols.json index 5f02641..d789d58 100644 --- a/ols.json +++ b/ols.json @@ -1,18 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", "collections": [ - { - "name": "core", - "path": "C:/projects/SectrPrototype/toolchain/Odin/core" - }, - { - "name": "vendor", - "path": "C:/projects/SectrPrototype/toolchain/Odin/vendor" - }, - { - "name": "code", - "path": "C:/projects/SectrPrototype/code" - }, { "name": "thirdparty", "path": "C:/projects/SectrPrototype/thirdparty"