diff --git a/Readme.md b/Readme.md index 5eaf510..a8db3ea 100644 --- a/Readme.md +++ b/Readme.md @@ -21,24 +21,34 @@ The dependencies are: * Odin repo's base, core, and vendor(raylib) libaries * An ini parser -The client(sectr) module's organization is relatively flat due to the nature of odin's module suste, not allowing for cyclic dependencies across modules, and modules can only be in one directory. +The client(sectr) module's organization is relatively flat due to the nature of odin's compiler, not allowing for cyclic dependencies across modules, and modules can only be in one directory. This makes it difficult to unflatten, not something organic todo in a prototype... -Even so the notatble groups are: +Even so the notable groups are: * API : Provides the overarching interface of the app's general behavior. Host uses this to provide the client its necessary data and exection env. * Has the following definitions: startup, shutdown, reload, tick, clean_frame +* Env : Core Memory & State definition + orchestration * Grime : Name speaks for itself, stuff not directly related to the target features to iterate upon for the prototype. * Defining dependency aliases or procedure overload tables, rolling own allocator, data structures, etc. * Font Provider : Manages fonts. * When loading fonts, the provider currently uses raylib to generate bitmap glyth sheets for a range of font sizes at once. * Goal is to eventually render using SDF shaders. -* Input : Standard input pooling and related features. Platform abstracted via raylib for now. +* Input : All human input related features + * Base input features (polling & related) are platform abstracted from raylib + * Input Events * Parser : AST generation, editing, and serialization. A 1/3 of this prototype will most likely be this alone. -* UI : AST visualzation & editing, backend visualization, project organizationa via workspaces (2d cavnases) +* Project : Encpasulation of user config/state separate from persistent app config/state as a 'project' + * Manages the codebase (program model database) + * Manages workspaces : View compositions of the codebase +* UI : Core graphic user interface framework, AST visualzation & editing, backend visualization * Will most likely be the bulk of this prototype. * PIMGUI (Persistent Immediate Mode User Interface) - * Auto-layout with heavy procedural generation of box widgets + * Auto-layout + * Supports heavy procedural generation of box widgets + +Due to the nature of the prototype there are 'sub-groups' such as the codebase being its own ordeal as well as the workspace. +They'll be elaborated in their own documentation There is some unused code in `code/__imgui_raddbg`. Its a partial translation of some data structures from raddbg's ui. @@ -47,3 +57,4 @@ There is some unused code in `code/__imgui_raddbg`. Its a partial translation of ![img](docs/assets/sectr_host_2024-03-09_04-30-27.png) ![img](docs/assets/sectr_host_2024-05-04_12-29-39.png) ![img](docs/assets/Code_2024-05-04_12-55-53.png) +![img](docs/assets/sectr_host_2024-05-11_22-34-15.png) diff --git a/code/env.odin b/code/env.odin index 71b8cf2..78887bd 100644 --- a/code/env.odin +++ b/code/env.odin @@ -133,6 +133,7 @@ MemoryConfig :: struct { commit_initial_filebuffer : uint, } +// ALl nobs available for this application AppConfig :: struct { using memory : MemoryConfig, @@ -154,6 +155,39 @@ AppConfig :: struct { ui_resize_border_width : f32, } +AppWindow :: struct { + extent : Extents2, // Window half-size + dpi_scale : f32, // Dots per inch scale (provided by raylib via glfw) + ppcm : f32, // Dots per centimetre +} + +FontData :: struct { + provider : FontProviderData, + + // TODO(Ed): We can have font constants here I guess but eventually + // I rather have fonts configurable for a 'theme' combo + // So that way which IDs are picked depends on runtime + firacode : FontID, + squidgy_slimes : FontID, + rec_mono_semicasual_reg : FontID, + + default_font : FontID, +} + +FrameTime :: struct { + sleep_is_granular : b32, + + delta_seconds : f64, + delta_ms : f64, + delta_ns : Duration, + target_ms : f64, + elapsed_ms : f64, + avg_ms : f64, + fps_avg : f64, +} + +// Global Singleton stored in the persistent virtual arena, the first allocated data. +// Use get_state() to conviently retrieve at any point for the program's lifetime State :: struct { default_slab_policy : SlabPolicy, persistent_slab : Slab, @@ -165,8 +199,6 @@ State :: struct { string_cache : StringCache, - font_provider_data : FontProviderData, - input_data : [2]InputState, input_prev : ^InputState, input : ^InputState, @@ -182,6 +214,7 @@ State :: struct { monitor_id : i32, monitor_refresh_hz : i32, + // using frametime : FrameTime, sleep_is_granular : b32, frametime_delta_seconds : f64, @@ -192,6 +225,9 @@ State :: struct { frametime_avg_ms : f64, fps_avg : f64, + // fonts : FontData, + font_provider_data : FontProviderData, + font_firacode : FontID, font_squidgy_slimes : FontID, font_rec_mono_semicasual_reg : FontID, @@ -206,90 +242,10 @@ State :: struct { cam_context : Camera, } -get_state :: proc "contextless" () -> ^ State { +get_state :: #force_inline proc "contextless" () -> ^ State { return cast( ^ State ) Memory_App.persistent.reserve_start } -AppWindow :: struct { - extent : Extents2, // Window half-size - dpi_scale : f32, // Dots per inch scale (provided by raylib via glfw) - ppcm : f32, // Dots per centimetre -} - -// PMDB -CodeBase :: struct { - placeholder : int, -} - -ProjectConfig :: struct { - placeholder : int, -} - -Project :: struct { - path : StrRunesPair, - name : StrRunesPair, - - config : ProjectConfig, - codebase : CodeBase, - - // TODO(Ed) : Support multiple workspaces - workspace : Workspace, -} - -Frame :: struct -{ - pos : Vec2, - size : Vec2, - - ui : ^UI_Box, -} - -Workspace :: struct { - name : StrRunesPair, - - cam : Camera, - zoom_target : f32, - - frames : Array(Frame), - - test_frame : Frame, - - // TODO(Ed) : The workspace is mainly a 'UI' conceptually... - ui : UI_State, -} - -DebugData :: struct { - square_size : i32, - square_pos : rl.Vector2, - - draw_debug_text_y : f32, - - cursor_locked : b32, - cursor_unlock_pos : Vec2, // Raylib changes the mose position on lock, we want restore the position the user would be in on screen - mouse_vis : b32, - last_mouse_pos : Vec2, - - // UI Vis - draw_ui_box_bounds_points : bool, - draw_ui_margin_bounds : bool, - draw_ui_anchor_bounds : bool, - draw_UI_padding_bounds : bool, - draw_ui_content_bounds : bool, - - // Test First - frame_2_created : b32, - - // Test Draggable - draggable_box_pos : Vec2, - draggable_box_size : Vec2, - box_original_size : Vec2, - - // Test parsing - path_lorem : string, - lorem_content : []byte, - lorem_parse : PWS_ParseResult, - - // Test 3d Viewport - cam_vp : rl.Camera3D, - viewport_rt : rl.RenderTexture, -} +// get_frametime :: #force_inline proc "contextless" () -> FrameTime { +// return get_state().frametime +// } diff --git a/code/env_scratch.odin b/code/env_scratch.odin new file mode 100644 index 0000000..196a2b8 --- /dev/null +++ b/code/env_scratch.odin @@ -0,0 +1,41 @@ +package sectr + +// Scratch space + +import rl "vendor:raylib" + +DebugData :: struct { + square_size : i32, + square_pos : rl.Vector2, + + draw_debug_text_y : f32, + + cursor_locked : b32, + cursor_unlock_pos : Vec2, // Raylib changes the mose position on lock, we want restore the position the user would be in on screen + mouse_vis : b32, + last_mouse_pos : Vec2, + + // UI Vis + draw_ui_box_bounds_points : bool, + draw_ui_margin_bounds : bool, + draw_ui_anchor_bounds : bool, + draw_UI_padding_bounds : bool, + draw_ui_content_bounds : bool, + + // Test First + frame_2_created : b32, + + // Test Draggable + draggable_box_pos : Vec2, + draggable_box_size : Vec2, + box_original_size : Vec2, + + // Test parsing + path_lorem : string, + lorem_content : []byte, + lorem_parse : PWS_ParseResult, + + // Test 3d Viewport + cam_vp : rl.Camera3D, + viewport_rt : rl.RenderTexture, +} \ No newline at end of file diff --git a/code/grime.odin b/code/grime.odin index d128778..60c5e96 100644 --- a/code/grime.odin +++ b/code/grime.odin @@ -268,9 +268,30 @@ to_writer :: proc { str_builder_to_writer, } -ui_set_layout :: proc { - ui_style_set_layout, - ui_style_theme_set_layout, +ui_layout_push :: proc { + ui_layout_push_layout, + ui_layout_push_theme, +} + +ui_layout :: proc { + ui_layout_via_layout, + ui_layout_via_combo, +} + +ui_style_push :: proc { + ui_style_push_style, + ui_style_push_combo, +} + +ui_style :: proc { + ui_style_via_style, + ui_style_via_combo, +} + +ui_theme :: proc { + ui_theme_via_layout_style, + ui_theme_via_combos, + ui_theme_via_theme, } wedge :: proc { diff --git a/code/grime_ptr.odin b/code/grime_ptr.odin new file mode 100644 index 0000000..02f3acd --- /dev/null +++ b/code/grime_ptr.odin @@ -0,0 +1,30 @@ +package sectr + +// Provides an alternative syntax for pointers + +Ptr :: struct( $ Type : typeid ) { + v : Type, +} + +exmaple_ptr :: proc() +{ + a, b : int + var : ^Ptr(int) + reg : ^int + + a = 1 + b = 1 + + var = &{a} + var.v = 2 + var = &{b} + var.v = 3 + + a = 1 + b = 1 + + reg = (& a) + (reg^) = 2 + reg = (& b) + (reg^) = 3 +} diff --git a/code/project.odin b/code/project.odin new file mode 100644 index 0000000..c02b231 --- /dev/null +++ b/code/project.odin @@ -0,0 +1,26 @@ +package sectr + +/* +Project: Encapsulation of all things a user can do separate from the core app behavior +that is managed independetly of it. +*/ + +// PMDB +CodeBase :: struct { + placeholder : int, +} + +ProjectConfig :: struct { + placeholder : int, +} + +Project :: struct { + path : StrRunesPair, + name : StrRunesPair, + + config : ProjectConfig, + codebase : CodeBase, + + // TODO(Ed) : Support multiple workspaces + workspace : Workspace, +} diff --git a/code/project_workspace.odin b/code/project_workspace.odin new file mode 100644 index 0000000..0eead14 --- /dev/null +++ b/code/project_workspace.odin @@ -0,0 +1,38 @@ +/* +Workspace : A canvas for compositoning a view for the codebase along with notes. + +Each workspace viewport supports both a canvas composition of code frames +or frame tiling towards the application's screenspace. +*/ +package sectr + +Workspace :: struct { + name : StrRunesPair, + + cam : Camera, + zoom_target : f32, + + frames : Array(Frame), + + test_frame : Frame, + + // TODO(Ed) : The workspace is mainly a 'UI' conceptually... + ui : UI_State, +} + +// Top level widgets for the workspace +Frame :: struct { + pos : Vec2, + size : Vec2, + + ui : UI_Widget, +} + +CodeFrame :: struct { + readonly : b32, // Should this frame allow editing? + +} + +NoteFrame :: struct { + +} diff --git a/code/tick_render.odin b/code/tick_render.odin index b95af9f..a5cae05 100644 --- a/code/tick_render.odin +++ b/code/tick_render.odin @@ -4,18 +4,20 @@ 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 ) +draw_rectangle :: #force_inline proc "contextless" ( rect : rl.Rectangle, box : ^UI_Box ) { + using box + if style.corner_radii[0] > 0 { + rl.DrawRectangleRounded( rect, style.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 ) +draw_rectangle_lines :: #force_inline proc "contextless" ( rect : rl.Rectangle, box : ^UI_Box, color : Color, thickness : f32 ) { + using box + if style.corner_radii[0] > 0 { + rl.DrawRectangleRoundedLines( rect, style.corner_radii[0], 9, thickness, color ) } else { rl.DrawRectangleLinesEx( rect, thickness, color ) @@ -119,6 +121,7 @@ render_mode_2d_workspace :: proc() // profile("Box") parent := current.parent + layout := current.layout style := current.style computed := & current.computed @@ -128,18 +131,17 @@ render_mode_2d_workspace :: proc() continue } - // TODO(Ed) : Render Borders // profile_begin("Calculating Raylib rectangles") - render_anchors := range2( - ws_view_to_render_pos(computed.anchors.min), - ws_view_to_render_pos(computed.anchors.max), - ) - render_margins := range2( - ws_view_to_render_pos(computed.margins.min), - ws_view_to_render_pos(computed.margins.max), - ) + // render_anchors := range2( + // ws_view_to_render_pos(computed.anchors.min), + // ws_view_to_render_pos(computed.anchors.max), + // ) + // render_margins := range2( + // ws_view_to_render_pos(computed.margins.min), + // ws_view_to_render_pos(computed.margins.max), + // ) render_bounds := range2( ws_view_to_render_pos(computed.bounds.min), ws_view_to_render_pos(computed.bounds.max), @@ -153,8 +155,8 @@ render_mode_2d_workspace :: proc() ws_view_to_render_pos(computed.content.max), ) - rect_anchors := range2_to_rl_rect( render_anchors ) - rect_margins := range2_to_rl_rect( render_margins ) + // 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 ) @@ -163,10 +165,10 @@ render_mode_2d_workspace :: proc() // 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 ) + draw_rectangle( rect_bounds, current ) } - if style.border_width > 0 { - draw_rectangle_lines( rect_bounds, style, style.border_color, style.border_width ) + if layout.border_width > 0 { + draw_rectangle_lines( rect_bounds, current, style.border_color, layout.border_width ) } // profile_end() @@ -174,10 +176,10 @@ render_mode_2d_workspace :: proc() // 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 ) + draw_rectangle_lines( rect_padding, current, Color_Debug_UI_Padding_Bounds, line_thickness ) } else { - draw_rectangle_lines( rect_content, style, Color_Debug_UI_Content_Bounds, line_thickness ) + draw_rectangle_lines( rect_content, current, Color_Debug_UI_Content_Bounds, line_thickness ) } // profile_end() @@ -195,7 +197,7 @@ render_mode_2d_workspace :: proc() // profile_end() if len(current.text.str) > 0 { - ws_view_draw_text( current.text, ws_view_to_render_pos(computed.text_pos * {1, -1}), style.layout.font_size, style.text_color ) + ws_view_draw_text( current.text, ws_view_to_render_pos(computed.text_pos * {1, -1}), layout.font_size, style.text_color ) } } } @@ -338,18 +340,19 @@ render_screen_ui :: proc() parent := current.parent style := current.style + layout := current.layout computed := & current.computed computed_size := computed.bounds.p1 - computed.bounds.p0 - render_anchors := range2( - screen_to_render_pos(computed.anchors.min), - screen_to_render_pos(computed.anchors.max), - ) - render_margins := range2( - screen_to_render_pos(computed.margins.min), - screen_to_render_pos(computed.margins.max), - ) + // render_anchors := range2( + // screen_to_render_pos(computed.anchors.min), + // screen_to_render_pos(computed.anchors.max), + // ) + // render_margins := range2( + // screen_to_render_pos(computed.margins.min), + // screen_to_render_pos(computed.margins.max), + // ) render_bounds := range2( screen_to_render_pos(computed.bounds.min), screen_to_render_pos(computed.bounds.max), @@ -362,8 +365,8 @@ render_screen_ui :: proc() screen_to_render_pos(computed.content.min), screen_to_render_pos(computed.content.max), ) - rect_anchors := range2_to_rl_rect( render_anchors ) - rect_margins := range2_to_rl_rect( render_margins ) + // 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 ) @@ -372,10 +375,10 @@ render_screen_ui :: proc() // 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 ) + draw_rectangle( rect_bounds, current ) } - if style.border_width > 0 { - draw_rectangle_lines( rect_bounds, style, style.border_color, style.border_width ) + if layout.border_width > 0 { + draw_rectangle_lines( rect_bounds, current, style.border_color, layout.border_width ) } // profile_end() @@ -383,34 +386,34 @@ render_screen_ui :: proc() // 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 ) + draw_rectangle_lines( rect_padding, current, Color_Debug_UI_Padding_Bounds, line_thickness ) } else { - // draw_rectangle_lines( rect_content, style, Color_Debug_UI_Content_Bounds, line_thickness ) + draw_rectangle_lines( rect_content, current, 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 })) + // 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 ) - } + // 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, current, Color_Red, line_thickness ) + // } point_radius : f32 = 3 @@ -421,12 +424,12 @@ render_screen_ui :: proc() // } // 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 ) + 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 && style.font.key != 0 { - draw_text_screenspace( current.text, screen_to_render_pos(computed.text_pos), style.layout.font_size, style.text_color ) + draw_text_screenspace( current.text, screen_to_render_pos(computed.text_pos), layout.font_size, style.text_color ) } } } diff --git a/code/tick_update.odin b/code/tick_update.odin index 1388bff..145b33b 100644 --- a/code/tick_update.odin +++ b/code/tick_update.odin @@ -175,8 +175,8 @@ update :: proc( delta_time : f64 ) -> b32 } move_velocity : Vec2 = { - - cast(f32) i32(debug_actions.cam_move_left) + cast(f32) i32(debug_actions.cam_move_right), - - cast(f32) i32(debug_actions.cam_move_up) + cast(f32) i32(debug_actions.cam_move_down), + + cast(f32) i32(debug_actions.cam_move_left) - cast(f32) i32(debug_actions.cam_move_right), + + cast(f32) i32(debug_actions.cam_move_up) - cast(f32) i32(debug_actions.cam_move_down), } move_velocity *= digital_move_speed * f32(delta_time) cam.target += move_velocity @@ -203,13 +203,14 @@ update :: proc( delta_time : f64 ) -> b32 ui_graph_build( & state.project.workspace.ui ) ui := ui_context - frame_style_flags : UI_StyleFlags = { + frame_style_flags : UI_LayoutFlags = { .Fixed_Position_X, .Fixed_Position_Y, .Fixed_Width, .Fixed_Height, } default_layout := UI_Layout { + flags = frame_style_flags, anchor = {}, - alignment = { 0., 0.0 }, + alignment = { 0.0, 0.0 }, font_size = 30, text_alignment = { 0.0, 0.0 }, // corner_radii = { 0.2, 0.2, 0.2, 0.2 }, @@ -217,27 +218,24 @@ update :: proc( delta_time : f64 ) -> b32 size = range2( { 1000, 1000 }, {}), // padding = { 20, 20, 20, 20 } } - + ui_layout( default_layout ) frame_style_default := UI_Style { - flags = frame_style_flags, bg_color = Color_BG_TextBox, font = default_font, text_color = Color_White, - - layout = default_layout, } - - frame_theme := to_ui_styletheme(frame_style_default) + frame_theme := to_ui_style_combo(frame_style_default) frame_theme.disabled.bg_color = Color_Frame_Disabled frame_theme.hot. bg_color = Color_Frame_Hover frame_theme.active. bg_color = Color_Frame_Select - ui_style_theme( frame_theme ) + ui_style( frame_theme ) config.ui_resize_border_width = 2.5 + // test_hover_n_click() // test_draggable() // test_text_box() // test_parenting( & default_layout, & frame_style_default ) - // test_whitespace_ast( & default_layout, & frame_style_default ) + test_whitespace_ast( & default_layout, & frame_style_default ) } //endregion Workspace Imgui Tick diff --git a/code/ui.odin b/code/ui.odin index 4700828..369f7d3 100644 --- a/code/ui.odin +++ b/code/ui.odin @@ -76,19 +76,6 @@ UI_BoxFlag :: enum u64 { UI_BoxFlags :: bit_set[UI_BoxFlag; u64] UI_BoxFlag_Scroll :: UI_BoxFlags { .Scroll_X, .Scroll_Y } -// The UI_Box's actual positioning and sizing -// There is an excess of rectangles here for debug puproses. -UI_Computed :: struct { - fresh : b32, // If the auto-layout has been computed for the current frame - anchors : Range2, // Bounds for anchors within parent - margins : Range2, // Bounds for margins within parent - bounds : Range2, // Bounds for box itself - padding : Range2, // Bounds for padding's starting bounds (will be offset by border if there is one) - content : Range2, // Bounds for content (text or children) - text_pos : Vec2, // Position of text within content - text_size : Vec2, // Size of text within content -} - UI_Cursor :: struct { placeholder : int, } @@ -125,10 +112,16 @@ UI_Box :: struct { // Regenerated per frame. using links : DLL_NodeFull( UI_Box ), // first, last, prev, next parent : ^UI_Box, - num_children : i32, + num_children : i16, + ancestors : i16, + parent_index : i16, + + flags : UI_BoxFlags, + computed : UI_Computed, + + prev_layout : UI_Layout, + layout : UI_Layout, - flags : UI_BoxFlags, - computed : UI_Computed, prev_style : UI_Style, style : UI_Style, @@ -139,8 +132,6 @@ UI_Box :: struct { disabled_delta : f32, style_delta : f32, - parent_index : i32, - // prev_computed : UI_Computed, // prev_style : UI_Style,v // mouse : UI_InteractState, @@ -172,8 +163,9 @@ UI_State :: struct { layout_dirty : b32, // TODO(Ed) : Look into using a build arena like Ryan does for these possibly (and thus have a linked-list stack) - theme_stack : StackFixed( UI_StyleTheme, UI_Style_Stack_Size ), - parent_stack : StackFixed( ^UI_Box, UI_Parent_Stack_Size ), + layout_combo_stack : StackFixed( UI_LayoutCombo, UI_Style_Stack_Size ), + style_combo_stack : StackFixed( UI_StyleCombo, UI_Style_Stack_Size ), + parent_stack : StackFixed( ^UI_Box, UI_Parent_Stack_Size ), // flag_stack : Stack( UI_BoxFlags, UI_BoxFlags_Stack_Size ), hot : UI_Key, @@ -192,7 +184,7 @@ UI_State :: struct { last_pressed_key_us : [MouseBtn.count] f32, } -ui_startup :: proc( ui : ^ UI_State, cache_allocator : Allocator ) +ui_startup :: proc( ui : ^ UI_State, cache_allocator : Allocator /* , cache_reserve_size : u64 */ ) { ui := ui ui^ = {} @@ -283,6 +275,7 @@ ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box) curr_box.parent_index = parent.num_children parent.num_children += 1 curr_box.parent = parent + curr_box.ancestors = parent.ancestors + 1 } ui.built_box_count += 1 @@ -416,3 +409,5 @@ ui_top_ancestor :: #force_inline proc "contextless" ( box : ^UI_Box ) -> (^UI_Bo for ; ancestor.parent != root; ancestor = ancestor.parent {} return ancestor } + +ui_context :: #force_inline proc() -> ^UI_State { return get_state().ui_context } diff --git a/code/ui_layout.odin b/code/ui_layout.odin index 39ba006..f6db43f 100644 --- a/code/ui_layout.odin +++ b/code/ui_layout.odin @@ -3,182 +3,145 @@ package sectr import "core:math" import "core:math/linalg" -// Note(Ed): This is naturally pretty expensive +// The UI_Box's actual positioning and sizing +// There is an excess of rectangles here for debug puproses. +UI_Computed :: struct { + fresh : b32, // If the auto-layout has been computed for the current frame + // anchors : Range2, // Bounds for anchors within parent + // margins : Range2, // Bounds for margins within parent + bounds : Range2, // Bounds for box itself + padding : Range2, // Bounds for padding's starting bounds (will be offset by border if there is one) + content : Range2, // Bounds for content (text or children) + text_pos : Vec2, // Position of text within content + text_size : Vec2, // Size of text within content +} -ui_compute_layout :: proc( ui : ^UI_State ) -{ - profile(#procedure) - state := get_state() +UI_LayoutDirectionX :: enum(i32) { + Left_To_Right, + Right_To_Left, +} - root := ui.root - { - computed := & root.computed - style := root.style - layout := & style.layout - computed.bounds.min = layout.pos - computed.bounds.max = layout.size.min - computed.content = computed.bounds - } +UI_LayoutDirectionY :: enum(i32) { + Top_To_Bottom, + Bottom_To_Top, +} - current := root.first - for ; current != nil; - { - // if current.computed.fresh do return +UI_LayoutSide :: struct { + // using _ : struct { + top, bottom : UI_Scalar, + left, right : UI_Scalar, + // } +} - // TODO(Ed): Lift this to ui_box_compute_layout - // profile("Layout Box") - style := current.style +UI_LayoutFlag :: enum u32 { - // These are used to choose via multiplication weather to apply - // position & size constraints of the parent. - // The parent's unadjusted content bounds however are enforced for position, - // they cannot be ignored. The user may bypass them by doing the - // relative offset math vs world/screen space if they desire. - fixed_pos_x : f32 = cast(f32) int(.Fixed_Position_X in style.flags) - fixed_pos_y : f32 = cast(f32) int(.Fixed_Position_Y in style.flags) - fixed_width : f32 = cast(f32) int(.Fixed_Width in style.flags) - fixed_height : f32 = cast(f32) int(.Fixed_Height in style.flags) + // Will perform scissor pass on children to their parent's bounds + // (Specified in the parent) + Clip_Children_To_Bounds, - size_to_text : bool = .Size_To_Text in style.flags + // 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, - parent := current.parent - computed := & current.computed + // Enforces box will always be within the bounds of the parent box. + Clamp_Position_X, + Clamp_Position_Y, - parent_content := parent.computed.content - parent_content_size := parent_content.max - parent_content.min - parent_center := parent_content.min + parent_content_size * 0.5 + // 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, - layout := & style.layout + // 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, - /* - If fixed position (X or Y): - * Ignore Margins - * Ignore Anchors + // 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, - If clampped position (X or Y): - * Positon cannot exceed the anchors/margins bounds. + // TODO(Ed): Implement this! + // For this to work, the children must have a minimum size set & their size overall must be greater than the parent's minimum size + Size_To_Content, - If fixed size (X or Y): - * Ignore Parent constraints (can only be clipped) + // Will size the box to its text. + Size_To_Text, - If auto-sized: - * Enforce parent size constraint of bounds relative to - where the adjusted content bounds are after applying margins & anchors. - The 'side' conflicting with the bounds will end at that bound side instead of clipping. + // TODO(Ed): Implement this! + Text_Wrap, - If size.min is not 0: - * Ignore parent constraints if the bounds go below that value. + Count, +} +UI_LayoutFlags :: bit_set[UI_LayoutFlag; u32] - If size.max is 0: - * Allow the child box to spread to entire adjusted content bounds, otherwise clampped to max size. - */ +// Used within UI_Box, provides the layout (spacial constraints & specification) of the widget and +UI_Layout :: struct { + flags : UI_LayoutFlags, + anchor : Range2, + alignment : Vec2, + text_alignment : Vec2, - // 1. Anchors - anchor := & layout.anchor - anchored_bounds := range2( - parent_content.min + parent_content_size * anchor.min, - parent_content.max - parent_content_size * anchor.max, - ) - // anchored_bounds_origin := (anchored_bounds.min + anchored_bounds.max) * 0.5 + font_size : f32, - // 2. Apply Margins - margins := range2( - { layout.margins.left, layout.margins.bottom }, - { layout.margins.right, layout.margins.top }, - ) - margined_bounds := range2( - anchored_bounds.min + margins.min, - anchored_bounds.max - margins.max, - ) - margined_bounds_origin := (margined_bounds.min + margined_bounds.max) * 0.5 - margined_size := margined_bounds.max - margined_bounds.min + margins : UI_LayoutSide, + padding : UI_LayoutSide, - // 3. Enforce Min/Max Size Constraints - adjusted_max_size_x := layout.size.max.x > 0 ? min( margined_size.x, layout.size.max.x ) : margined_size.x - adjusted_max_size_y := layout.size.max.y > 0 ? min( margined_size.y, layout.size.max.y ) : margined_size.y + border_width : UI_Scalar, - adjusted_size : Vec2 - adjusted_size.x = max( adjusted_max_size_x, layout.size.min.x) - adjusted_size.y = max( adjusted_max_size_y, layout.size.min.y) + // 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, - if .Fixed_Width in style.flags { - adjusted_size.x = layout.size.min.x - } - if .Fixed_Height in style.flags { - adjusted_size.y = layout.size.min.y - } + // 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, - text_size : Vec2 - if style.layout.font_size == computed.text_size.y { - text_size = computed.text_size - } - else { - text_size = cast(Vec2) measure_text_size( current.text.str, style.font, style.layout.font_size, 0 ) - } + transition_time : f32, +} - if size_to_text { - adjusted_size = text_size - } - - // 5. Determine relative position - - origin_center := margined_bounds_origin - origin_top_left := Vec2 { margined_bounds.min.x, margined_bounds.max.y } - - origin := .Origin_At_Anchor_Center in style.flags ? origin_center : origin_top_left - - rel_pos := origin + layout.pos - - if .Fixed_Position_X in style.flags { - rel_pos.x = origin.x + layout.pos.x - } - if .Fixed_Position_Y in style.flags { - rel_pos.y = origin.y + layout.pos.y - } - - vec2_one := Vec2 { 1, 1 } - - // 6. Determine the box bounds - // Adjust Alignment of pivot position - alignment := layout.alignment - bounds := range2( - rel_pos - adjusted_size * alignment, - rel_pos + adjusted_size * (vec2_one - alignment), - ) - - // Determine Padding's outer bounds - border_offset := Vec2 { layout.border_width, layout.border_width } - - padding_bounds := range2( - bounds.min + border_offset, - bounds.min - border_offset, - ) - - // Determine Content Bounds - content_bounds := range2( - bounds.min + { layout.padding.left, layout.padding.bottom } + border_offset, - bounds.max - { layout.padding.right, layout.padding.top } - border_offset, - ) - - computed.anchors = anchored_bounds - computed.margins = margined_bounds - computed.bounds = bounds - computed.padding = padding_bounds - computed.content = content_bounds - - if len(current.text.str) > 0 - { - content_size := content_bounds.max - content_bounds.min - text_pos : Vec2 - text_pos = content_bounds.min + { 0, text_size.y } - text_pos.x += ( content_size.x - text_size.x ) * layout.text_alignment.x - 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.fresh = true - - current = ui_box_tranverse_next( current ) +UI_LayoutCombo :: struct #raw_union { + array : [UI_StylePreset.Count] UI_Layout, + using layouts : struct { + default, disabled, hot, active : UI_Layout, } } + +to_ui_layout_side :: #force_inline proc( pixels : f32 ) -> UI_LayoutSide { return { pixels, pixels, pixels, pixels } } +to_ui_layout_combo :: #force_inline proc( layout : UI_Layout ) -> UI_LayoutCombo { return { layouts = {layout, layout, layout, layout} } } + +/* +Layout Interface + +Layout for UI_Boxes in the state graph is stored on a per-graph UI_State basis in the fixed sized stack called layout_combo_stack. +The following provides a convient way to manipulate this stack from the assuption of the program's state.ui_context + +The following procedure overloads are available from grime.odin: +* ui_layout +* ui_layout_push +*/ + +ui_layout_peek :: #force_inline proc() -> UI_LayoutCombo { return stack_peek( & get_state().ui_context.layout_combo_stack) } +ui_layout_ref :: #force_inline proc() -> ^UI_LayoutCombo { return stack_peek_ref( & get_state().ui_context.layout_combo_stack) } + +ui_layout_push_layout :: #force_inline proc( layout : UI_Layout ) { push( & get_state().ui_context.layout_combo_stack, to_ui_layout_combo(layout)) } +ui_layout_push_theme :: #force_inline proc( combo : UI_LayoutCombo ) { push( & get_state().ui_context.layout_combo_stack, combo ) } +ui_layout_pop :: #force_inline proc() { pop( & get_state().ui_context.layout_combo_stack ) } + +@(deferred_none = ui_layout_pop) ui_layout_via_layout :: #force_inline proc( layout : UI_Layout ) { ui_layout_push( layout) } +@(deferred_none = ui_layout_pop) ui_layout_via_combo :: #force_inline proc( combo : UI_LayoutCombo ) { ui_layout_push( combo) } + +ui_set_layout :: #force_inline proc( layout : UI_Layout, preset : UI_StylePreset ) { stack_peek_ref( & get_state().ui_context.layout_combo_stack).array[preset] = layout } + +/* +Widget Layout Ops +*/ + +ui_layout_children_horizontally :: proc( container : ^UI_Box, direction : UI_LayoutDirectionX, width_ref : ^f32 ) { +} diff --git a/code/ui_layout_compute.odin b/code/ui_layout_compute.odin new file mode 100644 index 0000000..0e032bb --- /dev/null +++ b/code/ui_layout_compute.odin @@ -0,0 +1,180 @@ +package sectr + +// Note(Ed): This is naturally pretty expensive + +ui_compute_layout :: proc( ui : ^UI_State ) +{ + profile(#procedure) + state := get_state() + + root := ui.root + { + computed := & root.computed + style := root.style + layout := & root.layout + computed.bounds.min = layout.pos + computed.bounds.max = layout.size.min + computed.content = computed.bounds + } + + current := root.first + for ; current != nil; + { + // if current.computed.fresh do return + + // TODO(Ed): Lift this to ui_box_compute_layout + // profile("Layout Box") + style := current.style + layout := & current.layout + + // These are used to choose via multiplication weather to apply + // position & size constraints of the parent. + // The parent's unadjusted content bounds however are enforced for position, + // they cannot be ignored. The user may bypass them by doing the + // relative offset math vs world/screen space if they desire. + fixed_pos_x : f32 = cast(f32) int(.Fixed_Position_X in layout.flags) + fixed_pos_y : f32 = cast(f32) int(.Fixed_Position_Y in layout.flags) + fixed_width : f32 = cast(f32) int(.Fixed_Width in layout.flags) + fixed_height : f32 = cast(f32) int(.Fixed_Height in layout.flags) + + size_to_text : bool = .Size_To_Text in layout.flags + + parent := current.parent + computed := & current.computed + + parent_content := parent.computed.content + parent_content_size := parent_content.max - parent_content.min + parent_center := parent_content.min + parent_content_size * 0.5 + + /* + If fixed position (X or Y): + * Ignore Margins + * Ignore Anchors + + If clampped position (X or Y): + * Positon cannot exceed the anchors/margins bounds. + + If fixed size (X or Y): + * Ignore Parent constraints (can only be clipped) + + If auto-sized: + * Enforce parent size constraint of bounds relative to + where the adjusted content bounds are after applying margins & anchors. + The 'side' conflicting with the bounds will end at that bound side instead of clipping. + + If size.min is not 0: + * Ignore parent constraints if the bounds go below that value. + + If size.max is 0: + * Allow the child box to spread to entire adjusted content bounds, otherwise clampped to max size. + */ + + // 1. Anchors + anchor := & layout.anchor + anchored_bounds := range2( + parent_content.min + parent_content_size * anchor.min, + parent_content.max - parent_content_size * anchor.max, + ) + // anchored_bounds_origin := (anchored_bounds.min + anchored_bounds.max) * 0.5 + + // 2. Apply Margins + margins := range2( + { layout.margins.left, layout.margins.bottom }, + { layout.margins.right, layout.margins.top }, + ) + margined_bounds := range2( + anchored_bounds.min + margins.min, + anchored_bounds.max - margins.max, + ) + margined_bounds_origin := (margined_bounds.min + margined_bounds.max) * 0.5 + margined_size := margined_bounds.max - margined_bounds.min + + // 3. Enforce Min/Max Size Constraints + adjusted_max_size_x := layout.size.max.x > 0 ? min( margined_size.x, layout.size.max.x ) : margined_size.x + adjusted_max_size_y := layout.size.max.y > 0 ? min( margined_size.y, layout.size.max.y ) : margined_size.y + + adjusted_size : Vec2 + adjusted_size.x = max( adjusted_max_size_x, layout.size.min.x) + adjusted_size.y = max( adjusted_max_size_y, layout.size.min.y) + + if .Fixed_Width in layout.flags { + adjusted_size.x = layout.size.min.x + } + if .Fixed_Height in layout.flags { + adjusted_size.y = layout.size.min.y + } + + text_size : Vec2 + if layout.font_size == computed.text_size.y { + text_size = computed.text_size + } + else { + text_size = cast(Vec2) measure_text_size( current.text.str, style.font, layout.font_size, 0 ) + } + + if size_to_text { + adjusted_size = text_size + } + + // 5. Determine relative position + + origin_center := margined_bounds_origin + origin_top_left := Vec2 { margined_bounds.min.x, margined_bounds.max.y } + + origin := .Origin_At_Anchor_Center in layout.flags ? origin_center : origin_top_left + + rel_pos := origin + layout.pos + + if .Fixed_Position_X in layout.flags { + rel_pos.x = origin.x + layout.pos.x + } + if .Fixed_Position_Y in layout.flags { + rel_pos.y = origin.y + layout.pos.y + } + + vec2_one := Vec2 { 1, 1 } + + // 6. Determine the box bounds + // Adjust Alignment of pivot position + alignment := layout.alignment + bounds := range2( + rel_pos - adjusted_size * alignment, + rel_pos + adjusted_size * (vec2_one - alignment), + ) + + // Determine Padding's outer bounds + border_offset := Vec2 { layout.border_width, layout.border_width } + + padding_bounds := range2( + bounds.min + border_offset, + bounds.min - border_offset, + ) + + // Determine Content Bounds + content_bounds := range2( + bounds.min + { layout.padding.left, layout.padding.bottom } + border_offset, + bounds.max - { layout.padding.right, layout.padding.top } - border_offset, + ) + + // computed.anchors = anchored_bounds + // computed.margins = margined_bounds + computed.bounds = bounds + computed.padding = padding_bounds + computed.content = content_bounds + + if len(current.text.str) > 0 + { + content_size := content_bounds.max - content_bounds.min + text_pos : Vec2 + text_pos = content_bounds.min + { 0, text_size.y } + text_pos.x += ( content_size.x - text_size.x ) * layout.text_alignment.x + 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.fresh = true + + current = ui_box_tranverse_next( current ) + } +} diff --git a/code/ui_layout_widget.odin b/code/ui_layout_widget.odin new file mode 100644 index 0000000..7ba6dc6 --- /dev/null +++ b/code/ui_layout_widget.odin @@ -0,0 +1,3 @@ +package sectr + + diff --git a/code/ui_screen.odin b/code/ui_screen.odin index e76ed04..9671c8d 100644 --- a/code/ui_screen.odin +++ b/code/ui_screen.odin @@ -10,13 +10,13 @@ UI_ScreenState :: struct settings_btn : struct { using widget : UI_Widget, - is_open : b32, } }, settings_menu : struct { pos, size, min_size : Vec2, container : UI_VBox, + is_open : b32, }, } @@ -40,38 +40,39 @@ ui_screen_menu_bar :: proc() using screen_ui { using menu_bar - ui_theme_via_style({ - flags = {}, + ui_layout( UI_Layout { + // flags = {.Fixed_Position_X, .Fixed_Position_Y, .Fixed_Width, .Fixed_Height}, + anchor = {}, + alignment = { 0, 1 }, + border_width = 1.0, + font_size = 12, + pos = menu_bar.pos, + size = range2( menu_bar.size, {}), + }) + ui_style( UI_Style { bg_color = { 0, 0, 0, 30 }, border_color = { 0, 0, 0, 200 }, font = default_font, text_color = Color_White, - layout = { - anchor = {}, - alignment = { 0, 1 }, - border_width = 1.0, - font_size = 12, - pos = menu_bar.pos, - size = range2( menu_bar.size, {}), - }, }) - container = ui_hbox( .Left_To_Right, "App Menu Bar", { .Mouse_Clickable} ) + // ui_hbox( & container, .Left_To_Right, "App Menu Bar", { .Mouse_Clickable} ) + container = ui_hbox_begin( .Left_To_Right, "Menu Bar" ) + ui_parent(container) - theme := to_ui_styletheme({ + ui_layout( UI_Layout { + flags = {}, + anchor = {}, + border_width = 1.0, + font_size = 12, + }) + style_theme := to_ui_style_combo({ bg_color = Color_Frame_Disabled, font = default_font, text_color = Color_White, - layout = { - anchor = range2( {0, 0}, {0, 0} ), - alignment = { 0.0, 0.0 }, - font_size = 18, - text_alignment = { 0.5, 0.5 }, - size = range2({25, 0}, {0, 0}) - } }) - theme.hot.bg_color = Color_Blue - theme.active.bg_color = Color_Frame_Select - ui_style_theme(theme) + style_theme.hot.bg_color = Color_Blue + style_theme.active.bg_color = Color_Frame_Select + ui_style(style_theme) move_box := ui_button("Move Box"); { @@ -79,28 +80,28 @@ ui_screen_menu_bar :: proc() if active { menu_bar.pos += input.mouse.delta } - style.anchor.ratio.x = 0.2 + layout.anchor.ratio.x = 0.2 } spacer := ui_spacer("Menu Bar: Move Spacer") - spacer.style.flags |= {.Fixed_Width} - spacer.style.size.min.x = 50 - // spacer.style.bg_color = Color_Red + spacer.layout.flags |= {.Fixed_Width} + spacer.layout.size.min.x = 50 settings_btn.widget = ui_button("Settings Btn") settings_btn.text = str_intern("Settings") - settings_btn.style.flags = { + settings_btn.layout.flags = { // .Scale_Width_By_Height_Ratio, .Fixed_Width } - settings_btn.style.size.min.x = 100 + settings_btn.layout.size.min.x = 100 if settings_btn.pressed { - settings_btn.is_open = true + settings_menu.is_open = true } spacer = ui_spacer("Menu Bar: End Spacer") - spacer.style.anchor.ratio.x = 1.0 - // spacer.style.bg_color = Color_Red + spacer.layout.anchor.ratio.x = 1.0 + + ui_hbox_end( container) } } @@ -109,7 +110,7 @@ ui_screen_settings_menu :: proc() profile("Settings Menu") using state := get_state() using state.screen_ui - if menu_bar.settings_btn.pressed || ! menu_bar.settings_btn.is_open do return + if ! settings_menu.is_open do return using settings_menu if size.x < min_size.x do size.x = min_size.x @@ -118,66 +119,66 @@ ui_screen_settings_menu :: proc() container = ui_vbox_begin( .Top_To_Bottom, "Settings Menu", {.Mouse_Clickable}) { using container - style.flags = { - // .Origin_At_Anchor_Center - } - style.pos = pos - style.alignment = { 0.5, 0.5 } - style.bg_color = Color_BG_Panel_Translucent - style.size = range2( size, {}) + // flags = {} + layout.flags = { .Origin_At_Anchor_Center } + layout.pos = pos + layout.alignment = { 0.5, 0.5 } + layout.size = range2( size, {}) + style.bg_color = Color_BG_Panel_Translucent } ui_parent(container) { - ui_theme_via_style({ + ui_layout( UI_Layout { + font_size = 16, + }) + ui_style( UI_Style { bg_color = Color_Transparent, font = default_font, text_color = Color_White, - layout = { font_size = 16 }, }) - ui_style_theme_ref().hot.bg_color = Color_Blue + ui_style_ref().hot.bg_color = Color_Blue frame_bar := ui_hbox_begin(.Left_To_Right, "Settings Menu: Frame Bar", { .Mouse_Clickable, .Focusable, .Click_To_Focus }) { - frame_bar.style.bg_color = Color_BG_Panel - frame_bar.style.flags = {.Fixed_Height} - frame_bar.style.alignment = { 0, 0 } - frame_bar.style.size.min.y = 50 - if frame_bar.active { - pos += input.mouse.delta - } + frame_bar.style.bg_color = Color_BG_Panel + frame_bar.layout.flags = {.Fixed_Height} + frame_bar.layout.size.min.y = 50 ui_parent(frame_bar) title := ui_text("Settings Menu: Title", str_intern("Settings Menu"), {.Disabled}) { using title - style.margins = { 0, 0, 15, 0} - style.text_alignment = {0 , 0.5} - style.anchor.ratio.x = 1.0 + layout.margins = { 0, 0, 15, 0} + layout.text_alignment = {0 , 0.5} + layout.anchor.ratio.x = 1.0 } - ui_style_theme(ui_style_theme_peek()) - theme := ui_style_theme_ref() + ui_style(ui_style_peek()) + theme := ui_style_ref() theme.default.bg_color = Color_GreyRed theme.hot. bg_color = Color_Red close_btn := ui_button("Settings Menu: Close Btn") { using close_btn text = str_intern("close") - style.flags = {.Fixed_Width} - style.size.min = {50, 0} - style.text_alignment = {0.5, 0.5} - style.anchor.ratio.x = 1.0 + layout.flags = {.Fixed_Width} + layout.size.min = {50, 0} + layout.text_alignment = {0.5, 0.5} + layout.anchor.ratio.x = 1.0 if close_btn.pressed { - menu_bar.settings_btn.is_open = false + settings_menu.is_open = false } } - ui_hbox_end(frame_bar)//, & size.x) + ui_hbox_end(frame_bar, & size.x) + } + if frame_bar.active { + pos += input.mouse.delta } spacer := ui_spacer("Settings Menu: Spacer") - spacer.style.anchor.ratio.y = 1.0 + spacer.layout.anchor.ratio.y = 1.0 - ui_vbox_end(container)//, & size.y) + ui_vbox_end(container, & size.y) } ui_resizable_handles( & container, & pos, & size ) diff --git a/code/ui_signal.odin b/code/ui_signal.odin index f2ddf1a..9c3d108 100644 --- a/code/ui_signal.odin +++ b/code/ui_signal.odin @@ -211,7 +211,8 @@ ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas box.prev_style = box.style box.style_delta = 0 } - box.style = stack_peek( & ui.theme_stack ).hot + box.layout = ui_layout_peek().hot + box.style = ui_style_peek().hot } if is_active { @@ -219,7 +220,8 @@ ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas box.prev_style = box.style box.style_delta = 0 } - box.style = stack_peek( & ui.theme_stack ).active + box.layout = ui_layout_peek().active + box.style = ui_style_peek().active } if is_disabled { @@ -227,7 +229,8 @@ ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas box.prev_style = box.style box.style_delta = 0 } - box.style = stack_peek( & ui.theme_stack ).disabled + box.layout = ui_layout_peek().disabled + box.style = ui_style_peek().disabled } if ! is_disabled && ! is_active && ! is_hot { @@ -238,7 +241,8 @@ ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas else { box.style_delta += frame_delta } - box.style = stack_peek( & ui.theme_stack ).default + box.layout = ui_layout_peek().default + box.style = ui_style_peek().default } } diff --git a/code/ui_style.odin b/code/ui_style.odin index 20ea86a..3ec26bc 100644 --- a/code/ui_style.odin +++ b/code/ui_style.odin @@ -1,95 +1,12 @@ package sectr -UI_LayoutDirectionX :: enum(i32) { - Left_To_Right, - Right_To_Left, +UI_TextAlign :: enum u32 { + Left, + Center, + Right, + Count } -UI_LayoutDirectionY :: enum(i32) { - Top_To_Bottom, - Bottom_To_Top, -} - -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, - - font_size : f32, - - 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, - border_width : UI_Scalar, - - // 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, -} - -// TODO(Ed): Change this to layout flags? Everything so far is just related to auto-layout -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, - - // TODO(Ed): Implement this! - // For this to work, the children must have a minimum size set & their size overall must be greater than the parent's minimum size - Size_To_Content, - - // Will size the box to its text. - Size_To_Text, - - // TODO(Ed): Implement this! - Text_Wrap, - - Count, -} -UI_StyleFlags :: bit_set[UI_StyleFlag; u32] - UI_StylePreset :: enum u32 { Default, Disabled, @@ -99,79 +16,61 @@ UI_StylePreset :: enum u32 { } UI_Style :: struct { - flags : UI_StyleFlags, - bg_color : Color, border_color : Color, + // 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 blur_size : f32, + // TODO(Ed): Add support for textures + // texture : Texture2, + + // TODO(Ed): Add support for custom shader + // shader : UI_Shader, + font : FontID, // TODO(Ed): Should this get moved to the layout struct? Techncially font-size is mainly text_color : Color, + // TODO(Ed) : Support setting the cursor state cursor : UI_Cursor, - // TODO(Ed): Should layout be separate from style? - // It technically entirely makes up style flags - // and the only thing shared I guess is the font determining the measured text size + transition time - // Technically we can provide a separate transition time option just for layout... - 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 { +UI_StyleCombo :: 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 -} +to_ui_style_combo :: #force_inline proc( style : UI_Style ) -> UI_StyleCombo { return { styles = {style, style, style, style} } } -to_ui_layoutside :: #force_inline proc( pixels : f32 ) -> UI_LayoutSide { return { pixels, pixels, pixels, pixels } } +/* +Style Interface -to_ui_styletheme :: #force_inline proc( style : UI_Style ) -> UI_StyleTheme { return { styles = {style, style, style, style} } } +Style for UI_Boxes in the state graph is stored on a per-graph UI_State basis in the fixed sized stack called style_combo_stack. +The following provides a convient way to manipulate this stack from the assuption of the program's state.ui_context -ui_style_peek :: #force_inline proc( box_state : UI_StylePreset ) -> UI_Style { return stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] } -ui_style_ref :: #force_inline proc( box_state : UI_StylePreset ) -> (^ UI_Style) { return & stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] } +The following procedure overloads are available from grime.odin : +* ui_style +* ui_style_push +*/ -ui_style_set :: #force_inline proc ( style : UI_Style, box_state : UI_StylePreset ) { stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] = style } +ui_style_peek :: #force_inline proc() -> UI_StyleCombo { return stack_peek( & get_state().ui_context.style_combo_stack ) } +ui_style_ref :: #force_inline proc() -> (^ UI_StyleCombo) { return stack_peek_ref( & get_state().ui_context.style_combo_stack ) } -ui_style_set_layout :: #force_inline proc ( layout : UI_Layout, preset : UI_StylePreset ) { stack_peek_ref( & get_state().ui_context.theme_stack ).array[preset].layout = layout } +ui_style_push_style :: #force_inline proc( style : UI_Style ) { push( & get_state().ui_context.style_combo_stack, to_ui_style_combo(style)) } +ui_style_push_combo :: #force_inline proc( combo : UI_StyleCombo ) { push( & get_state().ui_context.style_combo_stack, combo ) } +ui_style_pop :: #force_inline proc() { pop( & get_state().ui_context.style_combo_stack ) } -ui_style_theme_push :: #force_inline proc( preset : UI_StyleTheme ) { push( & get_state().ui_context.theme_stack, preset ) } -ui_style_theme_pop :: #force_inline proc() { pop( & get_state().ui_context.theme_stack ) } +@(deferred_none = ui_style_pop) ui_style_via_style :: #force_inline proc( style : UI_Style ) { ui_style_push( style) } +@(deferred_none = ui_style_pop) ui_style_via_combo :: #force_inline proc( combo : UI_StyleCombo ) { ui_style_push( combo) } -@(deferred_none = ui_style_theme_pop) -ui_style_theme :: #force_inline proc( preset : UI_StyleTheme ) { ui_style_theme_push( preset ) } - -ui_style_theme_peek :: #force_inline proc() -> UI_StyleTheme { return stack_peek( & get_state().ui_context.theme_stack ) } -ui_style_theme_ref :: #force_inline proc() -> (^ UI_StyleTheme) { return stack_peek_ref( & get_state().ui_context.theme_stack ) } - -@(deferred_none = ui_style_theme_pop) -ui_theme_via_style :: #force_inline 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 :: #force_inline proc( layout : UI_Layout ) { ui_style_theme_layout_push(layout) } +ui_style_set :: #force_inline proc ( style : UI_Style, preset : UI_StylePreset ) { stack_peek_ref( & get_state().ui_context.style_combo_stack ).array[preset] = style } diff --git a/code/ui_style_themes.odin b/code/ui_style_themes.odin deleted file mode 100644 index 1bee476..0000000 --- a/code/ui_style_themes.odin +++ /dev/null @@ -1,3 +0,0 @@ -package sectr - -ui_theme_btn_default : proc( ) -> UI_StyleTheme \ No newline at end of file diff --git a/code/ui_tests.odin b/code/ui_tests.odin index 8367520..0c58f34 100644 --- a/code/ui_tests.odin +++ b/code/ui_tests.odin @@ -11,9 +11,9 @@ test_hover_n_click :: proc() if first_btn.left_clicked || debug.frame_2_created { debug.frame_2_created = true - second_layout := first_btn.style.layout + second_layout := first_btn.layout second_layout.pos = { 250, 0 } - ui_set_layout( second_layout ) + ui_layout( second_layout ) second_box := ui_button( "SECOND BOX!") } @@ -34,12 +34,12 @@ test_draggable :: proc() pos = { 0, 0 }, size = range2({ 200, 200 }, {}), } - ui_style_theme_set_layout( draggable_layout ) + ui_layout( draggable_layout ) draggable := ui_widget( "Draggable Box!", UI_BoxFlags { .Mouse_Clickable, .Mouse_Resizable } ) if draggable.first_frame { - debug.draggable_box_pos = draggable.style.layout.pos + { 0, -100 } - debug.draggable_box_size = draggable.style.layout.size.min + debug.draggable_box_pos = draggable.layout.pos + { 0, -100 } + debug.draggable_box_size = draggable.layout.size.min } // Dragging @@ -51,8 +51,8 @@ test_draggable :: proc() draggable.style.bg_color = Color_Blue } - draggable.style.layout.pos = debug.draggable_box_pos - draggable.style.layout.size.min = debug.draggable_box_size + draggable.layout.pos = debug.draggable_box_pos + draggable.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) @@ -71,21 +71,21 @@ test_parenting :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_S parent_layout.margins = { 100, 100, 100, 100 } parent_layout.padding = { 5, 10, 5, 5 } parent_layout.pos = { 0, 0 } - - parent_theme := frame_style_default ^ - parent_theme.layout = parent_layout - parent_theme.flags = { + parent_layout.flags = { // .Fixed_Position_X, .Fixed_Position_Y, .Fixed_Width, .Fixed_Height, } - ui_theme_via_style(parent_theme) + ui_layout(parent_layout) + + parent_style := frame_style_default ^ + ui_style(parent_style) parent := ui_widget( "Parent", { .Mouse_Clickable, .Mouse_Resizable }) - ui_parent(parent) + ui_parent_push(parent) { if parent.first_frame { - debug.draggable_box_pos = parent.style.layout.pos - debug.draggable_box_size = parent.style.layout.size.min + debug.draggable_box_pos = parent.layout.pos + debug.draggable_box_size = parent.layout.size.min } if parent.active { debug.draggable_box_pos += mouse_world_delta() @@ -93,8 +93,8 @@ test_parenting :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_S if (ui.hot == parent.key) { parent.style.bg_color = Color_Blue } - parent.style.layout.pos = debug.draggable_box_pos - parent.style.layout.size.min = debug.draggable_box_size + parent.layout.pos = debug.draggable_box_pos + parent.layout.size.min = debug.draggable_box_size } child_layout := default_layout ^ @@ -104,16 +104,16 @@ test_parenting :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_S child_layout.padding = { 5, 5, 5, 5 } child_layout.anchor = range2({ 0.2, 0.1 }, { 0.1, 0.15 }) child_layout.pos = { 0, 0 } - - child_theme := frame_style_default ^ - child_theme.bg_color = Color_GreyRed - child_theme.flags = { + child_layout.flags = { // .Fixed_Width, .Fixed_Height, .Origin_At_Anchor_Center } - child_theme.layout = child_layout - ui_theme_via_style(child_theme) + + child_style := frame_style_default ^ + child_style.bg_color = Color_GreyRed + ui_theme(child_layout, child_style) child := ui_widget( "Child", { .Mouse_Clickable }) + ui_parent_pop() } test_text_box :: proc() @@ -122,27 +122,27 @@ test_text_box :: proc() ui := ui_context @static pos : Vec2 - style := ui_style_peek( .Default ) - style.text_alignment = { 1.0, 1.0 } + layout := ui_layout_peek().default + layout.text_alignment = { 1.0, 1.0 } // style.flags = { .Size_To_Text } - style.padding = { 10, 10, 10, 10 } - style.layout.font_size = 32 - ui_style_theme( { styles = { style, style, style, style, }} ) + layout.padding = { 10, 10, 10, 10 } + layout.font_size = 32 + ui_layout( layout) text := str_intern( "Lorem ipsum dolor sit amet") text_box := ui_text("TEXT BOX!", text, flags = { .Mouse_Clickable }) if text_box.first_frame { - pos = text_box.style.layout.pos + pos = text_box.layout.pos } if text_box.active { pos += mouse_world_delta() } - text_box.style.layout.pos = pos + text_box.layout.pos = pos - text_box.style.size.min = { text_box.computed.text_size.x * 1.5, text_box.computed.text_size.y * 3 } + text_box.layout.size.min = { text_box.computed.text_size.x * 1.5, text_box.computed.text_size.y * 3 } } test_whitespace_ast :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_Style ) @@ -151,30 +151,22 @@ test_whitespace_ast :: proc( default_layout : ^UI_Layout, frame_style_default : state := get_state(); using state ui := ui_context - text_style := frame_style_default ^ - text_style.flags = { + text_layout := default_layout^ + text_layout.flags = { .Origin_At_Anchor_Center, .Fixed_Position_X, .Fixed_Position_Y, // .Fixed_Width, .Fixed_Height, } - text_style.text_alignment = { 0.0, 0.5 } - text_style.alignment = { 0.0, 1.0 } - text_style.size.min = { 1600, 30 } - - text_theme := UI_StyleTheme { styles = { - text_style, - text_style, - text_style, - text_style, - }} - text_theme.default.bg_color = Color_Transparent - text_theme.disabled.bg_color = Color_Frame_Disabled - text_theme.hot.bg_color = Color_Frame_Hover - text_theme.active.bg_color = Color_Frame_Select - ui_style_theme( text_theme ) - - layout_text := text_style.layout - + text_layout.text_alignment = { 0.0, 0.5 } + text_layout.alignment = { 0.0, 1.0 } + text_layout.size.min = { 1600, 30 } + text_style := frame_style_default ^ + text_style_combo := to_ui_style_combo(text_style) + text_style_combo.default.bg_color = Color_Transparent + text_style_combo.disabled.bg_color = Color_Frame_Disabled + text_style_combo.hot.bg_color = Color_Frame_Hover + text_style_combo.active.bg_color = Color_Frame_Select + ui_theme( text_layout, text_style ) alloc_error : AllocatorError; success : bool // debug.lorem_content, success = os.read_entire_file( debug.path_lorem, frame_allocator() ) @@ -201,7 +193,7 @@ test_whitespace_ast :: proc( default_layout : ^UI_Layout, frame_style_default : continue } - ui_style_theme_set_layout( layout_text ) + ui_layout( text_layout ) line_hbox := ui_widget(str_fmt_alloc( "line %v", line_id ), {.Mouse_Clickable}) if line_hbox.key == ui.hot @@ -209,27 +201,19 @@ test_whitespace_ast :: proc( default_layout : ^UI_Layout, frame_style_default : line_hbox.text = StrRunesPair {} ui_parent(line_hbox) - chunk_layout := layout_text + chunk_layout := text_layout chunk_layout.alignment = { 0.0, 1.0 } chunk_layout.anchor = range2({ 0.0, 0 }, { 0.0, 0 }) chunk_layout.pos = {} + chunk_layout.flags = { .Fixed_Position_X, .Size_To_Text } chunk_style := text_style - chunk_style.flags = { .Fixed_Position_X, .Size_To_Text } - chunk_style.layout = chunk_layout - - chunk_theme := UI_StyleTheme { styles = { - chunk_style, - chunk_style, - chunk_style, - chunk_style, - }} - ui_style_theme( chunk_theme ) + ui_theme( to_ui_layout_combo(chunk_layout), to_ui_style_combo(chunk_style) ) head := line.first for ; head != nil; { - ui_style_theme_set_layout( chunk_layout ) + ui_layout( chunk_layout ) widget : UI_Widget #partial switch head.type @@ -269,14 +253,14 @@ test_whitespace_ast :: proc( default_layout : ^UI_Layout, frame_style_default : head = head.next } - line_hbox.style.size.min.x = chunk_layout.pos.x + line_hbox.layout.size.min.x = chunk_layout.pos.x } else { builder_backing : [16 * Kilobyte] byte builder := str.builder_from_bytes( builder_backing[:] ) - line_hbox.style.flags |= { .Size_To_Text } + line_hbox.layout.flags |= { .Size_To_Text } head := line.first.next for ; head != nil; @@ -293,11 +277,11 @@ test_whitespace_ast :: proc( default_layout : ^UI_Layout, frame_style_default : if len(line_hbox.text.str) > 0 { array_append( widgets_ptr, line_hbox ) - layout_text.pos.x = text_style.layout.pos.x - layout_text.pos.y += size_range2(line_hbox.computed.bounds).y + text_layout.pos.x = text_layout.pos.x + text_layout.pos.y += size_range2(line_hbox.computed.bounds).y } else { - layout_text.pos.y += size_range2( (& widgets.data[ widgets.num - 1 ]).computed.bounds ).y + text_layout.pos.y += size_range2( (& widgets.data[ widgets.num - 1 ]).computed.bounds ).y } line_id += 1 diff --git a/code/ui_theme.odin b/code/ui_theme.odin new file mode 100644 index 0000000..1790c41 --- /dev/null +++ b/code/ui_theme.odin @@ -0,0 +1,54 @@ +package sectr + +UI_ThemePtr :: struct { + layout : ^UI_LayoutCombo, + style : ^UI_StyleCombo, +} + +UI_Theme :: struct { + layout : UI_LayoutCombo, + style : UI_StyleCombo, +} + +ui_theme_pop :: #force_inline proc() { + ui_layout_pop() + ui_style_pop() +} + +@(deferred_none = ui_theme_pop) +ui_theme_via_layout_style :: #force_inline proc( layout : UI_Layout, style : UI_Style ) { + using ui := get_state().ui_context + ui_layout_push( layout ) + ui_style_push( style ) +} + +@(deferred_none = ui_theme_pop) +ui_theme_via_combos :: #force_inline proc( layout : UI_LayoutCombo, style : UI_StyleCombo ) { + using ui := get_state().ui_context + ui_layout_push( layout ) + ui_style_push( style ) +} + +@(deferred_none = ui_theme_pop) +ui_theme_via_theme :: #force_inline proc( theme : UI_Theme ) { + using ui := get_state().ui_context + ui_layout_push( theme.layout ) + ui_style_push( theme.style ) +} + + +/* +UI Themes: Comprise of UI_Box's layout & style + +Provides presets for themes and their interface for manipulating the combo stacks in UI_State in pairs +*/ + +UI_Theme_Btn_Default :: UI_Theme { + +} + +@(deferred_none = ui_layout_pop) +ui_theme_btn_default :: #force_inline proc() { + ui_layout_push(UI_Theme_Btn_Default.layout) + ui_style_push(UI_Theme_Btn_Default.style) +} diff --git a/code/ui_widgets.odin b/code/ui_widgets.odin index 9d08252..f30a2b1 100644 --- a/code/ui_widgets.odin +++ b/code/ui_widgets.odin @@ -47,92 +47,101 @@ ui_hbox_begin :: proc( direction : UI_LayoutDirectionX, label : string, flags : // profile(#procedure) hbox.direction = direction hbox.box = ui_box_make( flags, label ) - hbox.signal = ui_signal_from_box( hbox.box ) + hbox.signal = ui_signal_from_box(hbox.box) return } // Auto-layout children -ui_hbox_end :: proc( hbox : UI_HBox, width_ref : ^f32 = nil ) { +ui_hbox_end :: proc( hbox : UI_HBox, width_ref : ^f32 = nil ) +{ // profile(#procedure) - hbox_width : f32 - if width_ref != nil { - hbox_width = width_ref ^ - } - else { - hbox_width = hbox.computed.content.max.x - hbox.computed.content.min.x - } - // do layout calculations for the children - total_stretch_ratio : f32 = 0.0 - size_req_children : f32 = 0 - for child := hbox.first; child != nil; child = child.next + // ui_box_compute_layout(hox.widget) + + // ui_layout_children_horizontally( & hbox.box, hbox.direction, width_ref ) { - using child - using style.layout - scaled_width_by_height : b32 = b32(.Scale_Width_By_Height_Ratio in style.flags) - if .Fixed_Width in style.flags - { - if scaled_width_by_height { - height := size.max.y != 0 ? size.max.y : hbox_width - width := height * size.min.x + hbox_width : f32 + if width_ref != nil { + hbox_width = width_ref ^ + } + else { + hbox_width = hbox.computed.content.max.x - hbox.computed.content.min.x + } - size_req_children += width + // do layout calculations for the children + total_stretch_ratio : f32 = 0.0 + size_req_children : f32 = 0 + for child := hbox.first; child != nil; child = child.next + { + using child.layout + scaled_width_by_height : b32 = b32(.Scale_Width_By_Height_Ratio in flags) + if .Fixed_Width in flags + { + if scaled_width_by_height { + height := size.max.y != 0 ? size.max.y : hbox_width + width := height * size.min.x + + size_req_children += width + continue + } + + size_req_children += size.min.x continue } - size_req_children += size.min.x - continue + total_stretch_ratio += anchor.ratio.x } - total_stretch_ratio += anchor.ratio.x - } + avail_flex_space := hbox_width - size_req_children - avail_flex_space := hbox_width - size_req_children - - allocate_space :: proc( child : ^UI_Box, total_stretch_ratio, avail_flex_space : f32 ) - { - using child.style - if ! (.Fixed_Width in child.style.flags) { - size.min.x = anchor.ratio.x * (1 / total_stretch_ratio) * avail_flex_space + allocate_space :: proc( child : ^UI_Box, total_stretch_ratio, avail_flex_space : f32 ) + { + using child.layout + if ! (.Fixed_Width in flags) { + size.min.x = anchor.ratio.x * (1 / total_stretch_ratio) * avail_flex_space + } + flags |= {.Fixed_Width} + } + + space_used : f32 = 0.0 + switch hbox.direction{ + case .Right_To_Left: + for child := hbox.last; child != nil; child = child.prev { + allocate_space(child, total_stretch_ratio, avail_flex_space) + using child.layout + anchor = range2({0, 0}, {0, 0}) + alignment = { 0, 1 }// - hbox.layout.alignment + pos.x = space_used + space_used += size.min.x + size.min.y = hbox.computed.content.max.y - hbox.computed.content.min.y + } + case .Left_To_Right: + for child := hbox.first; child != nil; child = child.next { + allocate_space(child, total_stretch_ratio, avail_flex_space) + using child.layout + anchor = range2({0, 0}, {0, 0}) + alignment = { 0, 1 } + pos.x = space_used + space_used += size.min.x + size.min.y = hbox.computed.content.max.y - hbox.computed.content.min.y + } } - flags |= {.Fixed_Width} - alignment = {0, 1} } - space_used : f32 = 0.0 - switch hbox.direction{ - case .Right_To_Left: - for child := hbox.last; child != nil; child = child.prev { - allocate_space(child, total_stretch_ratio, avail_flex_space) - using child.style - anchor = range2({0, 0}, {0, 0}) - pos.x = space_used - space_used += size.min.x - } - case .Left_To_Right: - for child := hbox.first; child != nil; child = child.next { - allocate_space(child, total_stretch_ratio, avail_flex_space) - using child.style - anchor = range2({0, 0}, {0, 0}) - pos.x = space_used - space_used += size.min.x - } - } +} + +@(deferred_out = ui_hbox_end_auto) +ui_hbox :: #force_inline proc( direction : UI_LayoutDirectionX, label : string, flags : UI_BoxFlags = {} ) -> (hbox : UI_HBox) { + hbox = ui_hbox_begin(direction, label, flags) + ui_parent_push(hbox.box) + return } // Auto-layout children and pop parent from parent stack -ui_hbox_end_pop_parent :: proc( hbox : UI_HBox ) { - // ui_box_compute_layout(hox.widget) +ui_hbox_end_auto :: proc( hbox : UI_HBox ) { ui_parent_pop() ui_hbox_end(hbox) } - -@(deferred_out = ui_hbox_end_pop_parent) -ui_hbox :: #force_inline proc( direction : UI_LayoutDirectionX, label : string, flags : UI_BoxFlags = {} ) -> (hbox : UI_HBox) { - hbox = ui_hbox_begin(direction, label, flags) - ui_parent_push(hbox.widget) - return -} //endregion Horizontal Box // Adds resizable handles to a widget @@ -164,23 +173,26 @@ ui_resizable_handles :: proc( parent : ^UI_Widget, ui_parent(parent) flags := UI_BoxFlags { .Mouse_Clickable, .Focusable } - style_bar := UI_Style { + layout_bar_width := UI_Layout { flags = { .Fixed_Width }, - size = range2({handle_width, 0}, {}), - bg_color = Color_ResizeHandle, alignment = {1, 1}, margins = { handle_width, handle_width, 0, 0 }, + size = range2({handle_width, 0}, {}), + } + style_bar := UI_Style { + bg_color = Color_ResizeHandle, corner_radii = { 5, 0, 0, 0 } } - theme_bar := to_ui_styletheme(style_bar) + theme_bar := to_ui_style_combo(style_bar) theme_bar.default.bg_color = handle_color_default theme_bar.default.corner_radii[0] = 0 - ui_style_theme(theme_bar) - style_resize_height := style_bar - style_resize_height.flags = {.Fixed_Height} - style_resize_height.size.min = {0, handle_width} - style_resize_height.margins = { 0, 0, handle_width, handle_width } - style_resize_height.alignment = {0, 0} + ui_layout(layout_bar_width) + ui_style(theme_bar) + layout_bar_height := layout_bar_width + layout_bar_height.flags = {.Fixed_Height} + layout_bar_height.size.min = {0, handle_width} + layout_bar_height.margins = { 0, 0, handle_width, handle_width } + layout_bar_height.alignment = {0, 0} context.user_ptr = & parent.label name :: proc( ) -> StrRunesPair { @@ -188,47 +200,50 @@ ui_resizable_handles :: proc( parent : ^UI_Widget, return str_intern(str_fmt_tmp("%v: %v", )) } - if left do handle_left = ui_widget("Settings Menu: Resize Left Border", flags ) + if left do handle_left = ui_widget("Settings Menu: Resize Left Handle", flags ) if right { - handle_right = ui_widget("Settings Menu: Resize Right Border", flags) - handle_right.style.anchor.left = 1 - handle_right.style.alignment = { 0, 1 } + handle_right = ui_widget("Settings Menu: Resize Right Handle", flags) + handle_right.layout.anchor.left = 1 + handle_right.layout.alignment = { 0, 1 } } - ui_theme_via_style(style_resize_height) - ui_style_theme_ref().default.bg_color = handle_color_default + ui_layout(layout_bar_height) + ui_style_ref().default.bg_color = handle_color_default if top do handle_top = ui_widget("Settings Menu: Resize Top Border", flags ) if bottom { handle_bottom = ui_widget("Settings Menu: Resize Bottom Border", flags) - handle_bottom.style.anchor.top = 1 - handle_bottom.style.alignment = { 0, 1 } + using handle_bottom.layout + anchor.top = 1 + alignment = { 0, 1 } } - style_corner := UI_Style { + layout_corner := UI_Layout { flags = { .Fixed_Width, .Fixed_Height }, - size = range2({handle_width, handle_width}, {}), - bg_color = Color_ResizeHandle, alignment = {1, 0}, + size = range2({handle_width, handle_width}, {}), + } + style_corner := UI_Style { + bg_color = Color_ResizeHandle, corner_radii = { 5, 0, 0, 0 }, } - ui_theme_via_style(style_corner) - ui_style_theme_ref().default.bg_color = handle_color_default + ui_theme(layout_corner, style_corner) + ui_style_ref().default.bg_color = handle_color_default if corner_tl do handle_corner_tl = ui_widget("Settings Menu: Corner TL", flags) if corner_tr { handle_corner_tr = ui_widget("Settings Menu: Corner TR", flags) - handle_corner_tr.style.anchor = range2({1, 0}, {}) - handle_corner_tr.style.alignment = {0, 0} + handle_corner_tr.layout.anchor = range2({1, 0}, {}) + handle_corner_tr.layout.alignment = {0, 0} } if corner_bl { handle_corner_bl = ui_widget("Settings Menu: Corner BL", flags) - handle_corner_bl.style.anchor = range2({}, {0, 1}) - handle_corner_bl.style.alignment = { 1, 1 } + handle_corner_bl.layout.anchor = range2({}, {0, 1}) + handle_corner_bl.layout.alignment = { 1, 1 } } if corner_br { handle_corner_br = ui_widget("Settings Menu: Corner BR", flags) - handle_corner_br.style.anchor = range2({1, 0}, {0, 1}) - handle_corner_br.style.alignment = {0, 1} + handle_corner_br.layout.anchor = range2({1, 0}, {0, 1}) + handle_corner_br.layout.alignment = {0, 1} } process_handle_drag :: #force_inline proc ( handle : ^UI_Widget, @@ -268,7 +283,7 @@ ui_resizable_handles :: proc( parent : ^UI_Widget, } delta := get_state().input.mouse.delta - alignment := & parent.style.alignment + alignment := & parent.layout.alignment if right do process_handle_drag( & handle_right, { 1, 0 }, delta, {0, 0}, pos, size, alignment ) if left do process_handle_drag( & handle_left, { -1, 0 }, delta, {1, 0}, pos, size, alignment ) if top do process_handle_drag( & handle_top, { 0, 1 }, delta, {0, 0}, pos, size, alignment ) @@ -363,75 +378,78 @@ ui_vbox_begin :: proc( direction : UI_LayoutDirectionY, label : string, flags : ui_vbox_end :: proc( vbox : UI_VBox, height_ref : ^f32 = nil ) { // profile(#procedure) - vbox_height : f32 - if height_ref != nil { - vbox_height = height_ref ^ - } - else { - vbox_height = vbox.computed.bounds.max.y - vbox.computed.bounds.min.y - } + // ui_box_compute_layout(vbox) - // do layout calculations for the children - total_stretch_ratio : f32 = 0.0 - size_req_children : f32 = 0 - for child := vbox.first; child != nil; child = child.next + // ui_layout_children_vertically( & hbox.box, hbox.direction, width_ref ) { - using child - using style.layout - scaled_width_by_height : b32 = b32(.Scale_Width_By_Height_Ratio in style.flags) - if .Fixed_Height in style.flags - { - if scaled_width_by_height { - width := size.max.x != 0 ? size.max.x : vbox_height - height := width * size.min.y + vbox_height : f32 + if height_ref != nil { + vbox_height = height_ref ^ + } + else { + vbox_height = vbox.computed.bounds.max.y - vbox.computed.bounds.min.y + } - size_req_children += height + // do layout calculations for the children + total_stretch_ratio : f32 = 0.0 + size_req_children : f32 = 0 + for child := vbox.first; child != nil; child = child.next + { + using child.layout + scaled_width_by_height : b32 = b32(.Scale_Width_By_Height_Ratio in flags) + if .Fixed_Height in flags + { + if scaled_width_by_height { + width := size.max.x != 0 ? size.max.x : vbox_height + height := width * size.min.y + + size_req_children += height + continue + } + + size_req_children += size.min.y continue } - size_req_children += size.min.y - continue + total_stretch_ratio += anchor.ratio.y } - total_stretch_ratio += anchor.ratio.y - } + avail_flex_space := vbox_height - size_req_children - avail_flex_space := vbox_height - size_req_children - - allocate_space :: proc( child : ^UI_Box, total_stretch_ratio, avail_flex_space : f32 ) - { - using child.style - if ! (.Fixed_Height in child.style.flags) { - size.min.y = anchor.ratio.y * (1 / total_stretch_ratio) * avail_flex_space + allocate_space :: proc( child : ^UI_Box, total_stretch_ratio, avail_flex_space : f32 ) + { + using child.layout + if ! (.Fixed_Height in flags) { + size.min.y = anchor.ratio.y * (1 / total_stretch_ratio) * avail_flex_space + } + flags |= {.Fixed_Height} + alignment = {0, 0} } - flags |= {.Fixed_Height} - alignment = {0, 0} - } - space_used : f32 = 0.0 - switch vbox.direction { - case .Top_To_Bottom: - for child := vbox.last; child != nil; child = child.prev { - allocate_space(child, total_stretch_ratio, avail_flex_space) - using child.style - anchor = range2({0, 0}, {0, 1}) - pos.y = space_used - space_used += size.min.y - } - case .Bottom_To_Top: - for child := vbox.first; child != nil; child = child.next { - allocate_space(child, total_stretch_ratio, avail_flex_space) - using child.style - anchor = range2({0, 0}, {0, 1}) - pos.y = space_used - space_used += size.min.y - } + space_used : f32 = 0.0 + switch vbox.direction { + case .Top_To_Bottom: + for child := vbox.last; child != nil; child = child.prev { + allocate_space(child, total_stretch_ratio, avail_flex_space) + using child.layout + anchor = range2({0, 0}, {0, 1}) + pos.y = space_used + space_used += size.min.y + } + case .Bottom_To_Top: + for child := vbox.first; child != nil; child = child.next { + allocate_space(child, total_stretch_ratio, avail_flex_space) + using child.layout + anchor = range2({0, 0}, {0, 1}) + pos.y = space_used + space_used += size.min.y + } + } } } // Auto-layout children and pop parent from parent stack ui_vbox_end_pop_parent :: proc( vbox : UI_VBox ) { - // ui_box_compute_layout(vbox) ui_parent_pop() ui_vbox_end(vbox) } diff --git a/docs/assets/sectr_host_2024-05-11_22-34-15.png b/docs/assets/sectr_host_2024-05-11_22-34-15.png new file mode 100644 index 0000000..f09536b Binary files /dev/null and b/docs/assets/sectr_host_2024-05-11_22-34-15.png differ diff --git a/docs/ui_state.md b/docs/ui_state.md new file mode 100644 index 0000000..bf3c8de --- /dev/null +++ b/docs/ui_state.md @@ -0,0 +1,39 @@ +# UI + +## Ideal UI_Box processing per-frame + +Would be done in this order: + +Build Graph Start + +0. Parent constructed +1. `ui_box_make()` +2. Prepare layout & style +3. Construct & process children +4. Post-children populated processing +5. Auto-layout box +6. Process signal from children & depdendent events +7. `ui_signal_from_box(box)` +8. Process state dependent on signal +9. ... Eventual Render Pass + +Issues: + +You want to batch auto-layout to be deferred to the end of the construction for the state graph of the frame. +Rendering should be handled outside of the update tick asynchronously (at worst case). +StyleCombo and LayoutCombos are not stored in UI_Box (it would b N * (Style + Layout) per box of memory where N is the number of entries in a combo (right now there is 4) ) +A layout must be choosen before auto-layout occurs and rn the convention is that layout & style are choosen at the end of a signal since it depends on the box's state from the signal. + +Adjusted order: + +0. Parent constructed +1. Prepare layout & style beforehand +2. `ui_box_make()` +3. `ui_signal_from_box(box)` +4. Construct & process children +5. Post-children populated processing +6. ... Build Graph End +7. Auto-Layout Pass +8. Eventual Render Pass + +