Restructured the codebase yet again but this time with compiler support for monlithic packages
So no need to stage generate symbolic links in a flat directory for the compiler
This commit is contained in:
6
code/sectr/ui/canvas.odin
Normal file
6
code/sectr/ui/canvas.odin
Normal file
@ -0,0 +1,6 @@
|
||||
package sectr
|
||||
|
||||
// Specialization of floating that allows panning across the viewport in the space (space isn't clampped to a specific size))
|
||||
UI_Canvas :: struct {
|
||||
using floating : UI_Floating,
|
||||
}
|
262
code/sectr/ui/core.odin
Normal file
262
code/sectr/ui/core.odin
Normal file
@ -0,0 +1,262 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
// TODO(Ed) : This is in Raddbg base_types.h, consider moving outside of UI.
|
||||
|
||||
Corner :: enum i32 {
|
||||
Invalid = -1,
|
||||
_00,
|
||||
_01,
|
||||
_10,
|
||||
_11,
|
||||
TopLeft = _00,
|
||||
TopRight = _01,
|
||||
BottomLeft = _10,
|
||||
BottomRight = _11,
|
||||
Count = 4,
|
||||
}
|
||||
|
||||
Side :: enum i32 {
|
||||
Invalid = -1,
|
||||
Min = 0,
|
||||
Max = 1,
|
||||
Count
|
||||
}
|
||||
|
||||
// Side2 :: enum u32 {
|
||||
// Top,
|
||||
// Bottom,
|
||||
// Left,
|
||||
// Right,
|
||||
// Count,
|
||||
// }
|
||||
|
||||
// UI_AnchorPresets :: enum u32 {
|
||||
// Top_Left,
|
||||
// Top_Right,
|
||||
// Bottom_Right,
|
||||
// Bottom_Left,
|
||||
// Center_Left,
|
||||
// Center_Top,
|
||||
// Center_Right,
|
||||
// Center_Bottom,
|
||||
// Center,
|
||||
// Left_Wide,
|
||||
// Top_Wide,
|
||||
// Right_Wide,
|
||||
// Bottom_Wide,
|
||||
// VCenter_Wide,
|
||||
// HCenter_Wide,
|
||||
// Full,
|
||||
// Count,
|
||||
// }
|
||||
|
||||
UI_Cursor :: struct {
|
||||
placeholder : int,
|
||||
}
|
||||
|
||||
UI_FramePassKind :: enum {
|
||||
Generate,
|
||||
Compute,
|
||||
Logical,
|
||||
}
|
||||
|
||||
UI_InteractState :: struct {
|
||||
hot_time : f32,
|
||||
active_time : f32,
|
||||
disabled_time : f32,
|
||||
}
|
||||
|
||||
UI_Key :: distinct u64
|
||||
|
||||
UI_Scalar :: f32
|
||||
|
||||
UI_ScalarConstraint :: struct {
|
||||
min, max : UI_Scalar,
|
||||
}
|
||||
|
||||
UI_Scalar2 :: [Axis2.Count]UI_Scalar
|
||||
|
||||
|
||||
// UI_BoxFlags_Stack_Size :: 512
|
||||
UI_Layout_Stack_Size :: 512
|
||||
UI_Style_Stack_Size :: 512
|
||||
UI_Parent_Stack_Size :: 512
|
||||
// UI_Built_Boxes_Array_Size :: 8
|
||||
UI_Built_Boxes_Array_Size :: 128 * Kilobyte
|
||||
|
||||
UI_State :: struct {
|
||||
// TODO(Ed) : Use these
|
||||
// build_arenas : [2]Arena,
|
||||
// build_arena : ^ Arena,
|
||||
|
||||
built_box_count : i32,
|
||||
|
||||
caches : [2] HMapZPL( UI_Box ),
|
||||
prev_cache : ^HMapZPL( UI_Box ),
|
||||
curr_cache : ^HMapZPL( UI_Box ),
|
||||
|
||||
render_queue : Array(UI_RenderBoxInfo),
|
||||
|
||||
null_box : ^UI_Box, // This was used with the Linked list interface...
|
||||
// TODO(Ed): Should we change our convention for null boxes to use the above and nil as an invalid state?
|
||||
root : ^UI_Box,
|
||||
// Children of the root node are unique in that they have their order preserved per frame
|
||||
// This is to support overlapping frames
|
||||
// So long as their parent-index is non-negative they'll be rendered
|
||||
|
||||
// Do we need to recompute the layout?
|
||||
// layout_dirty : b32,
|
||||
|
||||
// TODO(Ed) : Look into using a build arena like Ryan does for these possibly (and thus have a linked-list stack)
|
||||
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,
|
||||
hot_start_style : UI_Style,
|
||||
|
||||
active_mouse : [MouseBtn.count] UI_Key,
|
||||
active : UI_Key,
|
||||
active_start_signal : UI_Signal,
|
||||
|
||||
clipboard_copy : UI_Key,
|
||||
last_clicked : UI_Key,
|
||||
|
||||
active_start_style : UI_Style,
|
||||
|
||||
last_pressed_key : [MouseBtn.count] UI_Key,
|
||||
last_pressed_key_us : [MouseBtn.count] f32,
|
||||
}
|
||||
|
||||
ui_startup :: proc( ui : ^ UI_State, cache_allocator : Allocator /* , cache_reserve_size : u64 */ )
|
||||
{
|
||||
ui := ui
|
||||
ui^ = {}
|
||||
|
||||
for & cache in ui.caches {
|
||||
box_cache, allocation_error := zpl_hmap_init_reserve( UI_Box, cache_allocator, UI_Built_Boxes_Array_Size )
|
||||
verify( allocation_error == AllocatorError.None, "Failed to allocate box cache" )
|
||||
cache = box_cache
|
||||
}
|
||||
ui.curr_cache = (& ui.caches[1])
|
||||
ui.prev_cache = (& ui.caches[0])
|
||||
|
||||
allocation_error : AllocatorError
|
||||
ui.render_queue, allocation_error = array_init_reserve( UI_RenderBoxInfo, cache_allocator, UI_Built_Boxes_Array_Size, fixed_cap = true )
|
||||
verify( allocation_error == AllocatorError.None, "Failed to allocate render queue" )
|
||||
|
||||
log("ui_startup completed")
|
||||
}
|
||||
|
||||
ui_reload :: proc( ui : ^ UI_State, cache_allocator : Allocator )
|
||||
{
|
||||
// We need to repopulate Allocator references
|
||||
for & cache in ui.caches {
|
||||
zpl_hmap_reload( & cache, cache_allocator)
|
||||
}
|
||||
ui.render_queue.backing = cache_allocator
|
||||
}
|
||||
|
||||
// TODO(Ed) : Is this even needed?
|
||||
ui_shutdown :: proc() {
|
||||
}
|
||||
|
||||
ui_cursor_pos :: #force_inline proc "contextless" () -> Vec2 {
|
||||
using state := get_state()
|
||||
if ui_context == & state.project.workspace.ui {
|
||||
return screen_to_ws_view_pos( input.mouse.pos )
|
||||
}
|
||||
else {
|
||||
return input.mouse.pos
|
||||
}
|
||||
}
|
||||
|
||||
ui_drag_delta :: #force_inline proc "contextless" () -> Vec2 {
|
||||
using state := get_state()
|
||||
return ui_cursor_pos() - state.ui_context.active_start_signal.cursor_pos
|
||||
}
|
||||
|
||||
ui_graph_build_begin :: proc( ui : ^ UI_State, bounds : Vec2 = {} )
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
state := get_state()
|
||||
get_state().ui_context = ui
|
||||
using get_state().ui_context
|
||||
|
||||
stack_clear( & layout_combo_stack )
|
||||
stack_clear( & style_combo_stack )
|
||||
array_clear( render_queue )
|
||||
|
||||
curr_cache, prev_cache = swap( curr_cache, prev_cache )
|
||||
|
||||
if ui.active == UI_Key(0) {
|
||||
//ui.hot = UI_Key(0)
|
||||
ui.active_start_signal = {}
|
||||
}
|
||||
|
||||
ui.built_box_count = 0
|
||||
root = ui_box_make( {}, str_intern(str_fmt_tmp("%s: root#001", ui == & state.screen_ui ? "Screen" : "Workspace" )).str)
|
||||
if ui == & state.screen_ui {
|
||||
root.layout.size = range2(Vec2(state.app_window.extent) * 2, {})
|
||||
}
|
||||
ui_parent_push(root)
|
||||
}
|
||||
|
||||
ui_graph_build_end :: proc( ui : ^UI_State )
|
||||
{
|
||||
profile(#procedure)
|
||||
|
||||
ui_parent_pop() // Should be ui_context.root
|
||||
|
||||
// Regenerate the computed layout if dirty
|
||||
ui_compute_layout( ui )
|
||||
|
||||
get_state().ui_context = nil
|
||||
}
|
||||
|
||||
@(deferred_in = ui_graph_build_end)
|
||||
ui_graph_build :: #force_inline proc( ui : ^ UI_State ) { ui_graph_build_begin( ui ) }
|
||||
|
||||
ui_key_from_string :: #force_inline proc "contextless" ( value : string ) -> UI_Key
|
||||
{
|
||||
// profile(#procedure)
|
||||
USE_RAD_DEBUGGERS_METHOD :: true
|
||||
|
||||
key : UI_Key
|
||||
|
||||
when USE_RAD_DEBUGGERS_METHOD {
|
||||
hash : u64
|
||||
for str_byte in transmute([]byte) value {
|
||||
hash = ((hash << 5) + hash) + u64(str_byte)
|
||||
}
|
||||
key = cast(UI_Key) hash
|
||||
}
|
||||
|
||||
when ! USE_RAD_DEBUGGERS_METHOD {
|
||||
key = cast(UI_Key) crc32( transmute([]byte) value )
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
ui_parent_push :: #force_inline proc( ui : ^ UI_Box ) { stack_push( & ui_context().parent_stack, ui ) }
|
||||
ui_parent_pop :: #force_inline proc() { stack_pop( & get_state().ui_context.parent_stack ) }
|
||||
|
||||
@(deferred_none = ui_parent_pop)
|
||||
ui_parent :: #force_inline proc( ui : ^UI_Box) { ui_parent_push( ui ) }
|
||||
|
||||
ui_prev_cached_box :: #force_inline proc( box : ^UI_Box ) -> ^UI_Box { return zpl_hmap_get( ui_context().prev_cache, cast(u64) box.key ) }
|
||||
|
||||
// Topmost ancestor that is not the root
|
||||
ui_top_ancestor :: #force_inline proc "contextless" ( box : ^UI_Box ) -> (^UI_Box) {
|
||||
using ui := get_state().ui_context
|
||||
ancestor := box
|
||||
for ; ancestor.parent != root; ancestor = ancestor.parent {}
|
||||
return ancestor
|
||||
}
|
||||
|
||||
ui_context :: #force_inline proc() -> ^UI_State { return get_state().ui_context }
|
153
code/sectr/ui/core_box.odin
Normal file
153
code/sectr/ui/core_box.odin
Normal file
@ -0,0 +1,153 @@
|
||||
package sectr
|
||||
|
||||
UI_BoxFlag :: enum u64
|
||||
{
|
||||
Disabled,
|
||||
|
||||
Focusable,
|
||||
Click_To_Focus,
|
||||
|
||||
Mouse_Clickable,
|
||||
Keyboard_Clickable,
|
||||
|
||||
Count,
|
||||
}
|
||||
UI_BoxFlags :: bit_set[UI_BoxFlag; u64]
|
||||
// UI_BoxFlag_Scroll :: UI_BoxFlags { .Scroll_X, .Scroll_Y }
|
||||
|
||||
UI_RenderBoxInfo :: struct {
|
||||
using computed : UI_Computed,
|
||||
using style : UI_Style,
|
||||
text : StrRunesPair,
|
||||
font_size : UI_Scalar,
|
||||
border_width : UI_Scalar,
|
||||
}
|
||||
|
||||
UI_Box :: struct {
|
||||
// Cache ID
|
||||
key : UI_Key,
|
||||
// label : string,
|
||||
label : StrRunesPair,
|
||||
text : StrRunesPair,
|
||||
|
||||
// Regenerated per frame.
|
||||
|
||||
// first, last : The first and last child of this box
|
||||
// prev, next : The adjacent neighboring boxes who are children of to the same parent
|
||||
using links : DLL_NodeFull( UI_Box ),
|
||||
parent : ^UI_Box,
|
||||
num_children : i32,
|
||||
ancestors : i32, // This value for rooted widgets gets set to -1 after rendering see ui_box_make() for the reason.
|
||||
parent_index : i32,
|
||||
|
||||
flags : UI_BoxFlags,
|
||||
computed : UI_Computed,
|
||||
|
||||
layout : UI_Layout,
|
||||
style : UI_Style,
|
||||
|
||||
// Persistent Data
|
||||
hot_delta : f32,
|
||||
active_delta : f32,
|
||||
disabled_delta : f32,
|
||||
style_delta : f32,
|
||||
first_frame : b8,
|
||||
// root_order_id : i16,
|
||||
|
||||
// mouse : UI_InteractState,
|
||||
// keyboard : UI_InteractState,
|
||||
}
|
||||
|
||||
ui_box_equal :: #force_inline proc "contextless" ( a, b : ^ UI_Box ) -> b32 {
|
||||
BoxSize :: size_of(UI_Box)
|
||||
|
||||
result : b32 = true
|
||||
result &= a.key == b.key // We assume for now the label is the same as the key, if not something is terribly wrong.
|
||||
result &= a.flags == b.flags
|
||||
return result
|
||||
}
|
||||
|
||||
ui_box_from_key :: #force_inline proc ( cache : ^HMapZPL(UI_Box), key : UI_Key ) -> (^UI_Box) {
|
||||
return zpl_hmap_get( cache, cast(u64) key )
|
||||
}
|
||||
|
||||
ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box)
|
||||
{
|
||||
// profile(#procedure)
|
||||
using ui := get_state().ui_context
|
||||
key := ui_key_from_string( label )
|
||||
|
||||
curr_box : (^ UI_Box)
|
||||
prev_box := zpl_hmap_get( prev_cache, cast(u64) key )
|
||||
{
|
||||
// profile("Assigning current box")
|
||||
set_result : ^ UI_Box
|
||||
set_error : AllocatorError
|
||||
if prev_box != nil
|
||||
{
|
||||
// Previous history was found, copy over previous state.
|
||||
set_result, set_error = zpl_hmap_set( curr_cache, cast(u64) key, (prev_box ^) )
|
||||
}
|
||||
else {
|
||||
box : UI_Box
|
||||
box.key = key
|
||||
box.label = str_intern( label )
|
||||
set_result, set_error = zpl_hmap_set( curr_cache, cast(u64) key, box )
|
||||
}
|
||||
|
||||
verify( set_error == AllocatorError.None, "Failed to set zpl_hmap due to allocator error" )
|
||||
curr_box = set_result
|
||||
curr_box.first_frame = prev_box == nil
|
||||
curr_box.flags = flags
|
||||
}
|
||||
|
||||
// Clear non-persistent data
|
||||
curr_box.computed.fresh = false
|
||||
curr_box.links = {}
|
||||
curr_box.num_children = 0
|
||||
|
||||
// If there is a parent, setup the relevant references
|
||||
parent := stack_peek( & parent_stack )
|
||||
if parent != nil
|
||||
{
|
||||
dll_full_push_back( parent, curr_box, nil )
|
||||
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
|
||||
return curr_box
|
||||
}
|
||||
|
||||
ui_box_tranverse_next :: proc "contextless" ( box : ^ UI_Box ) -> (^ UI_Box)
|
||||
{
|
||||
using state := get_state()
|
||||
// If current has children, do them first
|
||||
if box.first != nil
|
||||
{
|
||||
// Check to make sure parent is present on the screen, if its not don't bother.
|
||||
is_app_ui := ui_context == & screen_ui
|
||||
if intersects_range2( view_get_bounds(), box.computed.bounds)
|
||||
{
|
||||
return box.first
|
||||
}
|
||||
}
|
||||
|
||||
if box.next != nil do return box.next
|
||||
// There is no more adjacent nodes
|
||||
|
||||
parent := box.parent
|
||||
// Attempt to find a parent with a next, otherwise we just return a parent with nil
|
||||
for ; parent.parent != nil;
|
||||
{
|
||||
if parent.next != nil {
|
||||
break
|
||||
}
|
||||
parent = parent.parent
|
||||
}
|
||||
|
||||
// Lift back up to parent, and set it to its next.
|
||||
return parent.next
|
||||
}
|
165
code/sectr/ui/core_layout.odin
Normal file
165
code/sectr/ui/core_layout.odin
Normal file
@ -0,0 +1,165 @@
|
||||
package sectr
|
||||
|
||||
import "core:math"
|
||||
import "core:math/linalg"
|
||||
|
||||
|
||||
// Anchor_
|
||||
|
||||
// Alignment presets
|
||||
|
||||
LayoutAlign_OriginTL_Top :: Vec2{0.5, 0}
|
||||
LayoutAlign_OriginTL_TopLeft :: Vec2{ 0, 0}
|
||||
LayoutAlign_OriginTL_TopRight :: Vec2{ 1, 0}
|
||||
LayoutAlign_OriginTL_Centered :: Vec2{0.5, 0.5}
|
||||
LayoutAlign_OriginTL_Bottom :: Vec2{0.5, 1}
|
||||
LayoutAlign_OriginTL_BottomLeft :: Vec2{ 0, 1}
|
||||
LayoutAlign_OriginTL_BottomRight :: Vec2{ 1, 1}
|
||||
|
||||
// LayoutAlign_OriginTL_
|
||||
|
||||
Layout_OriginCenter_Centered :: Vec2{0.5, 0.5}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// The UI_Box's actual positioning and sizing
|
||||
// There is an excess of rectangles here for debug puproses.
|
||||
UI_Computed :: struct {
|
||||
// anchors : Range2, // Bounds for anchors within parent
|
||||
// margins : Range2, // Bounds for margins within parent
|
||||
padding : Range2, // Bounds for padding's starting bounds (will be offset by border if there is one), only here for debug vis
|
||||
|
||||
bounds : Range2, // Bounds for box itself
|
||||
content : Range2, // Bounds for content (text or children)
|
||||
text_pos : Vec2, // Position of text within content
|
||||
text_size : Vec2, // Size of text within content
|
||||
fresh : b32, // If the auto-layout has been computed for the current frame
|
||||
}
|
||||
|
||||
UI_LayoutDirectionX :: enum(i32) {
|
||||
Left_To_Right,
|
||||
Right_To_Left,
|
||||
}
|
||||
|
||||
UI_LayoutDirectionY :: enum(i32) {
|
||||
Top_To_Bottom,
|
||||
Bottom_To_Top,
|
||||
}
|
||||
|
||||
UI_LayoutSide :: struct {
|
||||
// using _ : struct {
|
||||
top, bottom : UI_Scalar,
|
||||
left, right : UI_Scalar,
|
||||
// }
|
||||
}
|
||||
|
||||
UI_LayoutFlag :: 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,
|
||||
|
||||
// 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!
|
||||
// ?Note(Ed): This can get pretty complicated... Maybe its better to leave this to composition of boxes.
|
||||
// ?A text wrapping panel can organize text and wrap it via procedrually generated lines in a hbox/vbox.
|
||||
// ?It would be a non-issue so long as the text rendering bottleneck is resolved.
|
||||
// Wrap text around the box, text_alignment specifies the justification for its compostion when wrapping.
|
||||
Text_Wrap,
|
||||
|
||||
Count,
|
||||
}
|
||||
UI_LayoutFlags :: bit_set[UI_LayoutFlag; u32]
|
||||
|
||||
// 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,
|
||||
|
||||
font_size : UI_Scalar,
|
||||
|
||||
margins : UI_LayoutSide,
|
||||
padding : UI_LayoutSide,
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
UI_LayoutCombo :: struct #raw_union {
|
||||
array : [UI_StylePreset.Count] UI_Layout,
|
||||
using layouts : struct {
|
||||
default, disabled, hot, active : UI_Layout,
|
||||
}
|
||||
}
|
||||
|
||||
to_ui_layout_side_f32 :: #force_inline proc( pixels : f32 ) -> UI_LayoutSide { return { pixels, pixels, pixels, pixels } }
|
||||
to_ui_layout_side_vec2 :: #force_inline proc( v : Vec2) -> UI_LayoutSide { return { v.x, v.x, v.y, v.y} }
|
||||
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 }
|
219
code/sectr/ui/core_layout_compute.odin
Normal file
219
code/sectr/ui/core_layout_compute.odin
Normal file
@ -0,0 +1,219 @@
|
||||
package sectr
|
||||
|
||||
ui_box_compute_layout :: proc( box : ^UI_Box,
|
||||
dont_mark_fresh : b32 = false,
|
||||
ancestors_layout_required : b32 = false,
|
||||
root_layout_required : b32 = false )
|
||||
{
|
||||
// profile("Layout Box")
|
||||
state := get_state()
|
||||
ui := state.ui_context
|
||||
using box
|
||||
|
||||
size_to_text : bool = .Size_To_Text in layout.flags
|
||||
|
||||
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 an axis is auto-sized by a ratio of the other axis
|
||||
* Using the referenced axis, set the size of the ratio'd axis by that ratio.
|
||||
|
||||
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)
|
||||
|
||||
text_size : Vec2
|
||||
if layout.font_size == computed.text_size.y {
|
||||
text_size = computed.text_size
|
||||
}
|
||||
else {
|
||||
text_size = cast(Vec2) measure_text_size( box.text.str, style.font, layout.font_size, 0 )
|
||||
}
|
||||
|
||||
if size_to_text {
|
||||
adjusted_size = text_size
|
||||
}
|
||||
|
||||
if .Scale_Width_By_Height_Ratio in layout.flags {
|
||||
adjusted_size.x = adjusted_size.y * layout.size.min.x
|
||||
}
|
||||
if .Scale_Height_By_Width_Ratio in layout.flags {
|
||||
adjusted_size.y = adjusted_size.x * layout.size.min.y
|
||||
}
|
||||
|
||||
if .Size_To_Content in layout.flags {
|
||||
// Preemtively traverse the children of this parent and have them compute their layout.
|
||||
// This parent will just set its size to the max bounding area of those children.
|
||||
// This will recursively occur if child also depends on their content size from their children, etc.
|
||||
ui_box_compute_layout_children(box)
|
||||
//ui_compute_children_bounding_area(box)
|
||||
}
|
||||
|
||||
// TODO(Ed): Should this force override all of the previous auto-sizing possible?
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
if ! (.Origin_At_Anchor_Center in layout.flags) {
|
||||
// The convention offset adjust the box so that the top-left point is at the top left of the anchor's bounds
|
||||
tl_convention_offset := adjusted_size * {0, -1}
|
||||
bounds = range2(
|
||||
rel_pos - adjusted_size * alignment + tl_convention_offset,
|
||||
rel_pos + adjusted_size * (vec2_one - alignment) + tl_convention_offset,
|
||||
)
|
||||
}
|
||||
else {
|
||||
centered_convention_offset := adjusted_size * -0.5
|
||||
bounds = range2(
|
||||
(rel_pos + centered_convention_offset) - adjusted_size * -alignment ,
|
||||
(rel_pos + centered_convention_offset) + adjusted_size * (alignment + vec2_one),
|
||||
)
|
||||
}
|
||||
|
||||
// 7. Padding & Content
|
||||
// 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.bounds = bounds
|
||||
computed.padding = padding_bounds
|
||||
computed.content = content_bounds
|
||||
|
||||
// 8. Text position & size
|
||||
if len(box.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 += (content_size - text_size) * layout.text_alignment
|
||||
|
||||
computed.text_size = text_size
|
||||
computed.text_pos = text_pos
|
||||
}
|
||||
computed.fresh = true && !dont_mark_fresh
|
||||
}
|
||||
|
||||
ui_box_compute_layout_children :: proc( box : ^UI_Box )
|
||||
{
|
||||
for current := box.first; current != nil && current.prev != box; current = ui_box_tranverse_next( current )
|
||||
{
|
||||
if current == box do return
|
||||
if current.computed.fresh do continue
|
||||
ui_box_compute_layout( current )
|
||||
}
|
||||
}
|
||||
|
||||
ui_core_compute_layout :: proc( ui : ^UI_State )
|
||||
{
|
||||
profile(#procedure)
|
||||
state := get_state()
|
||||
|
||||
root := ui.root
|
||||
{
|
||||
computed := & root.computed
|
||||
style := root.style
|
||||
layout := & root.layout
|
||||
if ui == & state.screen_ui {
|
||||
computed.bounds.min = transmute(Vec2) state.app_window.extent * -1
|
||||
computed.bounds.max = transmute(Vec2) state.app_window.extent
|
||||
}
|
||||
computed.content = computed.bounds
|
||||
}
|
||||
|
||||
for current := root.first; current != nil; current = ui_box_tranverse_next( current )
|
||||
{
|
||||
if ! current.computed.fresh {
|
||||
ui_box_compute_layout( current )
|
||||
}
|
||||
array_append( & ui.render_queue, UI_RenderBoxInfo {
|
||||
current.computed,
|
||||
current.style,
|
||||
current.text,
|
||||
current.layout.font_size,
|
||||
current.layout.border_width,
|
||||
})
|
||||
}
|
||||
}
|
256
code/sectr/ui/core_signal.odin
Normal file
256
code/sectr/ui/core_signal.odin
Normal file
@ -0,0 +1,256 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
UI_Signal :: struct {
|
||||
cursor_pos : Vec2,
|
||||
drag_delta : Vec2,
|
||||
scroll : Vec2,
|
||||
|
||||
left_clicked : b8,
|
||||
right_clicked : b8,
|
||||
double_clicked : b8,
|
||||
keyboard_clicked : b8,
|
||||
left_shift_held : b8,
|
||||
left_ctrl_held : b8,
|
||||
|
||||
active : b8,
|
||||
hot : b8,
|
||||
disabled : b8,
|
||||
|
||||
was_active : b8,
|
||||
was_hot : b8,
|
||||
was_disabled : b8,
|
||||
|
||||
pressed : b8,
|
||||
released : b8,
|
||||
cursor_over : b8,
|
||||
commit : b8,
|
||||
}
|
||||
|
||||
ui_signal_from_box :: proc ( box : ^ UI_Box, update_style := true, update_deltas := true ) -> UI_Signal
|
||||
{
|
||||
// profile(#procedure)
|
||||
ui := get_state().ui_context
|
||||
input := get_state().input
|
||||
|
||||
frame_delta := frametime_delta32()
|
||||
|
||||
signal := UI_Signal {}
|
||||
|
||||
// Cursor Collision
|
||||
// profile_begin( "Cursor collision")
|
||||
signal.cursor_pos = ui_cursor_pos()
|
||||
signal.cursor_over = cast(b8) pos_within_range2( signal.cursor_pos, box.computed.bounds )
|
||||
|
||||
UnderCheck:
|
||||
{
|
||||
if ! signal.cursor_over do break UnderCheck
|
||||
|
||||
last_root := ui_box_from_key( ui.prev_cache, ui.root.key )
|
||||
if last_root == nil do break UnderCheck
|
||||
|
||||
top_ancestor := ui_top_ancestor(box)
|
||||
if top_ancestor.parent_index < last_root.parent_index
|
||||
{
|
||||
for curr := last_root.last; curr != nil && curr.key != box.key; curr = curr.prev {
|
||||
if pos_within_range2( signal.cursor_pos, curr.computed.bounds ) {
|
||||
signal.cursor_over = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
// profile_begin("misc")
|
||||
left_pressed := pressed( input.mouse.left )
|
||||
left_released := released( input.mouse.left )
|
||||
|
||||
signal.left_shift_held = b8(input.keyboard.left_shift.ended_down)
|
||||
|
||||
mouse_clickable := UI_BoxFlag.Mouse_Clickable in box.flags
|
||||
keyboard_clickable := UI_BoxFlag.Keyboard_Clickable in box.flags
|
||||
|
||||
was_hot := (box.hot_delta > 0)
|
||||
was_active := (ui.active == box.key) && (box.active_delta > 0)
|
||||
was_disabled := box.disabled_delta > 0
|
||||
// if was_hot {
|
||||
// runtime.debug_trap()
|
||||
// }
|
||||
|
||||
// Check to see if this box is active
|
||||
if mouse_clickable && signal.cursor_over && left_pressed && was_hot
|
||||
{
|
||||
// ui.hot = box.key
|
||||
ui.active = box.key
|
||||
ui.active_mouse[MouseBtn.Left] = box.key
|
||||
|
||||
ui.last_pressed_key = box.key
|
||||
ui.active_start_style = box.style
|
||||
|
||||
signal.pressed = true
|
||||
signal.left_clicked = b8(left_pressed)
|
||||
// TODO(Ed) : Support double-click detection
|
||||
}
|
||||
|
||||
if mouse_clickable && ! signal.cursor_over && left_released
|
||||
{
|
||||
box.active_delta = 0
|
||||
|
||||
ui.active = UI_Key(0)
|
||||
ui.active_mouse[MouseBtn.Left] = UI_Key(0)
|
||||
|
||||
signal.released = true
|
||||
}
|
||||
|
||||
if keyboard_clickable
|
||||
{
|
||||
// TODO(Ed) : Add keyboard interaction support
|
||||
}
|
||||
|
||||
// TODO(Ed): Should panning and scrolling get supported here? (problably not...)
|
||||
// TODO(Ed) : Add scrolling support
|
||||
// if UI_BoxFlag.Scroll_X in box.flags {
|
||||
|
||||
// }
|
||||
// if UI_BoxFlag.Scroll_Y in box.flags {
|
||||
|
||||
// }
|
||||
// TODO(Ed) : Add panning support
|
||||
// if UI_BoxFlag.Pan_X in box.flags {
|
||||
|
||||
// }
|
||||
// if UI_BoxFlag.Pan_Y in box.flags {
|
||||
// }
|
||||
|
||||
is_disabled := UI_BoxFlag.Disabled in box.flags
|
||||
is_hot := ui.hot == box.key
|
||||
is_active := ui.active == box.key
|
||||
|
||||
// TODO(Ed): It should be able to enter hot without mouse_clickable
|
||||
if mouse_clickable && signal.cursor_over && ! is_disabled
|
||||
{
|
||||
hot_vacant := ui.hot == UI_Key(0)
|
||||
active_vacant := ui.active == UI_Key(0)
|
||||
// (active_vacant is_active)
|
||||
if signal.cursor_over && active_vacant
|
||||
{
|
||||
if ! hot_vacant {
|
||||
prev := ui_box_from_key( ui.curr_cache, ui.hot )
|
||||
prev.hot_delta = 0
|
||||
}
|
||||
// prev_hot := zpl_hmap_get( ui.prev_cache, u64(ui.hot) )
|
||||
// prev_hot_label := prev_hot != nil ? prev_hot.label.str : ""
|
||||
// log( str_fmt_tmp("Detected HOT via CURSOR OVER: %v is_hot: %v is_active: %v prev_hot: %v", box.label.str, is_hot, is_active, prev_hot_label ))
|
||||
ui.hot = box.key
|
||||
is_hot = true
|
||||
|
||||
ui.hot_start_style = box.style
|
||||
}
|
||||
}
|
||||
else if ! signal.cursor_over && was_hot
|
||||
{
|
||||
ui.hot = UI_Key(0)
|
||||
is_hot = false
|
||||
box.hot_delta = 0
|
||||
}
|
||||
|
||||
if mouse_clickable && signal.cursor_over && left_released
|
||||
{
|
||||
box.active_delta = 0
|
||||
|
||||
ui.active = UI_Key(0)
|
||||
ui.active_mouse[MouseBtn.Left] = UI_Key(0)
|
||||
|
||||
signal.released = true
|
||||
|
||||
if was_active {
|
||||
signal.left_clicked = true
|
||||
ui.last_clicked = box.key
|
||||
}
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
// State Deltas update
|
||||
// profile_begin( "state deltas upate")
|
||||
if is_hot
|
||||
{
|
||||
box.hot_delta += frame_delta
|
||||
if was_hot {
|
||||
box.style_delta += frame_delta
|
||||
}
|
||||
}
|
||||
if is_active
|
||||
{
|
||||
box.active_delta += frame_delta
|
||||
if was_active {
|
||||
box.style_delta += frame_delta
|
||||
}
|
||||
}
|
||||
else {
|
||||
box.active_delta = 0
|
||||
}
|
||||
if is_disabled
|
||||
{
|
||||
box.disabled_delta += frame_delta
|
||||
if was_hot {
|
||||
box.style_delta += frame_delta
|
||||
}
|
||||
}
|
||||
else {
|
||||
box.disabled_delta = 0
|
||||
}
|
||||
// profile_end()
|
||||
|
||||
signal.active = cast(b8) is_active
|
||||
signal.was_active = cast(b8) was_active
|
||||
// logf("was_active: %v", was_active)
|
||||
|
||||
// Update style if not in default state
|
||||
if update_style
|
||||
{
|
||||
// profile("Update style")
|
||||
|
||||
if is_hot
|
||||
{
|
||||
if ! was_hot {
|
||||
box.style_delta = 0
|
||||
}
|
||||
box.layout = ui_layout_peek().hot
|
||||
box.style = ui_style_peek().hot
|
||||
}
|
||||
if is_active
|
||||
{
|
||||
if ! was_active {
|
||||
box.style_delta = 0
|
||||
}
|
||||
box.layout = ui_layout_peek().active
|
||||
box.style = ui_style_peek().active
|
||||
}
|
||||
if is_disabled
|
||||
{
|
||||
if ! was_disabled {
|
||||
box.style_delta = 0
|
||||
}
|
||||
box.layout = ui_layout_peek().disabled
|
||||
box.style = ui_style_peek().disabled
|
||||
}
|
||||
|
||||
if ! is_disabled && ! is_active && ! is_hot {
|
||||
if was_disabled || was_active || was_hot {
|
||||
box.style_delta = 0
|
||||
}
|
||||
else {
|
||||
box.style_delta += frame_delta
|
||||
}
|
||||
box.layout = ui_layout_peek().default
|
||||
box.style = ui_style_peek().default
|
||||
}
|
||||
}
|
||||
|
||||
if is_active && ! was_active {
|
||||
ui.active_start_signal = signal
|
||||
}
|
||||
|
||||
return signal
|
||||
}
|
72
code/sectr/ui/core_style.odin
Normal file
72
code/sectr/ui/core_style.odin
Normal file
@ -0,0 +1,72 @@
|
||||
package sectr
|
||||
|
||||
// TODO(Ed): We problably can embedd this info into the UI_Layout with the regular text_alignment
|
||||
UI_TextAlign :: enum u32 {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
Count
|
||||
}
|
||||
|
||||
UI_StylePreset :: enum u32 {
|
||||
Default,
|
||||
Disabled,
|
||||
Hot,
|
||||
Active,
|
||||
Count,
|
||||
}
|
||||
|
||||
UI_Style :: struct {
|
||||
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,
|
||||
text_color : Color,
|
||||
|
||||
// TODO(Ed) : Support setting the cursor state
|
||||
cursor : UI_Cursor,
|
||||
}
|
||||
|
||||
UI_StyleCombo :: struct #raw_union {
|
||||
array : [UI_StylePreset.Count] UI_Style,
|
||||
using styles : struct {
|
||||
default, disabled, hot, active : UI_Style,
|
||||
}
|
||||
}
|
||||
|
||||
to_ui_style_combo :: #force_inline proc( style : UI_Style ) -> UI_StyleCombo { return { styles = {style, style, style, style} } }
|
||||
|
||||
/*
|
||||
Style Interface
|
||||
|
||||
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
|
||||
|
||||
The following procedure overloads are available from grime.odin :
|
||||
* ui_style
|
||||
* ui_style_push
|
||||
*/
|
||||
|
||||
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_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 ) }
|
||||
|
||||
@(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) }
|
||||
|
||||
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 }
|
12
code/sectr/ui/docking.odin
Normal file
12
code/sectr/ui/docking.odin
Normal file
@ -0,0 +1,12 @@
|
||||
package sectr
|
||||
|
||||
UI_DockedEntry :: struct {
|
||||
placeholder : int,
|
||||
}
|
||||
|
||||
// Non-overlapping, Tiled, window/frame manager
|
||||
// Has support for tabbing
|
||||
// AKA: Tiled Window Manager
|
||||
UI_Docking :: struct {
|
||||
placeholder : int,
|
||||
}
|
178
code/sectr/ui/floating.odin
Normal file
178
code/sectr/ui/floating.odin
Normal file
@ -0,0 +1,178 @@
|
||||
package sectr
|
||||
|
||||
UI_Floating :: struct {
|
||||
label : string,
|
||||
using links : DLL_NodePN(UI_Floating),
|
||||
captures : rawptr,
|
||||
builder : UI_FloatingBuilder,
|
||||
queued : b32,
|
||||
}
|
||||
UI_FloatingBuilder :: #type proc( captures : rawptr ) -> (became_active : b32)
|
||||
|
||||
// Overlapping/Stacking window/frame manager
|
||||
// AKA: Stacking or Floating Window Manager
|
||||
UI_FloatingManager :: struct {
|
||||
using links : DLL_NodeFL(UI_Floating),
|
||||
build_queue : Array(UI_Floating),
|
||||
tracked : HMapChainedPtr(UI_Floating),
|
||||
}
|
||||
|
||||
ui_floating_startup :: proc( self : ^UI_FloatingManager, allocator : Allocator, build_queue_cap, tracked_cap : u64, dbg_name : string = "" ) -> AllocatorError
|
||||
{
|
||||
error : AllocatorError
|
||||
|
||||
queue_dbg_name := str_intern(str_fmt_tmp("%s: build_queue", dbg_name))
|
||||
self.build_queue, error = array_init_reserve( UI_Floating, allocator, build_queue_cap, dbg_name = queue_dbg_name.str )
|
||||
if error != AllocatorError.None
|
||||
{
|
||||
ensure(false, "Failed to allocate the build_queue")
|
||||
return error
|
||||
}
|
||||
|
||||
tracked_dbg_name := str_intern(str_fmt_tmp("%s: tracked", dbg_name))
|
||||
self.tracked, error = hmap_chained_init(UI_Floating, uint(tracked_cap), allocator, dbg_name = tracked_dbg_name.str )
|
||||
if error != AllocatorError.None
|
||||
{
|
||||
ensure(false, "Failed to allocate tracking table")
|
||||
return error
|
||||
}
|
||||
return error
|
||||
}
|
||||
|
||||
ui_floating_reload :: proc( self : ^UI_FloatingManager, allocator : Allocator )
|
||||
{
|
||||
using self
|
||||
build_queue.backing = allocator
|
||||
hmap_chained_reload(tracked, allocator)
|
||||
}
|
||||
|
||||
ui_floating_just_builder :: #force_inline proc( label : string, builder : UI_FloatingBuilder ) -> ^UI_Floating
|
||||
{
|
||||
No_Captures : rawptr = nil
|
||||
return ui_floating_with_capture(label, No_Captures, builder)
|
||||
}
|
||||
|
||||
ui_floating_with_capture :: proc( label : string, captures : rawptr = nil, builder : UI_FloatingBuilder ) -> ^UI_Floating
|
||||
{
|
||||
entry := UI_Floating {
|
||||
label = label,
|
||||
captures = captures,
|
||||
builder = builder,
|
||||
}
|
||||
|
||||
floating := get_state().ui_floating_context
|
||||
array_append( & floating.build_queue, entry )
|
||||
return nil
|
||||
}
|
||||
|
||||
@(deferred_none = ui_floating_manager_end)
|
||||
ui_floating_manager :: proc ( manager : ^UI_FloatingManager )
|
||||
{
|
||||
ui_floating_manager_begin(manager)
|
||||
}
|
||||
|
||||
ui_floating_manager_begin :: proc ( manager : ^UI_FloatingManager )
|
||||
{
|
||||
state := get_state()
|
||||
state.ui_floating_context = manager
|
||||
}
|
||||
|
||||
ui_floating_manager_end :: proc()
|
||||
{
|
||||
state := get_state()
|
||||
floating := & state.ui_floating_context
|
||||
ui_floating_build()
|
||||
floating = nil
|
||||
}
|
||||
|
||||
ui_floating_build :: proc()
|
||||
{
|
||||
ui := ui_context()
|
||||
using floating := get_state().ui_floating_context
|
||||
|
||||
for to_enqueue in array_to_slice( build_queue)
|
||||
{
|
||||
key := ui_key_from_string(to_enqueue.label)
|
||||
lookup := hmap_chained_get( tracked, transmute(u64) key )
|
||||
|
||||
// Check if entry is already present
|
||||
if lookup != nil && (lookup.next != nil || lookup == last) {
|
||||
lookup.captures = to_enqueue.captures
|
||||
lookup.builder = to_enqueue.builder
|
||||
lookup.queued = true
|
||||
continue
|
||||
}
|
||||
|
||||
if lookup == nil {
|
||||
error : AllocatorError
|
||||
lookup, error = hmap_chained_set( tracked, transmute(u64) key, to_enqueue )
|
||||
if error != AllocatorError.None {
|
||||
ensure(false, "Failed to allocate entry to hashtable")
|
||||
continue
|
||||
}
|
||||
}
|
||||
else {
|
||||
lookup.captures = to_enqueue.captures
|
||||
lookup.builder = to_enqueue.builder
|
||||
}
|
||||
lookup.queued = true
|
||||
dll_full_push_back(floating, lookup, nil )
|
||||
// if first == nil {
|
||||
// first = lookup
|
||||
// last = lookup
|
||||
// continue
|
||||
// }
|
||||
// if first == last {
|
||||
// last = lookup
|
||||
// last.prev = first
|
||||
// first.next = last
|
||||
// continue
|
||||
// }
|
||||
// last.next = lookup
|
||||
// lookup.prev = last
|
||||
// last = lookup
|
||||
}
|
||||
array_clear(build_queue)
|
||||
|
||||
to_raise : ^UI_Floating
|
||||
for entry := first; entry != nil; entry = entry.next
|
||||
{
|
||||
if ! entry.queued
|
||||
{
|
||||
ensure(false, "There should be no queue failures yet")
|
||||
|
||||
if entry == first
|
||||
{
|
||||
first = entry.next
|
||||
entry.next = nil
|
||||
continue
|
||||
}
|
||||
if entry == last
|
||||
{
|
||||
last = last.prev
|
||||
last.prev = nil
|
||||
entry.prev = nil
|
||||
continue
|
||||
}
|
||||
|
||||
left := entry.prev
|
||||
right := entry.next
|
||||
|
||||
left.next = right
|
||||
right.prev = left
|
||||
entry.prev = nil
|
||||
entry.next = nil
|
||||
}
|
||||
|
||||
if entry.builder( entry.captures ) && entry != last && to_raise == nil
|
||||
{
|
||||
to_raise = entry
|
||||
}
|
||||
entry.queued = false
|
||||
}
|
||||
if to_raise != nil
|
||||
{
|
||||
dll_full_pop( to_raise, floating )
|
||||
dll_full_push_back( floating, to_raise, nil )
|
||||
}
|
||||
}
|
142
code/sectr/ui/layout_widget.odin
Normal file
142
code/sectr/ui/layout_widget.odin
Normal file
@ -0,0 +1,142 @@
|
||||
package sectr
|
||||
|
||||
/*
|
||||
Widget Layout Ops
|
||||
*/
|
||||
|
||||
ui_layout_children_horizontally :: proc( container : ^UI_Box, direction : UI_LayoutDirectionX, width_ref : ^f32 )
|
||||
{
|
||||
container_width : f32
|
||||
if width_ref != nil {
|
||||
container_width = width_ref ^
|
||||
}
|
||||
else {
|
||||
container_width = container.computed.content.max.x - container.computed.content.min.x
|
||||
}
|
||||
|
||||
// do layout calculations for the children
|
||||
total_stretch_ratio : f32 = 0.0
|
||||
size_req_children : f32 = 0
|
||||
for child := container.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 : container_width
|
||||
width := height * size.min.x
|
||||
|
||||
size_req_children += width
|
||||
continue
|
||||
}
|
||||
|
||||
size_req_children += size.min.x
|
||||
continue
|
||||
}
|
||||
|
||||
total_stretch_ratio += anchor.ratio.x
|
||||
}
|
||||
|
||||
avail_flex_space := container_width - size_req_children
|
||||
|
||||
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 - child.layout.margins.left - child.layout.margins.right
|
||||
}
|
||||
flags |= {.Fixed_Width}
|
||||
}
|
||||
|
||||
space_used : f32 = 0.0
|
||||
switch direction{
|
||||
case .Right_To_Left:
|
||||
for child := container.last; child != nil; child = child.prev {
|
||||
allocate_space(child, total_stretch_ratio, avail_flex_space)
|
||||
using child.layout
|
||||
anchor = range2({0, 0}, {0, 0})
|
||||
pos.x = space_used
|
||||
space_used += size.min.x + child.layout.margins.left + child.layout.margins.right
|
||||
}
|
||||
case .Left_To_Right:
|
||||
for child := container.first; child != nil; child = child.next {
|
||||
allocate_space(child, total_stretch_ratio, avail_flex_space)
|
||||
using child.layout
|
||||
anchor = range2({0, 0}, {0, 0})
|
||||
pos.x = space_used
|
||||
space_used += size.min.x + child.layout.margins.left + child.layout.margins.right
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ui_layout_children_vertically :: proc( container : ^UI_Box, direction : UI_LayoutDirectionY, height_ref : ^f32 )
|
||||
{
|
||||
container_height : f32
|
||||
if height_ref != nil {
|
||||
container_height = height_ref ^
|
||||
}
|
||||
else {
|
||||
container_height = container.computed.content.max.y - container.computed.content.min.y
|
||||
}
|
||||
|
||||
// do layout calculations for the children
|
||||
total_stretch_ratio : f32 = 0.0
|
||||
size_req_children : f32 = 0
|
||||
for child := container.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 : container_height
|
||||
height := width * size.min.y
|
||||
|
||||
size_req_children += height
|
||||
continue
|
||||
}
|
||||
|
||||
size_req_children += size.min.y
|
||||
continue
|
||||
}
|
||||
|
||||
total_stretch_ratio += anchor.ratio.y
|
||||
}
|
||||
|
||||
avail_flex_space := container_height - size_req_children
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
space_used : f32 = 0.0
|
||||
switch direction
|
||||
{
|
||||
case .Bottom_To_Top:
|
||||
for child := container.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, 0}
|
||||
pos.y = -space_used
|
||||
space_used += size.min.y
|
||||
size.min.x = container.computed.content.max.x + container.computed.content.min.x
|
||||
}
|
||||
case .Top_To_Bottom:
|
||||
for child := container.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, 0}
|
||||
pos.y = -space_used
|
||||
space_used += size.min.y
|
||||
size.min.x = container.computed.content.max.x - container.computed.content.min.x
|
||||
}
|
||||
}
|
||||
}
|
299
code/sectr/ui/tests.odin
Normal file
299
code/sectr/ui/tests.odin
Normal file
@ -0,0 +1,299 @@
|
||||
package sectr
|
||||
|
||||
import "core:math/linalg"
|
||||
import str "core:strings"
|
||||
|
||||
test_hover_n_click :: proc()
|
||||
{
|
||||
state := get_state(); using state
|
||||
|
||||
first_btn := ui_button( "FIRST BOX!" )
|
||||
if first_btn.left_clicked || debug.frame_2_created {
|
||||
debug.frame_2_created = true
|
||||
|
||||
second_layout := first_btn.layout
|
||||
second_layout.pos = { 250, 0 }
|
||||
ui_layout( second_layout )
|
||||
|
||||
second_box := ui_button( "SECOND BOX!")
|
||||
}
|
||||
}
|
||||
|
||||
test_draggable :: proc()
|
||||
{
|
||||
state := get_state(); using state
|
||||
ui := ui_context
|
||||
|
||||
draggable_layout := UI_Layout {
|
||||
flags = {
|
||||
.Fixed_Position_X, .Fixed_Position_Y,
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
.Origin_At_Anchor_Center,
|
||||
},
|
||||
// alignment = { 0.0, 0.5 },
|
||||
alignment = { 0.5, 0 },
|
||||
text_alignment = { 0.0, 0.0 },
|
||||
// alignment = { 1.0, 1.0 },
|
||||
pos = { 0, 0 },
|
||||
size = range2({ 200, 200 }, {}),
|
||||
}
|
||||
ui_layout( draggable_layout )
|
||||
ui_style( UI_Style {
|
||||
corner_radii = { 0.3, 0.3, 0.3, 0.3 },
|
||||
})
|
||||
|
||||
draggable := ui_widget( "Draggable Box!", UI_BoxFlags { .Mouse_Clickable } )
|
||||
if draggable.first_frame {
|
||||
debug.draggable_box_pos = draggable.layout.pos + { 0, -100 }
|
||||
debug.draggable_box_size = draggable.layout.size.min
|
||||
}
|
||||
|
||||
// Dragging
|
||||
if draggable.active {
|
||||
debug.draggable_box_pos += mouse_world_delta()
|
||||
}
|
||||
|
||||
if (ui.hot == draggable.key) {
|
||||
draggable.style.bg_color = Color_Blue
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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.0, 0.0 }
|
||||
// parent_layout.margins = { 100, 100, 100, 100 }
|
||||
parent_layout.padding = { 5, 10, 5, 5 }
|
||||
parent_layout.pos = { 0, 0 }
|
||||
parent_layout.flags = {
|
||||
.Fixed_Position_X, .Fixed_Position_Y,
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
.Origin_At_Anchor_Center
|
||||
}
|
||||
ui_layout(parent_layout)
|
||||
|
||||
parent_style := frame_style_default ^
|
||||
ui_style(parent_style)
|
||||
|
||||
parent := ui_widget( "Parent", { .Mouse_Clickable })
|
||||
ui_parent_push(parent)
|
||||
{
|
||||
if parent.first_frame {
|
||||
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()
|
||||
}
|
||||
if (ui.hot == parent.key) {
|
||||
parent.style.bg_color = Color_Blue
|
||||
}
|
||||
parent.layout.pos = debug.draggable_box_pos
|
||||
parent.layout.size.min = debug.draggable_box_size
|
||||
}
|
||||
ui_resizable_handles( & parent, & debug.draggable_box_pos, & debug.draggable_box_size)
|
||||
|
||||
child_layout := default_layout ^
|
||||
child_layout.size = range2({ 100, 100 }, { 0, 0 })
|
||||
child_layout.alignment = { 0.0, 0.0 }
|
||||
// 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_layout.flags = {
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
// .Origin_At_Anchor_Center
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
state := get_state(); using state
|
||||
ui := ui_context
|
||||
|
||||
@static pos : Vec2
|
||||
layout := ui_layout_peek().default
|
||||
layout.text_alignment = { 1.0, 1.0 }
|
||||
// style.flags = { .Size_To_Text }
|
||||
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.layout.pos
|
||||
}
|
||||
|
||||
if text_box.active {
|
||||
pos += mouse_world_delta()
|
||||
}
|
||||
|
||||
text_box.layout.pos = pos
|
||||
|
||||
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 )
|
||||
{
|
||||
profile("Whitespace AST test")
|
||||
state := get_state(); using state
|
||||
ui := ui_context
|
||||
|
||||
text_layout := default_layout^
|
||||
text_layout.flags = {
|
||||
.Origin_At_Anchor_Center,
|
||||
.Fixed_Position_X, .Fixed_Position_Y,
|
||||
.Fixed_Width, .Fixed_Height,
|
||||
}
|
||||
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() )
|
||||
|
||||
// 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( debug.lorem_parse.lines )
|
||||
{
|
||||
if line_id == 0 {
|
||||
line_id += 1
|
||||
continue
|
||||
}
|
||||
|
||||
ui_layout( text_layout )
|
||||
line_hbox := ui_widget(str_fmt_alloc( "line %v", line_id ), {.Mouse_Clickable})
|
||||
|
||||
if line_hbox.key == ui.hot
|
||||
{
|
||||
line_hbox.text = StrRunesPair {}
|
||||
ui_parent(line_hbox)
|
||||
|
||||
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
|
||||
ui_theme( to_ui_layout_combo(chunk_layout), to_ui_style_combo(chunk_style) )
|
||||
|
||||
head := line.first
|
||||
for ; head != nil;
|
||||
{
|
||||
ui_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.layout.size.min.x = chunk_layout.pos.x
|
||||
}
|
||||
else
|
||||
{
|
||||
builder_backing : [16 * Kilobyte] byte
|
||||
builder := str.builder_from_bytes( builder_backing[:] )
|
||||
|
||||
line_hbox.layout.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 )
|
||||
text_layout.pos.x = text_layout.pos.x
|
||||
text_layout.pos.y += size_range2(line_hbox.computed.bounds).y
|
||||
}
|
||||
else {
|
||||
text_layout.pos.y += size_range2( (& widgets.data[ widgets.num - 1 ]).computed.bounds ).y
|
||||
}
|
||||
|
||||
line_id += 1
|
||||
}
|
||||
|
||||
label_id += 1 // Dummy action
|
||||
}
|
37
code/sectr/ui/theme.odin
Normal file
37
code/sectr/ui/theme.odin
Normal file
@ -0,0 +1,37 @@
|
||||
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 )
|
||||
}
|
3
code/sectr/ui/util.odin
Normal file
3
code/sectr/ui/util.odin
Normal file
@ -0,0 +1,3 @@
|
||||
package sectr
|
||||
|
||||
|
541
code/sectr/ui/widgets.odin
Normal file
541
code/sectr/ui/widgets.odin
Normal file
@ -0,0 +1,541 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
import lalg "core:math/linalg"
|
||||
|
||||
UI_Widget :: struct {
|
||||
using box : ^UI_Box,
|
||||
using signal : UI_Signal,
|
||||
}
|
||||
|
||||
ui_widget :: proc( label : string, flags : UI_BoxFlags ) -> (widget : UI_Widget)
|
||||
{
|
||||
widget.box = ui_box_make( flags, label )
|
||||
widget.signal = ui_signal_from_box( widget.box )
|
||||
return
|
||||
}
|
||||
|
||||
ui_button :: proc( label : string, flags : UI_BoxFlags = {} ) -> (btn : UI_Widget)
|
||||
{
|
||||
btn_flags := UI_BoxFlags { .Mouse_Clickable, .Focusable, .Click_To_Focus }
|
||||
btn.box = ui_box_make( btn_flags | flags, label )
|
||||
btn.signal = ui_signal_from_box( btn.box )
|
||||
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.
|
||||
|
||||
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 managed 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.
|
||||
*/
|
||||
|
||||
// Horizontal Widget
|
||||
UI_HBox :: struct {
|
||||
using widget : UI_Widget,
|
||||
direction : UI_LayoutDirectionX,
|
||||
}
|
||||
|
||||
// Boilerplate creation
|
||||
ui_hbox_begin :: proc( direction : UI_LayoutDirectionX, label : string, flags : UI_BoxFlags = {} ) -> (hbox : UI_HBox) {
|
||||
// profile(#procedure)
|
||||
hbox.direction = direction
|
||||
hbox.box = ui_box_make( flags, label )
|
||||
hbox.signal = ui_signal_from_box(hbox.box)
|
||||
// ui_box_compute_layout(hbox)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-layout children
|
||||
ui_hbox_end :: proc( hbox : UI_HBox, width_ref : ^f32 = nil, compute_layout := true )
|
||||
{
|
||||
// profile(#procedure)
|
||||
if compute_layout do ui_box_compute_layout(hbox.box, dont_mark_fresh = true)
|
||||
ui_layout_children_horizontally( hbox.box, hbox.direction, width_ref )
|
||||
}
|
||||
|
||||
@(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_auto :: proc( hbox : UI_HBox ) {
|
||||
ui_hbox_end(hbox)
|
||||
ui_parent_pop()
|
||||
}
|
||||
#endregion("Horizontal Box")
|
||||
|
||||
#region("Resizable")
|
||||
// Parameterized widget def for ui_resizable_handles
|
||||
UI_Resizable :: struct {
|
||||
using widget : UI_Widget,
|
||||
handle_width : f32,
|
||||
theme : ^UI_Theme,
|
||||
left : bool,
|
||||
right : bool,
|
||||
top : bool,
|
||||
bottom : bool,
|
||||
corner_tr : bool,
|
||||
corner_tl : bool,
|
||||
corner_br : bool,
|
||||
corner_bl : bool,
|
||||
compute_layout : bool
|
||||
}
|
||||
|
||||
ui_resizable_begin :: proc( label : string, flags : UI_BoxFlags = {},
|
||||
handle_width : f32 = 15,
|
||||
theme : ^UI_Theme,
|
||||
left := true,
|
||||
right := true,
|
||||
top := true,
|
||||
bottom := true,
|
||||
corner_tr := true,
|
||||
corner_tl := true,
|
||||
corner_br := true,
|
||||
corner_bl := true,
|
||||
compute_layout := true ) -> (resizable : UI_Resizable)
|
||||
{
|
||||
resizable.box = ui_box_make(flags, label)
|
||||
resizable.signal = ui_signal_from_box(resizable.box)
|
||||
|
||||
resizable.handle_width = handle_width
|
||||
resizable.theme = theme
|
||||
resizable.left = left
|
||||
resizable.right = right
|
||||
resizable.top = top
|
||||
resizable.bottom = bottom
|
||||
resizable.corner_tr = corner_tr
|
||||
resizable.corner_tl = corner_tl
|
||||
resizable.corner_br = corner_br
|
||||
resizable.corner_bl = corner_bl
|
||||
resizable.compute_layout = compute_layout
|
||||
return
|
||||
}
|
||||
|
||||
ui_resizable_end :: proc( resizable : ^UI_Resizable, pos, size : ^Vec2 ) {
|
||||
using resizable
|
||||
ui_resizable_handles( & widget, pos, size,
|
||||
handle_width,
|
||||
theme,
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
corner_tr,
|
||||
corner_tl,
|
||||
corner_br,
|
||||
corner_bl,
|
||||
compute_layout)
|
||||
}
|
||||
|
||||
ui_resizable_begin_auto :: proc() {
|
||||
|
||||
}
|
||||
|
||||
ui_resizable_end_auto :: proc() {
|
||||
|
||||
}
|
||||
|
||||
// Adds resizable handles to a widget
|
||||
ui_resizable_handles :: proc( parent : ^UI_Widget, pos : ^Vec2, size : ^Vec2,
|
||||
handle_width : f32 = 15,
|
||||
theme : ^UI_Theme = nil,
|
||||
left := true,
|
||||
right := true,
|
||||
top := true,
|
||||
bottom := true,
|
||||
corner_tr := true,
|
||||
corner_tl := true,
|
||||
corner_br := true,
|
||||
corner_bl := true,
|
||||
compute_layout := true) -> (drag_signal : b32)
|
||||
{
|
||||
profile(#procedure)
|
||||
handle_left : UI_Widget
|
||||
handle_right : UI_Widget
|
||||
handle_top : UI_Widget
|
||||
handle_bottom : UI_Widget
|
||||
handle_corner_tr : UI_Widget
|
||||
handle_corner_tl : UI_Widget
|
||||
handle_corner_br : UI_Widget
|
||||
handle_corner_bl : UI_Widget
|
||||
ui_parent(parent)
|
||||
|
||||
@(deferred_none = ui_theme_pop)
|
||||
theme_handle :: proc( base : ^UI_Theme, margins, size : Vec2, flags : UI_LayoutFlags = {})
|
||||
{
|
||||
layout_combo : UI_LayoutCombo
|
||||
style_combo : UI_StyleCombo
|
||||
if base != nil
|
||||
{
|
||||
layout_combo = base.layout
|
||||
style_combo = base.style
|
||||
{
|
||||
layout_combo.default.margins = {margins.x, margins.x, margins.y, margins.y}
|
||||
layout_combo.default.size.min = size
|
||||
}
|
||||
{
|
||||
layout_combo.hot.margins = {margins.x, margins.x, margins.y, margins.y}
|
||||
layout_combo.hot.size.min = size
|
||||
}
|
||||
{
|
||||
layout_combo.active.margins = {margins.x, margins.x, margins.y, margins.y}
|
||||
layout_combo.active.size.min = size
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
layout := UI_Layout {
|
||||
flags = flags,
|
||||
anchor = range2({},{}),
|
||||
alignment = {0, 0},
|
||||
text_alignment = {0.0, 0.0},
|
||||
font_size = 16,
|
||||
margins = to_ui_layout_side(margins),
|
||||
padding = {0, 0, 0, 0},
|
||||
border_width = 0,
|
||||
pos = {0, 0},
|
||||
size = range2(size,{})
|
||||
}
|
||||
style := UI_Style {
|
||||
bg_color = Color_Transparent,
|
||||
border_color = Color_Transparent,
|
||||
corner_radii = {5, 0, 0, 0},
|
||||
blur_size = 0,
|
||||
font = get_state().default_font,
|
||||
text_color = Color_ThmDark_Text_Default,
|
||||
cursor = {},
|
||||
}
|
||||
layout_combo = to_ui_layout_combo(layout)
|
||||
style_combo = to_ui_style_combo(style)
|
||||
{
|
||||
using layout_combo.hot
|
||||
using style_combo.hot
|
||||
bg_color = Color_ThmDark_ResizeHandle_Hot
|
||||
}
|
||||
{
|
||||
using layout_combo.active
|
||||
using style_combo.active
|
||||
bg_color = Color_ThmDark_ResizeHandle_Active
|
||||
}
|
||||
}
|
||||
theme := UI_Theme {
|
||||
layout_combo, style_combo
|
||||
}
|
||||
ui_layout_push(theme.layout)
|
||||
ui_style_push(theme.style)
|
||||
}
|
||||
|
||||
flags := UI_BoxFlags { .Mouse_Clickable }
|
||||
|
||||
name :: proc( label : string ) -> string {
|
||||
parent_label := (transmute(^string) context.user_ptr) ^
|
||||
return str_intern(str_fmt_alloc("%v: %v", parent_label, label )).str
|
||||
}
|
||||
context.user_ptr = & parent.label
|
||||
|
||||
#region("Handle & Corner construction")
|
||||
theme_handle( theme, {handle_width, 0}, {handle_width,0})
|
||||
if left {
|
||||
handle_left = ui_widget(name("resize_handle_left"), flags )
|
||||
handle_left.layout.anchor.left = 0
|
||||
handle_left.layout.anchor.right = 1
|
||||
handle_left.layout.alignment = {1, 0}
|
||||
}
|
||||
if right {
|
||||
handle_right = ui_widget(name("resize_handle_right"), flags )
|
||||
handle_right.layout.anchor.left = 1
|
||||
}
|
||||
theme_handle( theme, {0, handle_width}, {0, handle_width})
|
||||
if top {
|
||||
handle_top = ui_widget(name("resize_handle_top"), flags )
|
||||
handle_top.layout.anchor.bottom = 1
|
||||
handle_top.layout.alignment = {0, -1}
|
||||
}
|
||||
if bottom {
|
||||
handle_bottom = ui_widget("resize_handle_bottom", flags)
|
||||
handle_bottom.layout.anchor.top = 1
|
||||
handle_bottom.layout.alignment = { 0, 0 }
|
||||
}
|
||||
theme_handle( theme, {0,0}, {handle_width, handle_width}, {.Fixed_Width, .Fixed_Height} )
|
||||
if corner_tl {
|
||||
handle_corner_tl = ui_widget(name("corner_top_left"), flags)
|
||||
handle_corner_tl.layout.alignment = {1, -1}
|
||||
}
|
||||
if corner_tr {
|
||||
handle_corner_tr = ui_widget(name("corner_top_right"), flags)
|
||||
handle_corner_tr.layout.anchor = range2({1, 0}, {})
|
||||
handle_corner_tr.layout.alignment = {0, -1}
|
||||
}
|
||||
if corner_bl {
|
||||
handle_corner_bl = ui_widget("corner_bottom_left", flags)
|
||||
handle_corner_bl.layout.anchor = range2({}, {0, 1})
|
||||
handle_corner_bl.layout.alignment = { 1, 0 }
|
||||
}
|
||||
if corner_br {
|
||||
handle_corner_br = ui_widget("corner_bottom_right", flags)
|
||||
handle_corner_br.layout.anchor = range2({1, 0}, {0, 1})
|
||||
handle_corner_br.layout.alignment = {0, 0}
|
||||
}
|
||||
#endregion("Handle & Corner construction")
|
||||
|
||||
process_handle_drag :: #force_inline proc ( handle : ^UI_Widget,
|
||||
direction : Vec2,
|
||||
target_alignment : Vec2,
|
||||
target_center_aligned : Vec2,
|
||||
pos : ^Vec2,
|
||||
size : ^Vec2,
|
||||
alignment : ^Vec2, ) -> b32
|
||||
{
|
||||
@static active_context : ^UI_State
|
||||
@static was_dragging : b32 = false
|
||||
@static start_size : Vec2
|
||||
@static prev_left_shift_held : b8
|
||||
@static prev_alignment : Vec2
|
||||
|
||||
ui := get_state().ui_context
|
||||
using handle
|
||||
if ui.last_pressed_key != key || (!active && (!released || !was_dragging)) do return false
|
||||
|
||||
direction := direction
|
||||
align_adjsutment := left_shift_held ? target_center_aligned : target_alignment
|
||||
|
||||
size_delta := ui_drag_delta()
|
||||
pos_adjust := size^ * (alignment^ - align_adjsutment)
|
||||
pos_reverse := size^ * (alignment^ - prev_alignment)
|
||||
|
||||
shift_changed := (left_shift_held != prev_left_shift_held)
|
||||
|
||||
need_to_change_alignment_and_pos := pressed || shift_changed
|
||||
|
||||
if active
|
||||
{
|
||||
if pressed
|
||||
{
|
||||
active_context = ui
|
||||
start_size = size^
|
||||
prev_left_shift_held = left_shift_held
|
||||
}
|
||||
if (.Origin_At_Anchor_Center in parent.layout.flags) && !left_shift_held {
|
||||
pos_adjust = size^ * 0.5 * direction
|
||||
pos_reverse = size^ * 0.5 * direction
|
||||
}
|
||||
|
||||
latest_size := start_size + size_delta * direction
|
||||
|
||||
if pressed
|
||||
{
|
||||
pos^ -= pos_adjust
|
||||
}
|
||||
else if shift_changed
|
||||
{
|
||||
if (.Origin_At_Anchor_Center in parent.layout.flags) {
|
||||
pos^ -= pos_reverse
|
||||
alignment^ = !left_shift_held ? target_center_aligned : target_alignment
|
||||
}
|
||||
else
|
||||
{
|
||||
if !left_shift_held {
|
||||
pos^ -= size^ * direction * 0.5
|
||||
alignment^ = target_center_aligned
|
||||
}
|
||||
else {
|
||||
pos^ += size^ * direction * 0.5 // Right
|
||||
alignment^ = target_alignment
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size^ = latest_size
|
||||
alignment^ = align_adjsutment
|
||||
}
|
||||
was_dragging = true
|
||||
}
|
||||
else if released && was_dragging
|
||||
{
|
||||
// This needed to be added as for some reason, this was getting called in screen_ui even when we were resizing with a handle in a worksapce
|
||||
if active_context != ui do return false
|
||||
|
||||
if (.Origin_At_Anchor_Center in parent.layout.flags) && !left_shift_held {
|
||||
pos_adjust = size^ * 0.5 * direction
|
||||
pos_reverse = size^ * 0.5 * direction
|
||||
}
|
||||
pos^ += pos_adjust
|
||||
alignment^ = align_adjsutment
|
||||
was_dragging = false
|
||||
start_size = 0
|
||||
}
|
||||
// text = active_context.root.label
|
||||
// style.text_color = Color_White
|
||||
|
||||
prev_left_shift_held = handle.left_shift_held
|
||||
prev_alignment = align_adjsutment
|
||||
return was_dragging
|
||||
}
|
||||
|
||||
state := get_state()
|
||||
alignment := & parent.layout.alignment
|
||||
|
||||
if .Origin_At_Anchor_Center in parent.layout.flags
|
||||
{
|
||||
if right do drag_signal |= process_handle_drag( & handle_right, { 1, 0}, { 0.5, 0}, {0, 0}, pos, size, alignment )
|
||||
if left do drag_signal |= process_handle_drag( & handle_left, {-1, 0}, {-0.5, 0}, {0, 0}, pos, size, alignment )
|
||||
if top do drag_signal |= process_handle_drag( & handle_top, { 0, 1}, { 0, 0.5}, {0, 0}, pos, size, alignment )
|
||||
if bottom do drag_signal |= process_handle_drag( & handle_bottom, { 0, -1}, { 0, -0.5}, {0, 0}, pos, size, alignment )
|
||||
if corner_tr do drag_signal |= process_handle_drag( & handle_corner_tr, { 1, 1}, { 0.5, 0.5}, {0, 0}, pos, size, alignment )
|
||||
if corner_tl do drag_signal |= process_handle_drag( & handle_corner_tl, {-1, 1}, {-0.5, 0.5}, {0, 0}, pos, size, alignment )
|
||||
if corner_br do drag_signal |= process_handle_drag( & handle_corner_br, { 1, -1}, { 0.5, -0.5}, {0, 0}, pos, size, alignment )
|
||||
if corner_bl do drag_signal |= process_handle_drag( & handle_corner_bl, {-1, -1}, {-0.5, -0.5}, {0, 0}, pos, size, alignment )
|
||||
}
|
||||
else
|
||||
{
|
||||
if right do drag_signal |= process_handle_drag( & handle_right, { 1, 0 }, {0, 0}, { 0.5, 0}, pos, size, alignment )
|
||||
if left do drag_signal |= process_handle_drag( & handle_left, { -1, 0 }, {1, 0}, { 0.5, 0}, pos, size, alignment )
|
||||
if top do drag_signal |= process_handle_drag( & handle_top, { 0, 1 }, {0, -1}, { 0.0, -0.5}, pos, size, alignment )
|
||||
if bottom do drag_signal |= process_handle_drag( & handle_bottom, { 0, -1 }, {0, 0}, { 0.0, -0.5}, pos, size, alignment )
|
||||
if corner_tr do drag_signal |= process_handle_drag( & handle_corner_tr, { 1, 1 }, {0, -1}, { 0.5, -0.5}, pos, size, alignment )
|
||||
if corner_tl do drag_signal |= process_handle_drag( & handle_corner_tl, { -1, 1 }, {1, -1}, { 0.5, -0.5}, pos, size, alignment )
|
||||
if corner_br do drag_signal |= process_handle_drag( & handle_corner_br, { 1, -1 }, {0, 0}, { 0.5, -0.5}, pos, size, alignment )
|
||||
if corner_bl do drag_signal |= process_handle_drag( & handle_corner_bl, { -1, -1 }, {1, 0}, { 0.5, -0.5}, pos, size, alignment )
|
||||
}
|
||||
|
||||
if drag_signal && compute_layout do ui_box_compute_layout(parent)
|
||||
return
|
||||
}
|
||||
#endregion("Resizable")
|
||||
|
||||
ui_spacer :: proc( label : string ) -> (widget : UI_Widget) {
|
||||
widget.box = ui_box_make( {.Mouse_Clickable}, label )
|
||||
widget.signal = ui_signal_from_box( widget.box )
|
||||
|
||||
widget.style.bg_color = Color_Transparent
|
||||
return
|
||||
}
|
||||
|
||||
UI_ScrollBox :: struct {
|
||||
using widget : UI_Widget,
|
||||
scroll_bar : UI_Widget,
|
||||
content : UI_Widget,
|
||||
}
|
||||
|
||||
ui_scroll_box :: proc( label : string, flags : UI_BoxFlags ) -> (scroll_box : UI_ScrollBox) {
|
||||
fatal("NOT IMPLEMENTED")
|
||||
return
|
||||
}
|
||||
|
||||
// ui_scrollable_view( )
|
||||
|
||||
#region("Text")
|
||||
|
||||
ui_text :: proc( label : string, content : StrRunesPair, 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
|
||||
return { box, signal }
|
||||
}
|
||||
|
||||
ui_text_spaces :: proc( label : string, flags : UI_BoxFlags = {} ) -> UI_Widget
|
||||
{
|
||||
// profile(#procedure)
|
||||
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
|
||||
return { box, signal }
|
||||
}
|
||||
|
||||
ui_text_tabs :: 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" )
|
||||
|
||||
box := ui_box_make( flags, label )
|
||||
signal := ui_signal_from_box( box )
|
||||
|
||||
box.text = tab_str
|
||||
return { box, signal }
|
||||
}
|
||||
|
||||
ui_text_wrap_panel :: proc( parent : ^UI_Widget )
|
||||
{
|
||||
fatal("NOT IMPLEMENTED")
|
||||
}
|
||||
#endregion("Text")
|
||||
|
||||
#region("Vertical Box")
|
||||
/*
|
||||
Vertical Boxes automatically manage a collection of widgets and
|
||||
attempt to slot them adjacent to each other along the y-axis.
|
||||
|
||||
The user must provide the direction that the vbox 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 managed 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.y value to determine the "stretch ratio".
|
||||
|
||||
Keep in mind the stretch ratio is only respected if no size.min.y value is violated for each of the widgets.
|
||||
*/
|
||||
|
||||
UI_VBox :: struct {
|
||||
using widget : UI_Widget,
|
||||
direction : UI_LayoutDirectionY,
|
||||
}
|
||||
|
||||
// Boilerplate creation
|
||||
ui_vbox_begin :: proc( direction : UI_LayoutDirectionY, label : string, flags : UI_BoxFlags = {}, compute_layout := false ) -> (vbox : UI_VBox) {
|
||||
// profile(#procedure)
|
||||
vbox.direction = direction
|
||||
vbox.box = ui_box_make( flags, label )
|
||||
vbox.signal = ui_signal_from_box( vbox.box )
|
||||
if compute_layout do ui_box_compute_layout(vbox, dont_mark_fresh = true)
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-layout children
|
||||
ui_vbox_end :: proc( vbox : UI_VBox, height_ref : ^f32 = nil, compute_layout := true ) {
|
||||
// profile(#procedure)
|
||||
if compute_layout do ui_box_compute_layout(vbox, dont_mark_fresh = true)
|
||||
ui_layout_children_vertically( vbox.box, vbox.direction, height_ref )
|
||||
}
|
||||
|
||||
// Auto-layout children and pop parent from parent stack
|
||||
ui_vbox_end_pop_parent :: proc( vbox : UI_VBox ) {
|
||||
ui_parent_pop()
|
||||
ui_vbox_end(vbox)
|
||||
}
|
||||
|
||||
@(deferred_out = ui_vbox_end_pop_parent)
|
||||
ui_vbox :: #force_inline proc( direction : UI_LayoutDirectionY, label : string, flags : UI_BoxFlags = {} ) -> (vbox : UI_VBox) {
|
||||
vbox = ui_vbox_begin(direction, label, flags)
|
||||
ui_parent_push(vbox.widget)
|
||||
return
|
||||
}
|
||||
#endregion("Vertical Box")
|
Reference in New Issue
Block a user