From b8e8e7c88a10b7a10ad8cea25a12bd37e501ecb6 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Wed, 8 May 2024 02:26:39 -0400 Subject: [PATCH] Progress on setting up app's UI and horizontal/vertical box widgets --- code/api.odin | 4 + code/env.odin | 11 +- code/grime_string_interning.odin | 14 +- code/grime_unicode.odin | 2 +- code/math.odin | 7 + code/math_pga2.odin | 2 + code/parser_whitespace.odin | 4 +- code/space.odin | 8 + code/text.odin | 2 +- code/tick_render.odin | 36 ++- code/tick_update.odin | 364 +++++++++++++------------------ code/ui.odin | 25 ++- code/ui_layout.odin | 4 +- code/ui_tests.odin | 231 ++++++++++++++++++++ code/ui_widgets.odin | 99 ++++++++- ols.json | 8 +- toolchain/Odin | 2 +- 17 files changed, 577 insertions(+), 246 deletions(-) diff --git a/code/api.odin b/code/api.odin index bddab54..ee363e1 100644 --- a/code/api.odin +++ b/code/api.odin @@ -190,6 +190,9 @@ startup :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem log( "Default font loaded" ) } + // Setup the app ui state + ui_startup( & app_ui, cache_allocator = persistent_slab_allocator() ) + // Demo project setup { using project @@ -274,6 +277,7 @@ reload :: proc( prof : ^SpallProfiler, persistent_mem, frame_mem, transient_mem, context.allocator = persistent_allocator() context.temp_allocator = transient_allocator() + Memory_App.state = get_state() using state // Procedure Addresses are not preserved on hot-reload. They must be restored for persistent data. diff --git a/code/env.odin b/code/env.odin index 963e6a3..b3843f1 100644 --- a/code/env.odin +++ b/code/env.odin @@ -10,6 +10,7 @@ import rl "vendor:raylib" Str_App_State := "App State" + Memory_App : Memory Memory_Base_Address_Persistent :: Terabyte * 1 @@ -177,6 +178,7 @@ State :: struct { config : AppConfig, app_window : AppWindow, + app_ui : UI_State, monitor_id : i32, monitor_refresh_hz : i32, @@ -203,8 +205,7 @@ State :: struct { } get_state :: proc "contextless" () -> ^ State { - // return cast( ^ State ) Memory_App.persistent.reserve_start - return Memory_App.state + return cast( ^ State ) Memory_App.persistent.reserve_start } AppWindow :: struct { @@ -223,8 +224,8 @@ ProjectConfig :: struct { } Project :: struct { - path : StringCached, - name : StringCached, + path : StrRunesPair, + name : StrRunesPair, config : ProjectConfig, codebase : CodeBase, @@ -242,7 +243,7 @@ Frame :: struct } Workspace :: struct { - name : StringCached, + name : StrRunesPair, cam : Camera, zoom_target : f32, diff --git a/code/grime_string_interning.odin b/code/grime_string_interning.odin index 28a3fca..d91335b 100644 --- a/code/grime_string_interning.odin +++ b/code/grime_string_interning.odin @@ -20,14 +20,14 @@ StringKey :: distinct u64 RunesCached :: []rune // TODO(Ed): Should this just track the key instead? (by default) -StringCached :: struct { +StrRunesPair :: struct { str : string, runes : []rune, } StringCache :: struct { slab : Slab, - table : HMapZPL(StringCached), + table : HMapZPL(StrRunesPair), } str_cache_init :: proc( /*allocator : Allocator*/ ) -> ( cache : StringCache ) { @@ -61,7 +61,7 @@ str_cache_init :: proc( /*allocator : Allocator*/ ) -> ( cache : StringCache ) { cache.slab, alloc_error = slab_init( & policy, allocator = persistent_allocator(), dbg_name = dbg_name ) verify(alloc_error == .None, "Failed to initialize the string cache" ) - cache.table, alloc_error = zpl_hmap_init_reserve( StringCached, persistent_allocator(), 1 * Megabyte, dbg_name ) + cache.table, alloc_error = zpl_hmap_init_reserve( StrRunesPair, persistent_allocator(), 1 * Megabyte, dbg_name ) return } @@ -69,7 +69,7 @@ str_cache_init :: proc( /*allocator : Allocator*/ ) -> ( cache : StringCache ) { // cache : ^StringCache, str_intern :: proc( content : string -) -> StringCached +) -> StrRunesPair { // profile(#procedure) cache := & get_state().string_cache @@ -97,8 +97,8 @@ str_intern :: proc( slab_validate_pools( get_state().persistent_slab ) - // result, alloc_error = zpl_hmap_set( & cache.table, key, StringCached { transmute(string) byte_slice(str_mem, length), runes } ) - result, alloc_error = zpl_hmap_set( & cache.table, key, StringCached { transmute(string) str_mem, runes } ) + // result, alloc_error = zpl_hmap_set( & cache.table, key, StrRunesPair { transmute(string) byte_slice(str_mem, length), runes } ) + result, alloc_error = zpl_hmap_set( & cache.table, key, StrRunesPair { transmute(string) str_mem, runes } ) verify( alloc_error == .None, "String cache had a backing allocator error" ) slab_validate_pools( get_state().persistent_slab ) @@ -108,7 +108,7 @@ str_intern :: proc( return (result ^) } -// runes_intern :: proc( content : []rune ) -> StringCached +// runes_intern :: proc( content : []rune ) -> StrRunesPair // { // cache := get_state().string_cache // } diff --git a/code/grime_unicode.odin b/code/grime_unicode.odin index 0380fa9..b3bc8b1 100644 --- a/code/grime_unicode.odin +++ b/code/grime_unicode.odin @@ -7,7 +7,7 @@ rune16 :: distinct u16 // Exposing the alloc_error @(require_results) -string_to_runes :: proc ( content : string, allocator := context.allocator) -> (runes : []rune, alloc_error : AllocatorError) { +string_to_runes :: proc ( content : string, allocator := context.allocator) -> (runes : []rune, alloc_error : AllocatorError) #optional_allocator_error { num := str_rune_count(content) runes, alloc_error = make([]rune, num, allocator) diff --git a/code/math.odin b/code/math.odin index 6d50006..cd2c124 100644 --- a/code/math.odin +++ b/code/math.odin @@ -77,6 +77,13 @@ Range2 :: struct #raw_union { x0, y0 : f32, x1, y1 : f32, }, + using side : struct { + left, bottom : f32, + right, top : f32, + }, + ratio : struct { + x, y : f32, + }, // TODO(Ed) : Test these array : [4]f32, diff --git a/code/math_pga2.odin b/code/math_pga2.odin index 9dc707e..9afbd9e 100644 --- a/code/math_pga2.odin +++ b/code/math_pga2.odin @@ -17,6 +17,8 @@ Rotor2 :: struct { rotor2_to_complex64 :: #force_inline proc( rotor : Rotor2 ) -> complex64 { return transmute(complex64) rotor; } +vec2 :: #force_inline proc "contextless" ( x, y : f32 ) -> Vec2 { return {x, y} } + dot_vec2 :: proc "contextless" ( a, b : Vec2 ) -> (s : f32) { x := a.x * b.x y := a.y + b.y diff --git a/code/parser_whitespace.odin b/code/parser_whitespace.odin index df8ee13..1390b3a 100644 --- a/code/parser_whitespace.odin +++ b/code/parser_whitespace.odin @@ -63,7 +63,7 @@ PWS_LexResult :: struct { PWS_Token :: struct { type : PWS_TokenType, line, column : u32, - content : StringCached, + content : StrRunesPair, } PWS_AST_Type :: enum u32 { @@ -80,7 +80,7 @@ PWS_AST :: struct { type : PWS_AST_Type, line, column : u32, - content : StringCached, + content : StrRunesPair, } PWS_ParseError :: struct { diff --git a/code/space.odin b/code/space.odin index e6d6c38..edc26f3 100644 --- a/code/space.odin +++ b/code/space.odin @@ -138,6 +138,14 @@ screen_size :: proc "contextless" () -> AreaSize { return transmute(AreaSize) ( extent * 2.0 ) } +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} + return range2( bottom_left, top_right ) +} + screen_get_corners :: #force_inline proc "contextless"() -> BoundsCorners2 { state := get_state(); using state screen_extent := state.app_window.extent diff --git a/code/text.odin b/code/text.odin index 85c63c6..a9c418b 100644 --- a/code/text.odin +++ b/code/text.odin @@ -66,7 +66,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 : StringCached, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default ) +draw_text_string_cached :: proc( content : StrRunesPair, pos : Vec2, size : f32, color : rl.Color = rl.WHITE, font : FontID = Font_Default ) { // profile(#procedure) state := get_state(); using state diff --git a/code/tick_render.odin b/code/tick_render.odin index e09bca3..eb35f8a 100644 --- a/code/tick_render.odin +++ b/code/tick_render.odin @@ -23,17 +23,42 @@ render :: 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 + //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 + } + + 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, } - profile("Render Screenspace") 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 } @@ -230,16 +255,19 @@ render_mode_2d :: proc() { 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 := 1 * cam_zoom_ratio // 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, style, 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, style, Color_Debug_UI_Content_Bounds, line_thickness ) } // profile_end() @@ -291,7 +319,7 @@ render_mode_2d :: proc() rl.DrawCircleV( world_to_screen_pos(cursor_world_pos), 5, Color_GreyRed ) } - // rl.DrawCircleV( { 0, 0 }, 1 * cam_zoom_ratio, Color_White ) + rl.DrawCircleV( { 0, 0 }, 1 * cam_zoom_ratio, Color_White ) rl.EndMode2D() } diff --git a/code/tick_update.odin b/code/tick_update.odin index 447ec89..afdb519 100644 --- a/code/tick_update.odin +++ b/code/tick_update.odin @@ -191,9 +191,9 @@ update :: proc( delta_time : f64 ) -> b32 } //endregion 2D Camera Manual Nav - //region Imgui Tick + //region WorkspaceImgui Tick { - profile("Imgui Tick") + profile("Workspace Imgui") // Creates the root box node, set its as the first parent. ui_graph_build( & state.project.workspace.ui ) @@ -238,235 +238,181 @@ update :: proc( delta_time : f64 ) -> b32 config.ui_resize_border_width = 2.5 // test_draggable() // test_text_box() + // test_parenting( & default_layout, & frame_style_default ) + // test_whitespace_ast( & default_layout, & frame_style_default ) - // test_parenting() - if false - { - // frame := ui_widget( "Frame", {} ) - // ui_parent(frame) - parent_layout := default_layout - parent_layout.size = range2( { 300, 300 }, {} ) - parent_layout.alignment = { 0.5, 0.5 } - 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 = { - // .Fixed_Position_X, .Fixed_Position_Y, - .Fixed_Width, .Fixed_Height, - } - ui_theme_via_style(parent_theme) - - parent := ui_widget( "Parent", { .Mouse_Clickable, .Mouse_Resizable }) - ui_parent(parent) - { - if parent.first_frame { - debug.draggable_box_pos = parent.style.layout.pos - debug.draggable_box_size = parent.style.layout.size.min - } - if parent.dragging { - debug.draggable_box_pos += mouse_world_delta() - } - if parent.resizing - { - og_layout := ui_context.active_start_style.layout - - center := debug.draggable_box_pos - original_distance := linalg.distance(ui.active_start_signal.cursor_pos, center) - cursor_distance := linalg.distance(parent.cursor_pos, center) - scale_factor := cursor_distance * (1 / original_distance) - - debug.draggable_box_size = og_layout.size.min * scale_factor - } - if (ui.hot == parent.key) && (ui.hot_resizable || ui.active_start_signal.resizing) { - parent.style.bg_color = Color_Blue - } - parent.style.layout.pos = debug.draggable_box_pos - parent.style.layout.size.min = debug.draggable_box_size - } - - child_layout := default_layout - child_layout.size = range2({ 0, 0 }, { 0, 0 }) - child_layout.alignment = { 0.5, 0.5 } - child_layout.margins = { 20, 20, 20, 20 } - 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 = { - // .Fixed_Width, .Fixed_Height, - .Origin_At_Anchor_Center - } - child_theme.layout = child_layout - ui_theme_via_style(child_theme) - child := ui_widget( "Child", { .Mouse_Clickable }) - } - - // Whitespace AST test + /* + 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 { - profile("Whitespace AST test") + fmt :: str_fmt_alloc - text_style := frame_style_default - text_style.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 } + @static bar_pos := Vec2 {} + bar_size := vec2( 400, 40 ) - 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 - - - alloc_error : AllocatorError; success : bool - // debug.lorem_content, success = os.read_entire_file( debug.path_lorem, frame_allocator() ) - - // debug.lorem_parse, alloc_error = pws_parser_parse( transmute(string) debug.lorem_content, frame_slab_allocator() ) - // verify( alloc_error == .None, "Faield to parse due to allocation failure" ) - - text_space := str_intern( " " ) - text_tab := str_intern( "\t") - - // index := 0 - widgets : Array(UI_Widget) - // widgets, alloc_error = array_init_reserve( UI_Widget, frame_slab_allocator(), 8 ) - widgets, alloc_error = array_init_reserve( UI_Widget, frame_slab_allocator(), 4 * Kilobyte ) - widgets_ptr := & widgets - - label_id := 0 - - line_id := 0 - for line in array_to_slice_num( debug.lorem_parse.lines ) + menu_bar : UI_Widget { - if line_id == 0 { - line_id += 1 - continue + 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) - ui_style_theme_set_layout( layout_text ) - line_hbox := ui_widget(str_fmt_alloc( "line %v", line_id ), {}) + style := UI_Style { + flags = { + // .Origin_At_Anchor_Center + .Fixed_Height + }, + bg_color = Color_Frame_Disabled, - if line_hbox.key == ui.hot - { - line_hbox.text = StringCached {} - ui_parent(line_hbox) + font = default_font, + font_size = 18 * (1.0/cam.zoom), + text_color = Color_White, - chunk_layout := layout_text - chunk_layout.alignment = { 0.0, 1.0 } - chunk_layout.anchor = range2({ 0.0, 0 }, { 0.0, 0 }) - chunk_layout.pos = {} - - 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 ) - - head := line.first - for ; head != nil; - { - ui_style_theme_set_layout( chunk_layout ) - widget : UI_Widget - - #partial switch head.type - { - case .Visible: - label := str_intern( str_fmt_alloc( "%v %v", head.content.str, label_id )) - widget = ui_text( label.str, head.content ) - label_id += 1 - - chunk_layout.pos.x += size_range2( widget.computed.bounds ).x - - case .Spaces: - label := str_intern( str_fmt_alloc( "%v %v", "space", label_id )) - widget = ui_text_spaces( label.str ) - label_id += 1 - - for idx in 1 ..< len( head.content.runes ) - { - // TODO(Ed): VIRTUAL WHITESPACE - // widget.style.layout.size.x += range2_size( widget.computed.bounds ) - } - chunk_layout.pos.x += size_range2( widget.computed.bounds ).x - - case .Tabs: - label := str_intern( str_fmt_alloc( "%v %v", "tab", label_id )) - widget = ui_text_tabs( label.str ) - label_id += 1 - - for idx in 1 ..< len( head.content.runes ) - { - // widget.style.layout.size.x += range2_size( widget.computed.bounds ) - } - chunk_layout.pos.x += size_range2( widget.computed.bounds ).x - } - - array_append( widgets_ptr, widget ) - head = head.next + 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}) } - - line_hbox.style.size.min.x = chunk_layout.pos.x } - else + 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 { - builder_backing : [16 * Kilobyte] byte - builder := str.builder_from_bytes( builder_backing[:] ) - - line_hbox.style.flags |= { .Size_To_Text } - - head := line.first.next - for ; head != nil; - { - str.write_string( & builder, head.content.str ) - head = head.next + move_box = ui_button("Move Box") + if move_box.dragging { + // bar_pos += mouse_world_delta() + bar_pos += state.input.mouse.delta } - - line_hbox.text = str_intern( to_string( builder ) ) - // if len(line_hbox.text.str) == 0 { - // line_hbox.text = str_intern( " " ) - // } } - 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 - } - else { - layout_text.pos.y += size_range2( (& widgets.data[ widgets.num - 1 ]).computed.bounds ).y + 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, + } } - line_id += 1 + // 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 + } } - label_id += 1 // Dummy action + @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 Imgui Tick + //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 diff --git a/code/ui.odin b/code/ui.odin index b264530..822b599 100644 --- a/code/ui.odin +++ b/code/ui.odin @@ -179,10 +179,18 @@ UI_StyleFlag :: enum u32 { Clamp_Position_Y, // Enroces the widget will maintain its size reguardless of any constraints - // Will override parent 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, @@ -244,8 +252,8 @@ UI_Box :: struct { // Cache ID key : UI_Key, // label : string, - label : StringCached, - text : StringCached, + label : StrRunesPair, + text : StrRunesPair, // Regenerated per frame. using links : DLL_NodeFull( UI_Box ), // first, last, prev, next @@ -574,3 +582,14 @@ ui_style_theme_set_layout :: proc ( layout : UI_Layout ) { 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 ae5c8ea..291f34e 100644 --- a/code/ui_layout.odin +++ b/code/ui_layout.odin @@ -153,8 +153,8 @@ ui_compute_layout :: proc() // Determine Content Bounds content_bounds := range2( - bounds.min + { layout.padding.left, layout.padding.bottom }, - bounds.max - { layout.padding.right, layout.padding.top }, + bounds.min + { layout.padding.left, layout.padding.bottom } + border_offset, + bounds.max - { layout.padding.right, layout.padding.top } - border_offset, ) computed.anchors = anchored_bounds diff --git a/code/ui_tests.odin b/code/ui_tests.odin index a19a052..f97daba 100644 --- a/code/ui_tests.odin +++ b/code/ui_tests.odin @@ -1,6 +1,7 @@ package sectr import "core:math/linalg" +import str "core:strings" test_hover_n_click :: proc() { @@ -67,6 +68,75 @@ test_draggable :: proc() draggable.style.layout.size.min = debug.draggable_box_size } +test_parenting :: proc( default_layout : ^UI_Layout, frame_style_default : ^UI_Style ) +{ + state := get_state(); using state + ui := ui_context + + // frame := ui_widget( "Frame", {} ) + // ui_parent(frame) + parent_layout := default_layout ^ + parent_layout.size = range2( { 300, 300 }, {} ) + parent_layout.alignment = { 0.5, 0.5 } + 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 = { + // .Fixed_Position_X, .Fixed_Position_Y, + .Fixed_Width, .Fixed_Height, + } + ui_theme_via_style(parent_theme) + + parent := ui_widget( "Parent", { .Mouse_Clickable, .Mouse_Resizable }) + ui_parent(parent) + { + if parent.first_frame { + debug.draggable_box_pos = parent.style.layout.pos + debug.draggable_box_size = parent.style.layout.size.min + } + if parent.dragging { + debug.draggable_box_pos += mouse_world_delta() + } + if parent.resizing + { + og_layout := ui_context.active_start_style.layout + + center := debug.draggable_box_pos + original_distance := linalg.distance(ui.active_start_signal.cursor_pos, center) + cursor_distance := linalg.distance(parent.cursor_pos, center) + scale_factor := cursor_distance * (1 / original_distance) + + debug.draggable_box_size = og_layout.size.min * scale_factor + } + if (ui.hot == parent.key) && (ui.hot_resizable || ui.active_start_signal.resizing) { + parent.style.bg_color = Color_Blue + } + parent.style.layout.pos = debug.draggable_box_pos + parent.style.layout.size.min = debug.draggable_box_size + } + + child_layout := default_layout ^ + child_layout.size = range2({ 0, 0 }, { 0, 0 }) + child_layout.alignment = { 0.5, 0.5 } + child_layout.margins = { 20, 20, 20, 20 } + 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 = { + // .Fixed_Width, .Fixed_Height, + .Origin_At_Anchor_Center + } + child_theme.layout = child_layout + ui_theme_via_style(child_theme) + child := ui_widget( "Child", { .Mouse_Clickable }) +} + test_text_box :: proc() { state := get_state(); using state @@ -95,3 +165,164 @@ test_text_box :: proc() text_box.style.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 ) +{ + profile("Whitespace AST test") + state := get_state(); using state + ui := ui_context + + text_style := frame_style_default ^ + text_style.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 + + + alloc_error : AllocatorError; success : bool + // debug.lorem_content, success = os.read_entire_file( debug.path_lorem, frame_allocator() ) + + // debug.lorem_parse, alloc_error = pws_parser_parse( transmute(string) debug.lorem_content, frame_slab_allocator() ) + // verify( alloc_error == .None, "Faield to parse due to allocation failure" ) + + text_space := str_intern( " " ) + text_tab := str_intern( "\t") + + // index := 0 + widgets : Array(UI_Widget) + // widgets, alloc_error = array_init_reserve( UI_Widget, frame_slab_allocator(), 8 ) + widgets, alloc_error = array_init_reserve( UI_Widget, frame_slab_allocator(), 4 * Kilobyte ) + widgets_ptr := & widgets + + label_id := 0 + + line_id := 0 + for line in array_to_slice_num( debug.lorem_parse.lines ) + { + if line_id == 0 { + line_id += 1 + continue + } + + ui_style_theme_set_layout( layout_text ) + line_hbox := ui_widget(str_fmt_alloc( "line %v", line_id ), {}) + + if line_hbox.key == ui.hot + { + line_hbox.text = StrRunesPair {} + ui_parent(line_hbox) + + chunk_layout := layout_text + chunk_layout.alignment = { 0.0, 1.0 } + chunk_layout.anchor = range2({ 0.0, 0 }, { 0.0, 0 }) + chunk_layout.pos = {} + + 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 ) + + head := line.first + for ; head != nil; + { + ui_style_theme_set_layout( chunk_layout ) + widget : UI_Widget + + #partial switch head.type + { + case .Visible: + label := str_intern( str_fmt_alloc( "%v %v", head.content.str, label_id )) + widget = ui_text( label.str, head.content ) + label_id += 1 + + chunk_layout.pos.x += size_range2( widget.computed.bounds ).x + + case .Spaces: + label := str_intern( str_fmt_alloc( "%v %v", "space", label_id )) + widget = ui_text_spaces( label.str ) + label_id += 1 + + for idx in 1 ..< len( head.content.runes ) + { + // TODO(Ed): VIRTUAL WHITESPACE + // widget.style.layout.size.x += range2_size( widget.computed.bounds ) + } + chunk_layout.pos.x += size_range2( widget.computed.bounds ).x + + case .Tabs: + label := str_intern( str_fmt_alloc( "%v %v", "tab", label_id )) + widget = ui_text_tabs( label.str ) + label_id += 1 + + for idx in 1 ..< len( head.content.runes ) + { + // widget.style.layout.size.x += range2_size( widget.computed.bounds ) + } + chunk_layout.pos.x += size_range2( widget.computed.bounds ).x + } + + array_append( widgets_ptr, widget ) + head = head.next + } + + line_hbox.style.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 } + + head := line.first.next + for ; head != nil; + { + str.write_string( & builder, head.content.str ) + head = head.next + } + + line_hbox.text = str_intern( to_string( builder ) ) + // if len(line_hbox.text.str) == 0 { + // line_hbox.text = str_intern( " " ) + // } + } + + 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 + } + else { + layout_text.pos.y += size_range2( (& widgets.data[ widgets.num - 1 ]).computed.bounds ).y + } + + line_id += 1 + } + + label_id += 1 // Dummy action +} diff --git a/code/ui_widgets.odin b/code/ui_widgets.odin index bab1883..c54aa5c 100644 --- a/code/ui_widgets.odin +++ b/code/ui_widgets.odin @@ -5,7 +5,6 @@ UI_Widget :: struct { using signal : UI_Signal, } - ui_widget :: proc( label : string, flags : UI_BoxFlags ) -> (widget : UI_Widget) { // profile(#procedure) @@ -15,7 +14,6 @@ ui_widget :: proc( label : string, flags : UI_BoxFlags ) -> (widget : UI_Widget) return } - ui_button :: proc( label : string, flags : UI_BoxFlags = {} ) -> (btn : UI_Widget) { // profile(#procedure) @@ -26,8 +24,76 @@ ui_button :: proc( label : string, flags : UI_BoxFlags = {} ) -> (btn : UI_Widge return } +//region Horizontal Box +/* +Horizontal Boxes automatically manage a collection of widgets and +attempt to slot them adjacent to each other along the x-axis. -ui_text :: proc( label : string, content : StringCached, flags : UI_BoxFlags = {} ) -> UI_Widget +The user must provide the direction that the hbox will append entries. +How the widgets will be scaled will be based on the individual entires style flags. + +All the usual behaviors that the style and box flags do apply when manage by the box widget. +Whether or not the horizontal box will scale the widget's width is if: +fixed size or "scale by ratio" flags are not used for the width. +The hbox will use the anchor's (range2) ratio.x value to determine the "stretch ratio". + +Keep in mind the stretch ratio is only respected if no size.min.x value is violated for each of the widgets. +*/ + +ui_hbox_begin :: proc( label : string, flags : UI_BoxFlags = {} +//, direction +) -> (widget : UI_Widget) { + // profile(#procedure) + + widget.box = ui_box_make( flags, label ) + widget.signal = ui_signal_from_box( widget.box ) + return +} +ui_hbox_end :: proc( hbox : UI_Widget ) -> UI_Widget { + hbox_width := hbox.computed.content.max.y - hbox.computed.content.min.y + + // 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 + 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 + + size_req_children += width + continue + } + + size_req_children += size.min.x + continue + } + + + } + availble_flexible_space := hbox_width - size_req_children + return hbox +} +ui_hbox_auto_end :: proc( vbox : UI_Widget ) { + ui_hbox_end(vbox) + ui_parent_pop() +} + +@(deferred_out = ui_hbox_end) +ui_hbox :: #force_inline proc( label : string, flags : UI_BoxFlags = {} ) -> (widget : UI_Widget) { + widget = ui_hbox_begin(label, flags) + ui_parent(widget) + return +} + +//endregion Horizontal Box + +ui_text :: proc( label : string, content : StrRunesPair, flags : UI_BoxFlags = {} ) -> UI_Widget { // profile(#procedure) state := get_state(); using state @@ -39,7 +105,6 @@ ui_text :: proc( label : string, content : StringCached, flags : UI_BoxFlags = { return { box, signal } } - ui_text_spaces :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget { // profile(#procedure) @@ -55,7 +120,6 @@ ui_text_spaces :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget return { box, signal } } - ui_text_tabs :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget { // profile(#procedure) @@ -70,3 +134,28 @@ ui_text_tabs :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget box.text = tab_str return { box, signal } } + +ui_vbox_begin :: proc( label : string, flags : UI_BoxFlags = {} ) -> (widget : UI_Widget) { + // profile(#procedure) + + widget.box = ui_box_make( flags, label ) + // widget.signal = ui_signal_from_box( widget.box ) + return +} +ui_vbox_end :: proc( hbox : UI_Widget ) -> UI_Widget { + // do layout calculations for the children + return hbox +} +ui_vbox_auto_end :: proc( hbox : UI_Widget ) { + ui_vbox_end(hbox) + ui_parent_pop() +} + +// ui_vbox_append( widget : UI_Widget ) + +@(deferred_out = ui_vbox_auto_end) +ui_vbox :: #force_inline proc( label : string, flags : UI_BoxFlags = {} ) -> (widget : UI_Widget) { + widget = ui_vbox_begin(label, flags) + ui_parent_push(widget) + return +} diff --git a/ols.json b/ols.json index dfb0f32..5f02641 100644 --- a/ols.json +++ b/ols.json @@ -14,12 +14,8 @@ "path": "C:/projects/SectrPrototype/code" }, { - "name": "ini", - "path": "C:/projects/SectrPrototype/thirdparty/ini" - }, - { - "name": "backtrace", - "path": "C:/projects/SectrPrototype/thirdparty/backtrace" + "name": "thirdparty", + "path": "C:/projects/SectrPrototype/thirdparty" } ], "odin_command": "C:/projects/SectrPrototype/toolchain/Odin/odin.exe", diff --git a/toolchain/Odin b/toolchain/Odin index 647d7ed..373733f 160000 --- a/toolchain/Odin +++ b/toolchain/Odin @@ -1 +1 @@ -Subproject commit 647d7ed9e3e07b8b248d3b56eaa8fa60b451e1c9 +Subproject commit 373733fb2b410cd51b4d674b09f5ed9e38677c99