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:
2024-05-16 17:27:15 -04:00
parent 0527a033c8
commit b30f3c3466
75 changed files with 24 additions and 97 deletions

View 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
View 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
View 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
}

View 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 }

View 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,
})
}
}

View 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
}

View 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 }

View 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
View 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 )
}
}

View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
package sectr

541
code/sectr/ui/widgets.odin Normal file
View 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")