diff --git a/code/colors.odin b/code/colors.odin index 26768f6..da007a3 100644 --- a/code/colors.odin +++ b/code/colors.odin @@ -15,8 +15,8 @@ Color_BG_TextBox_Green :: Color { 102, 102, 110, 255 } Color_Frame_Disabled :: Color { 22, 22, 22, 120 } Color_Frame_Hover :: Color { 122, 122, 125, 200 } Color_Frame_Select :: Color { 188, 188, 188, 220 } -Color_GreyRed :: Color { 220, 100, 100, 125 } -Color_White_A125 :: Color { 255, 255, 255, 125 } +Color_GreyRed :: Color { 220, 100, 100, 165 } +Color_White_A125 :: Color { 255, 255, 255, 165 } -Color_Debug_UI_Padding_Bounds :: Color { 40, 195, 170, 125 } -Color_Debug_UI_Content_Bounds :: Color { 195, 40, 170, 125 } +Color_Debug_UI_Padding_Bounds :: Color { 40, 195, 170, 160 } +Color_Debug_UI_Content_Bounds :: Color { 170, 120, 240, 160 } diff --git a/code/env.odin b/code/env.odin index fe3c4d9..11de102 100644 --- a/code/env.odin +++ b/code/env.odin @@ -250,6 +250,13 @@ DebugData :: struct { 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, diff --git a/code/tick_render.odin b/code/tick_render.odin index 3c70207..2295774 100644 --- a/code/tick_render.odin +++ b/code/tick_render.odin @@ -147,6 +147,8 @@ render_mode_2d :: proc() style := current.style computed := & current.computed + computed_size := computed.bounds.p1 - computed.bounds.p0 + if ! within_range2( view_bounds, computed.bounds ) { continue } @@ -228,7 +230,7 @@ render_mode_2d :: proc() { // profile("Resize Bounds") resize_border_width := cast(f32) get_state().config.ui_resize_border_width - resize_percent_width := style.size * (resize_border_width * 1.0/ 200.0) + 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 })) @@ -249,8 +251,14 @@ render_mode_2d :: proc() point_radius := 3 * cam_zoom_ratio // profile_begin("circles") - // rl.DrawCircleV( render_bounds.p0, point_radius, Color_Red ) - // rl.DrawCircleV( render_bounds.p1, point_radius, Color_Blue ) + // center := Vec2 { + // render_bounds.p0.x + computed_size.x * 0.5, + // render_bounds.p0.y - computed_size.y * 0.5, + // } + // rl.DrawCircleV( center, point_radius, Color_White ) + + rl.DrawCircleV( render_bounds.p0, point_radius, Color_Red ) + rl.DrawCircleV( render_bounds.p1, point_radius, Color_Blue ) // profile_end() if len(current.text.str) > 0 { diff --git a/code/tick_update.odin b/code/tick_update.odin index adeaad1..1d275a3 100644 --- a/code/tick_update.odin +++ b/code/tick_update.odin @@ -207,9 +207,10 @@ update :: proc( delta_time : f64 ) -> b32 anchor = {}, alignment = { 0.0, 0.0 }, text_alignment = { 0.0, 0.0 }, - corner_radii = { 0.2, 0.2, 0.2, 0.2 }, + // corner_radii = { 0.2, 0.2, 0.2, 0.2 }, pos = { 0, 0 }, - size = { 200, 200 }, + size = range2( { 1000, 1000 }, {}), + // padding = { 20, 20, 20, 20 } } frame_style_default := UI_Style { @@ -230,8 +231,8 @@ update :: proc( delta_time : f64 ) -> b32 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 + // frame_theme.hot.bg_color = Color_Frame_Hover + frame_theme.active.bg_color = Color_Frame_Select ui_style_theme( frame_theme ) config.ui_resize_border_width = 2.5 @@ -241,42 +242,72 @@ update :: proc( delta_time : f64 ) -> b32 // test_parenting() if true { + frame := ui_widget( "Frame", {} ) + ui_parent(frame) + parent_layout := default_layout - parent_layout.size = { 300, 300 } + parent_layout.size = range2( { 300, 300 }, {} ) parent_layout.alignment = { 0.0, 0.0 } + parent_layout.padding = {} + parent_layout.pos = { 0, 0 } + parent_theme := frame_style_default + parent_theme.layout = parent_layout + parent_theme.flags = { + .Fixed_Width, .Fixed_Height, + } + ui_theme_via_style(parent_theme) - ui_style_theme_set_layout( parent_layout ) - parent := ui_widget( "Parent", { .Mouse_Clickable }) + parent := ui_widget( "Parent", { .Mouse_Clickable, .Mouse_Resizable }) ui_parent(parent) { if parent.first_frame { - debug.draggable_box_pos = parent.style.layout.pos + { 0, -100 } - debug.draggable_box_size = parent.style.layout.size + debug.draggable_box_pos = parent.style.layout.pos + { 0, 0 } + 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 = { 100, 100 } - child_layout.alignment = { 0.5, 0.5 } - child_layout.margins = { 20, 20, 20, 20 } - child_layout.anchor = range2( { 0.5, 0.5 }, { 0.5, 0.5 }) + child_layout.size = range2({ 50, 50 }, { 200, 200 }) + child_layout.alignment = { 0.0, 0.0 } + child_layout.margins = { 00, 00, 00, 00 } + child_layout.padding = {} + child_layout.anchor = range2({ 0.0, 0.0 }, { 0.0, 0.0 }) + child_layout.pos = { 0, 0 } child_theme := frame_style_default - // child_theme.flags = {} + child_theme.flags = { + // .Fixed_Width, .Fixed_Height, + } child_theme.layout = child_layout ui_theme_via_style(child_theme) child := ui_widget( "Child", { .Mouse_Clickable }) } // Whitespace AST test - if true + if false { profile("Whitespace AST test") @@ -292,10 +323,10 @@ update :: proc( delta_time : f64 ) -> b32 text_style, text_style, }} - text_theme.default.bg_color = Color_Transparent + 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 + text_theme.hot.bg_color = Color_Frame_Hover + text_theme.active.bg_color = Color_Frame_Select layout_text := default_layout diff --git a/code/ui.odin b/code/ui.odin index e5f3bf8..8dcd981 100644 --- a/code/ui.odin +++ b/code/ui.odin @@ -54,6 +54,7 @@ UI_AnchorPresets :: enum u32 { UI_BoxFlag :: enum u64 { Disabled, + Focusable, Click_To_Focus, @@ -137,23 +138,20 @@ UI_Layout :: struct { margins : UI_LayoutSide, padding : UI_LayoutSide, + // TODO(Ed): We cannot support individual corners unless we add it to raylib (or finally change the rendering backend) corner_radii : [Corner.Count]f32, // Position in relative coordinate space. // If the box's flags has Fixed_Position, then this will be its aboslute position in the relative coordinate space pos : Vec2, - // TODO(Ed) : Should everything no matter what its parent is use a WS_Pos instead of a raw vector pos? // TODO(Ed): Support a min/max range for the size of a box - size : Vec2, - // size : Range2 + 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, - - size_to_text : b8, - // TODO(Ed) : Add support for size_to_content? - // size_to_content : b8, + // tile_pos : WS_Pos, } UI_Signal :: struct { @@ -175,11 +173,22 @@ UI_Signal :: struct { } 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 widget will always remain in a specific position relative to the parent. + // Overriding the anchors and margins. Fixed_Position_X, Fixed_Position_Y, + + // Enroces the widget will maintain its size reguardless of any constraints + // Will override parent constraints Fixed_Width, Fixed_Height, + Size_To_Text, Text_Wrap, Count, @@ -200,9 +209,11 @@ UI_Style :: struct { bg_color : Color, border_color : Color, + // TODO(Ed) : Add support for this eventually blur_size : f32, font : FontID, + // TODO(Ed): Should this get moved to the layout struct? Techncially font-size is mainly font_size : f32, text_color : Color, @@ -210,6 +221,8 @@ UI_Style :: struct { 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, } diff --git a/code/ui_layout.odin b/code/ui_layout.odin index 7fdf1a5..3d0e683 100644 --- a/code/ui_layout.odin +++ b/code/ui_layout.odin @@ -1,5 +1,8 @@ package sectr +import "core:math" +import "core:math/linalg" + ui_compute_layout :: proc() { profile(#procedure) @@ -13,7 +16,7 @@ ui_compute_layout :: proc() layout := & style.layout bounds.min = layout.pos - bounds.max = layout.size + bounds.max = layout.size.min computed.content = bounds^ computed.padding = {} @@ -30,85 +33,201 @@ ui_compute_layout :: proc() style := current.style layout := & style.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 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) + + size_to_text : bool = .Size_To_Text in style.flags + + margins := range2( { layout.margins.left, -layout.margins.top }, { -layout.margins.right, layout.margins.bottom }, ) - margined_bounds := range2( parent_content.p0 + margins.p0, parent_content.p1 + margins.p1, ) - - margined_size := margined_bounds.p1 - margined_bounds.p0 - - anchored_bounds := range2( - margined_bounds.p0 + margined_size * layout.anchor.p0, - margined_bounds.p0 + margined_size * layout.anchor.p1, - ) - - anchored_size := Vec2 { - anchored_bounds.max.x - anchored_bounds.min.x, - anchored_bounds.max.y - anchored_bounds.min.y, - } + margined_size := linalg.abs(margined_bounds.p1 - margined_bounds.p0) anchor := & layout.anchor - pos : Vec2 - if UI_StyleFlag.Fixed_Position_X in style.flags { - pos.x = layout.pos.x - pos.x += anchored_bounds.p0.x + // Margins + Anchors Applied + adjusted_bounds := range2( + { margined_bounds.p0.x + margined_size.x * anchor.p0.x, margined_bounds.p0.y + margined_size.y * anchor.p0.y }, + { margined_bounds.p1.x + margined_size.x * anchor.p1.x, margined_bounds.p1.y + margined_size.y * anchor.p1.y }, + ) + adjusted_bounds_size := linalg.abs(adjusted_bounds.p1 - adjusted_bounds.p0) + + // Resolves final constrained bounds of the parent for the child box + // Will be applied to the box after the child's positon is resolved. + + fixed_pos := Vec2 { fixed_pos_x, fixed_pos_y } + constraint_min := adjusted_bounds.min //* (1 - fixed_pos) + parent_content.min * fixed_pos + constraint_max := adjusted_bounds.max //* (1 - fixed_pos) + parent_content.max * fixed_pos + + // constraint_min_x := adjusted_bounds.min.x //* (1 - fixed_pos_x) + parent_content.min.x * fixed_pos_x + // constraint_min_y := adjusted_bounds.min.y //* (1 - fixed_pos_y) + parent_content.min.y * fixed_pos_y + // constraint_max_x := adjusted_bounds.max.x //* (1 - fixed_pos_x) + parent_content.max.x * fixed_pos_x + // constraint_max_y := adjusted_bounds.max.y //* (1 - fixed_pos_y) + parent_content.max.y * fixed_pos_y + + constrained_bounds := range2( + constraint_min, + constraint_max, + // { constraint_min_x, constraint_min_y }, + // { constraint_max_x, constraint_max_y }, + ) + constrained_size := linalg.abs(constrained_bounds.p1 - constrained_bounds.p0) + + + /* + If fixed position (X or Y): + * Ignore Margins + * Ignore Anchors + + 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 not 0: + * Allow the child box to spread to entire adjusted content bounds. + */ + + size_unit_bounds := range2( + { 0.0, 0.0 }, + { 1.0, -1.0 }, + ) + + alignment := layout.alignment + aligned_unit_bounds := range2( + size_unit_bounds.p0 + { -alignment.x, alignment.y }, + size_unit_bounds.p1 - { alignment.x, -alignment.y }, + ) + + wtf := range2( + { constrained_bounds.p0.x, constrained_bounds.p0.y }, + { constrained_bounds.p1.x, constrained_bounds.p1.y }, + ) + + // projected_bounds := range2( + // aligned_unit_bounds.p0 * wtf.p0, + // aligned_unit_bounds.p1 * wtf.p1, + // ) + + + constrained_half_size := constrained_size * 0.5 + min_half_size := layout.size.min * 0.5 + max_half_size := layout.size.max * 0.5 + half_size := linalg.max( constrained_half_size, min_half_size ) + half_size = linalg.min( half_size, max_half_size ) + + projected_bounds := range2( + aligned_unit_bounds.p0 * half_size, + aligned_unit_bounds.p1 * half_size, + ) + + rel_projected_bounds := range2( + layout.pos + projected_bounds.p0, + layout.pos + projected_bounds.p1, + ) + + bounds : Range2 + + // Resolve and apply the size constraint based off of positon of box and the constrained bounds + + // Check to see if left or right side is over + if ! (.Fixed_Width in style.flags) + { + bounds.p0.x = rel_projected_bounds.p0.x < constrained_bounds.p0.x ? constrained_bounds.p0.x : rel_projected_bounds.p0.x + bounds.p1.x = rel_projected_bounds.p1.x > constrained_bounds.p1.x ? constrained_bounds.p1.x : rel_projected_bounds.p1.x } - if UI_StyleFlag.Fixed_Position_Y in style.flags { - pos.y = layout.pos.y - pos.y += anchored_bounds.p0.y + else { + size_unit_bounds := range2( + { 0.0, 0.0 }, + { 1.0, -1.0 }, + ) + + alignment := layout.alignment + aligned_unit_bounds := range2( + size_unit_bounds.p0 + { -alignment.x, alignment.y }, + size_unit_bounds.p1 - { alignment.x, -alignment.y }, + ) + + // Apply size.p0.x directly + bounds.p0.x = aligned_unit_bounds.p0.x * layout.size.min.x + bounds.p1.x = aligned_unit_bounds.p1.x * layout.size.min.x + + bounds.p0.x += constrained_bounds.p0.x + bounds.p1.x += constrained_bounds.p0.x + + bounds.p0.x += layout.pos.x + bounds.p1.x += layout.pos.x } + if ! (.Fixed_Height in style.flags) + { + bounds.p0.y = rel_projected_bounds.p0.y > constrained_bounds.p0.y ? constrained_bounds.p0.y : rel_projected_bounds.p0.y + bounds.p1.y = rel_projected_bounds.p1.y < constrained_bounds.p1.y ? constrained_bounds.p1.y : rel_projected_bounds.p1.y + } + else { + size_unit_bounds := range2( + { 0.0, 0.0 }, + { 1.0, -1.0 }, + ) + + alignment := layout.alignment + aligned_unit_bounds := range2( + size_unit_bounds.p0 + { -alignment.x, alignment.y }, + size_unit_bounds.p1 - { alignment.x, -alignment.y }, + ) + + // Apply size.p0.y directly + bounds.p0.y = aligned_unit_bounds.p0.y * layout.size.min.y + bounds.p1.y = aligned_unit_bounds.p1.y * layout.size.min.y + + bounds.p0.y += constrained_bounds.p0.y //+ aligned_unit_bounds + bounds.p1.y += constrained_bounds.p0.y //+ aligned_unit_bounds + + bounds.p0.y += layout.pos.y + bounds.p1.y += layout.pos.y + } + + // Enforce the min/max size + bounds_size := bounds.p1 - bounds.p0 + // if bounds_size > layout.size.max { + // Enforce max + + + // } + + text_size : Vec2 - // If the computed matches, we alreayd have the size, don't bother. - // if computed.text_size.y == style.font_size { - if current.first_frame || ! style.size_to_text || computed.text_size.y != size_range2(computed.bounds).y { + // If the computed matches, we already have the size, don't bother. + if current.first_frame || ! size_to_text || computed.text_size.y != size_range2(computed.bounds).y { text_size = cast(Vec2) measure_text_size( current.text.str, style.font, style.font_size, 0 ) } else { text_size = computed.text_size } - - size : Vec2 - if UI_StyleFlag.Fixed_Width in style.flags { - size.x = layout.size.x - } - else { - size.x = anchored_size.x + if size_to_text { + // size = text_size } - if UI_StyleFlag.Fixed_Height in style.flags { - size.y = layout.size.y - } - else { - size.y = anchored_size.y - } - if style.size_to_text { - size = text_size - } - - half_size := size * 0.5 - size_bounds := range2( - Vec2 {}, - { size.x, -size.y }, - ) - - aligned_bounds := range2( - size_bounds.p0 + size * { -layout.alignment.x, layout.alignment.y }, - size_bounds.p1 - size * { layout.alignment.x, -layout.alignment.y }, - ) - - bounds := & computed.bounds - (bounds^) = aligned_bounds - (bounds^) = range2( - pos + aligned_bounds.p0, - pos + aligned_bounds.p1, - ) + computed.bounds = bounds border_offset := Vec2 { layout.border_width, layout.border_width } padding := & computed.padding diff --git a/code/ui_signal.odin b/code/ui_signal.odin index 7fb734f..31f08f9 100644 --- a/code/ui_signal.odin +++ b/code/ui_signal.odin @@ -15,8 +15,10 @@ ui_signal_from_box :: proc ( box : ^ UI_Box ) -> UI_Signal signal.cursor_pos = ui_cursor_pos() signal.cursor_over = cast(b8) pos_within_range2( signal.cursor_pos, box.computed.bounds ) + computed_size := box.computed.bounds.p1 - box.computed.bounds.p0 + resize_border_width := cast(f32) get_state().config.ui_resize_border_width - resize_percent_width := box.style.size * (resize_border_width * 1.0/ 200.0) + resize_percent_width := computed_size * (resize_border_width * 1.0/ 200.0) resize_border_non_range := add(box.computed.bounds, range2( { resize_percent_width.x, -resize_percent_width.x }, { -resize_percent_width.x, resize_percent_width.x })) diff --git a/code/ui_tests.odin b/code/ui_tests.odin index 4bc391b..5795a6a 100644 --- a/code/ui_tests.odin +++ b/code/ui_tests.odin @@ -31,14 +31,14 @@ test_draggable :: proc() // alignment = { 1.0, 1.0 }, // corner_radii = { 0.3, 0.3, 0.3, 0.3 }, pos = { 0, 0 }, - size = { 200, 200 }, + size = range2({ 200, 200 }, {}), } ui_style_theme_set_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 + debug.draggable_box_size = draggable.style.layout.size.min } // Dragging @@ -56,7 +56,7 @@ test_draggable :: proc() cursor_distance := linalg.distance(draggable.cursor_pos, center) scale_factor := cursor_distance * (1 / original_distance) - debug.draggable_box_size = og_layout.size * scale_factor + debug.draggable_box_size = og_layout.size.min * scale_factor } if (ui.hot == draggable.key) && (ui.hot_resizable || ui.active_start_signal.resizing) { @@ -64,7 +64,7 @@ test_draggable :: proc() } draggable.style.layout.pos = debug.draggable_box_pos - draggable.style.layout.size = debug.draggable_box_size + draggable.style.layout.size.min = debug.draggable_box_size } test_text_box :: proc() diff --git a/code/ui_util.odin b/code/ui_util.odin new file mode 100644 index 0000000..7ba6dc6 --- /dev/null +++ b/code/ui_util.odin @@ -0,0 +1,3 @@ +package sectr + + diff --git a/code/ui_widgets.odin b/code/ui_widgets.odin index 5c7fa5b..1098e51 100644 --- a/code/ui_widgets.odin +++ b/code/ui_widgets.odin @@ -27,46 +27,41 @@ ui_button :: proc( label : string, flags : UI_BoxFlags = {} ) -> (btn : UI_Widge ui_text :: proc( label : string, content : StringCached, flags : UI_BoxFlags = {} ) -> UI_Widget { // profile(#procedure) - state := get_state(); using state box := ui_box_make( flags, label ) signal := ui_signal_from_box( box ) box.text = content - box.style.layout.size_to_text = true return { box, signal } } ui_space :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget { // profile(#procedure) - - space_str := str_intern( " " ) - state := get_state(); using state + // TODO(Ed) : Move this somwhere in state. + space_str := str_intern( " " ) + box := ui_box_make( flags, label ) signal := ui_signal_from_box( box ) box.text = space_str - box.style.layout.size_to_text = true return { box, signal } } ui_tab :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget { // profile(#procedure) + state := get_state(); using state + // TODO(Ed) : Move this somwhere in state. tab_str := str_intern( "\t" ) - state := get_state(); using state - box := ui_box_make( flags, label ) signal := ui_signal_from_box( box ) box.text = tab_str - - box.style.layout.size_to_text = true return { box, signal } }