got basic ui elmental interaction working, + alignment of anchor
This commit is contained in:
442
code/ui.odin
442
code/ui.odin
@ -1,12 +1,8 @@
|
||||
package sectr
|
||||
|
||||
import "base:runtime"
|
||||
|
||||
// TODO(Ed) : This is in Raddbg base_types.h, consider moving outside of UI.
|
||||
Axis2 :: enum i32 {
|
||||
Invalid = -1,
|
||||
X = 0,
|
||||
Y = 1,
|
||||
Count,
|
||||
}
|
||||
|
||||
Corner :: enum i32 {
|
||||
Invalid = -1,
|
||||
@ -21,23 +17,6 @@ Corner :: enum i32 {
|
||||
Count = 4,
|
||||
}
|
||||
|
||||
Range2 :: struct #raw_union{
|
||||
using _ : struct {
|
||||
min, max : Vec2
|
||||
},
|
||||
using _ : struct {
|
||||
p0, p1 : Vec2
|
||||
},
|
||||
using _ : struct {
|
||||
x0, y0 : f32,
|
||||
x1, y1 : f32,
|
||||
},
|
||||
}
|
||||
|
||||
Rect :: struct {
|
||||
top_left, bottom_right : Vec2
|
||||
}
|
||||
|
||||
Side :: enum i32 {
|
||||
Invalid = -1,
|
||||
Min = 0,
|
||||
@ -74,35 +53,36 @@ UI_AnchorPresets :: enum u32 {
|
||||
}
|
||||
|
||||
UI_BoxFlag :: enum u64 {
|
||||
Disabled,
|
||||
Focusable,
|
||||
|
||||
Mouse_Clickable,
|
||||
Keyboard_Clickable,
|
||||
Click_To_Focus,
|
||||
|
||||
Fixed_With,
|
||||
Fixed_Height,
|
||||
Scroll_X,
|
||||
Scroll_Y,
|
||||
|
||||
Text_Wrap,
|
||||
Pan_X,
|
||||
Pan_Y,
|
||||
|
||||
Count,
|
||||
}
|
||||
UI_BoxFlags :: bit_set[UI_BoxFlag; u64]
|
||||
UI_BoxFlag_Scroll :: UI_BoxFlags { .Scroll_X, .Scroll_Y }
|
||||
|
||||
// The UI_Box's actual positioning and sizing
|
||||
// There is an excess of rectangles here for debug puproses.
|
||||
UI_Computed :: struct {
|
||||
bounds : Range2,
|
||||
border : Range2,
|
||||
margin : Range2,
|
||||
padding : Range2,
|
||||
content : Range2,
|
||||
}
|
||||
|
||||
UI_LayoutSide :: struct #raw_union {
|
||||
using _ : struct {
|
||||
top, bottom : UI_Scalar2,
|
||||
left, right : UI_Scalar2,
|
||||
top, bottom : UI_Scalar,
|
||||
left, right : UI_Scalar,
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,6 +90,18 @@ 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
|
||||
@ -128,8 +120,12 @@ UI_Scalar2 :: [Axis2.Count]UI_Scalar
|
||||
|
||||
// Desiered constraints on the UI_Box.
|
||||
UI_Layout :: struct {
|
||||
// TODO(Ed) : Should layout have its own flags (separate from the style flags)
|
||||
// flags : UI_LayoutFlags
|
||||
|
||||
// TODO(Ed) : Make sure this is all we need to represent an anchor.
|
||||
anchor : Range2,
|
||||
anchor : Range2,
|
||||
alignment : Vec2,
|
||||
|
||||
border_width : UI_Scalar,
|
||||
|
||||
@ -138,19 +134,62 @@ UI_Layout :: struct {
|
||||
|
||||
corner_radii : [Corner.Count]f32,
|
||||
|
||||
// TODO(Ed) : Add support for this
|
||||
size_to_content : b32,
|
||||
size : Vec2,
|
||||
// Position in relative coordinate space.
|
||||
// If the box's flags has Fixed_Position, then this will be its aboslute position in the relative coordinate space
|
||||
pos : Vec2,
|
||||
// TODO(Ed) : Should everything no matter what its parent is use a WS_Pos instead of a raw vector pos?
|
||||
|
||||
// If the box is a child of the root parent, its automatically in world space and thus will use the tile_pos.
|
||||
tile_pos : WS_Pos,
|
||||
|
||||
// TODO(Ed) : Add support for size_to_content?
|
||||
// size_to_content : b32,
|
||||
size : Vec2,
|
||||
}
|
||||
|
||||
UI_BoxState :: enum {
|
||||
Disabled,
|
||||
UI_Signal :: struct {
|
||||
box : ^ UI_Box,
|
||||
|
||||
cursor_pos : Vec2,
|
||||
drag_delta : Vec2,
|
||||
scroll : Vec2,
|
||||
|
||||
left_clicked : b8,
|
||||
right_clicked : b8,
|
||||
double_clicked : b8,
|
||||
keyboard_clicked : b8,
|
||||
|
||||
pressed : b8,
|
||||
released : b8,
|
||||
dragging : b8,
|
||||
hovering : b8,
|
||||
cursor_over : b8,
|
||||
commit : b8,
|
||||
}
|
||||
|
||||
UI_StyleFlag :: enum u32 {
|
||||
Fixed_Position_X,
|
||||
Fixed_Position_Y,
|
||||
Fixed_Width,
|
||||
Fixed_Height,
|
||||
|
||||
Text_Wrap,
|
||||
|
||||
Count,
|
||||
}
|
||||
UI_StyleFlags :: bit_set[UI_StyleFlag; u32]
|
||||
|
||||
UI_StylePreset :: enum u32 {
|
||||
Default,
|
||||
Disabled,
|
||||
Hovered,
|
||||
Focused,
|
||||
Count,
|
||||
}
|
||||
|
||||
UI_Style :: struct {
|
||||
flags : UI_StyleFlags,
|
||||
|
||||
bg_color : Color,
|
||||
border_color : Color,
|
||||
|
||||
@ -168,6 +207,13 @@ UI_Style :: struct {
|
||||
transition_time : f32,
|
||||
}
|
||||
|
||||
UI_StyleTheme :: struct #raw_union {
|
||||
array : [UI_StylePreset.Count] UI_Style,
|
||||
using styles : struct {
|
||||
default, disabled, hovered, focused : UI_Style,
|
||||
}
|
||||
}
|
||||
|
||||
UI_TextAlign :: enum u32 {
|
||||
Left,
|
||||
Center,
|
||||
@ -175,45 +221,35 @@ UI_TextAlign :: enum u32 {
|
||||
Count
|
||||
}
|
||||
|
||||
UI_InteractState :: struct {
|
||||
hot_time : f32,
|
||||
active_time : f32,
|
||||
disabled_time : f32,
|
||||
}
|
||||
|
||||
UI_Box :: struct {
|
||||
// Cache ID
|
||||
key : UI_Key,
|
||||
label : string,
|
||||
|
||||
// Regenerated per frame.
|
||||
using _ : DLL_NodeFull( UI_Box ), // first, last, prev, next
|
||||
using links : DLL_NodeFull( UI_Box ), // first, last, prev, next
|
||||
parent : ^ UI_Box,
|
||||
num_children : i32,
|
||||
|
||||
flags : UI_BoxFlags,
|
||||
computed : UI_Computed,
|
||||
style : UI_Style,
|
||||
theme : UI_StyleTheme,
|
||||
style : ^ UI_Style,
|
||||
|
||||
// Persistent Data
|
||||
// hash_links : DLL_Node_PN( ^ UI_Box), // This isn't necessary if not using RJF hash table.
|
||||
style_delta : f32,
|
||||
// prev_computed : UI_Computed,
|
||||
// prev_style : UI_Style,v
|
||||
mouse : UI_InteractState,
|
||||
keyboard : UI_InteractState,
|
||||
}
|
||||
|
||||
// UI_BoxFlags_Stack_Size :: 512
|
||||
UI_Layout_Stack_Size :: 512
|
||||
UI_Style_Stack_Size :: 512
|
||||
UI_Parent_Stack_Size :: 1024
|
||||
UI_Built_Boxes_Array_Size :: Kilobyte * 8
|
||||
|
||||
UI_FramePassKind :: enum {
|
||||
Generate,
|
||||
Compute,
|
||||
Logical,
|
||||
}
|
||||
|
||||
UI_State :: struct {
|
||||
// TODO(Ed) : Use these
|
||||
build_arenas : [2]Arena,
|
||||
@ -225,16 +261,19 @@ UI_State :: struct {
|
||||
prev_cache : ^ HMapZPL( UI_Box ),
|
||||
curr_cache : ^ HMapZPL( UI_Box ),
|
||||
|
||||
root : ^ UI_Box,
|
||||
null_box : ^ UI_Box, // Ryan had this, I don't know why yet.
|
||||
root : ^ UI_Box,
|
||||
|
||||
// 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)
|
||||
style_stack : Stack( UI_Style, UI_Style_Stack_Size ),
|
||||
parent_stack : Stack( ^ UI_Box, UI_Parent_Stack_Size ),
|
||||
theme_stack : Stack( UI_StyleTheme, UI_Style_Stack_Size ),
|
||||
parent_stack : Stack( ^ UI_Box, UI_Parent_Stack_Size ),
|
||||
// flag_stack : Stack( UI_BoxFlags, UI_BoxFlags_Stack_Size ),
|
||||
|
||||
hot : UI_Key,
|
||||
active_mouse : [MouseBtn.count] UI_Key,
|
||||
active : UI_Key,
|
||||
clipboard_copy : UI_Key,
|
||||
last_clicked : UI_Key,
|
||||
@ -242,21 +281,9 @@ UI_State :: struct {
|
||||
drag_start_mouse : Vec2,
|
||||
// drag_state_arena : ^ Arena,
|
||||
// drag_state data : string,
|
||||
}
|
||||
|
||||
ui_key_from_string :: proc( value : string ) -> UI_Key {
|
||||
key := cast(UI_Key) crc32( transmute([]byte) value )
|
||||
return key
|
||||
}
|
||||
|
||||
ui_box_equal :: proc( 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
|
||||
last_pressed_key : [MouseBtn.count] UI_Key,
|
||||
last_pressed_key_us : [MouseBtn.count] f32,
|
||||
}
|
||||
|
||||
ui_startup :: proc( ui : ^ UI_State, cache_allocator : Allocator )
|
||||
@ -287,53 +314,13 @@ ui_reload :: proc( ui : ^ UI_State, cache_allocator : Allocator )
|
||||
ui_shutdown :: proc() {
|
||||
}
|
||||
|
||||
ui_graph_build_begin :: proc( ui : ^ UI_State, bounds : Vec2 = {} )
|
||||
{
|
||||
get_state().ui_context = ui
|
||||
using get_state().ui_context
|
||||
ui_box_equal :: proc( a, b : ^ UI_Box ) -> b32 {
|
||||
BoxSize :: size_of(UI_Box)
|
||||
|
||||
swap( & curr_cache, & prev_cache )
|
||||
|
||||
root = ui_box_make( {}, "root#001" )
|
||||
ui_parent_push(root)
|
||||
|
||||
log("BUILD GRAPH BEGIN")
|
||||
}
|
||||
|
||||
// TODO(Ed) :: Is this even needed?
|
||||
ui_graph_build_end :: proc()
|
||||
{
|
||||
ui_parent_pop()
|
||||
|
||||
// Regenerate the computed layout if dirty
|
||||
// ui_compute_layout()
|
||||
|
||||
get_state().ui_context = nil
|
||||
log("BUILD GRAPH END")
|
||||
}
|
||||
|
||||
@(deferred_none = ui_graph_build_end)
|
||||
ui_graph_build :: proc( ui : ^ UI_State ) {
|
||||
ui_graph_build_begin( ui )
|
||||
}
|
||||
|
||||
ui_parent_push :: proc( ui : ^ UI_Box ) {
|
||||
stack := & get_state().ui_context.parent_stack
|
||||
stack_push( & get_state().ui_context.parent_stack, ui )
|
||||
}
|
||||
|
||||
ui_parent_pop :: proc() {
|
||||
// If size_to_content is set, we need to compute the layout now.
|
||||
|
||||
// Check to make sure that the parent's children are the same for this frame,
|
||||
// if its not we need to mark the layout as dirty.
|
||||
|
||||
stack_pop( & get_state().ui_context.parent_stack )
|
||||
}
|
||||
|
||||
@(deferred_none = ui_parent_pop)
|
||||
ui_parent :: proc( ui : ^ UI_Box) {
|
||||
ui_parent_push( ui )
|
||||
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_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box)
|
||||
@ -362,6 +349,7 @@ ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box)
|
||||
curr_box = set_result
|
||||
}
|
||||
|
||||
// TODO(Ed) : Review this when we learn layouts more...
|
||||
if prev_box != nil {
|
||||
layout_dirty &= ! ui_box_equal( curr_box, prev_box )
|
||||
}
|
||||
@ -370,45 +358,225 @@ ui_box_make :: proc( flags : UI_BoxFlags, label : string ) -> (^ UI_Box)
|
||||
}
|
||||
|
||||
curr_box.flags = flags
|
||||
curr_box.style = ( stack_peek( & style_stack ) ^ )
|
||||
curr_box.parent = ( stack_peek( & parent_stack ) ^ )
|
||||
curr_box.theme = stack_peek( & theme_stack )
|
||||
curr_box.parent = stack_peek( & parent_stack )
|
||||
|
||||
// Clear old links
|
||||
curr_box.parent = nil
|
||||
curr_box.links = {}
|
||||
curr_box.num_children = 0
|
||||
|
||||
// If there is a parent, setup the relevant references
|
||||
if curr_box.parent != nil
|
||||
parent := stack_peek( & parent_stack )
|
||||
if parent != nil
|
||||
{
|
||||
// dbl_linked_list_push_back( box.parent, nil, box )
|
||||
curr_box.parent.last = curr_box
|
||||
|
||||
if curr_box.parent.first == nil {
|
||||
curr_box.parent.first = curr_box
|
||||
}
|
||||
dll_full_push_back( null_box, parent, curr_box )
|
||||
parent.num_children += 1
|
||||
curr_box.parent = parent
|
||||
}
|
||||
|
||||
return curr_box
|
||||
}
|
||||
|
||||
ui_set_layout :: proc ( layout : UI_Layout ) {
|
||||
log("LAYOUT SET")
|
||||
ui_box_tranverse_next :: proc( box : ^ UI_Box ) -> (^ UI_Box) {
|
||||
// If current has children, do them first
|
||||
if box.first != nil {
|
||||
return box.first
|
||||
}
|
||||
|
||||
if box.next == nil
|
||||
{
|
||||
// There is no more adjacent nodes
|
||||
if box.parent != nil {
|
||||
// Lift back up to parent, and set it to its next.
|
||||
return box.parent.next
|
||||
}
|
||||
}
|
||||
|
||||
return box.next
|
||||
}
|
||||
|
||||
ui_compute_layout :: proc() {
|
||||
// TODO(Ed) : This generates the bounds for each box.
|
||||
ui_graph_build_begin :: proc( ui : ^ UI_State, bounds : Vec2 = {} )
|
||||
{
|
||||
get_state().ui_context = ui
|
||||
using get_state().ui_context
|
||||
|
||||
curr_cache, prev_cache = swap( curr_cache, prev_cache )
|
||||
|
||||
if ui.active == UI_Key(0) {
|
||||
ui.hot = UI_Key(0)
|
||||
}
|
||||
|
||||
root = ui_box_make( {}, "root#001" )
|
||||
root.style = & root.theme.default
|
||||
ui_parent_push(root)
|
||||
}
|
||||
|
||||
ui_layout_set_size :: proc( size : Vec2 ) {
|
||||
// TODO(Ed) :: Is this even needed?
|
||||
ui_graph_build_end :: proc()
|
||||
{
|
||||
ui_parent_pop() // Should be ui_context.root
|
||||
|
||||
// Regenerate the computed layout if dirty
|
||||
ui_compute_layout()
|
||||
|
||||
get_state().ui_context = nil
|
||||
}
|
||||
|
||||
ui_style_push :: proc( preset : UI_Style ) {
|
||||
log("STYLE PUSH")
|
||||
stack_push( & get_state().ui_context.style_stack, preset )
|
||||
@(deferred_none = ui_graph_build_end)
|
||||
ui_graph_build :: proc( ui : ^ UI_State ) {
|
||||
ui_graph_build_begin( ui )
|
||||
}
|
||||
|
||||
ui_style_pop :: proc() {
|
||||
log("STYLE POP")
|
||||
stack_pop( & get_state().ui_context.style_stack )
|
||||
ui_key_from_string :: proc( value : string ) -> UI_Key {
|
||||
key := cast(UI_Key) crc32( transmute([]byte) value )
|
||||
return key
|
||||
}
|
||||
|
||||
@(deferred_none = ui_style_pop)
|
||||
ui_style :: proc( preset : UI_Style ) {
|
||||
ui_style_push( preset )
|
||||
ui_parent_push :: proc( ui : ^ UI_Box ) {
|
||||
stack := & get_state().ui_context.parent_stack
|
||||
stack_push( & get_state().ui_context.parent_stack, ui )
|
||||
}
|
||||
|
||||
ui_parent_pop :: proc() {
|
||||
// If size_to_content is set, we need to compute the layout now.
|
||||
|
||||
// Check to make sure that the parent's children are the same for this frame,
|
||||
// if its not we need to mark the layout as dirty.
|
||||
|
||||
stack_pop( & get_state().ui_context.parent_stack )
|
||||
}
|
||||
|
||||
@(deferred_none = ui_parent_pop)
|
||||
ui_parent :: proc( ui : ^ UI_Box) {
|
||||
ui_parent_push( ui )
|
||||
}
|
||||
|
||||
ui_signal_from_box :: proc ( box : ^ UI_Box ) -> UI_Signal
|
||||
{
|
||||
ui := get_state().ui_context
|
||||
input := get_state().input
|
||||
|
||||
signal := UI_Signal { box = box }
|
||||
|
||||
if ui == & get_state().project.workspace.ui {
|
||||
signal.cursor_pos = screen_to_world( input.mouse.pos )
|
||||
}
|
||||
else {
|
||||
signal.cursor_pos = input.mouse.pos
|
||||
}
|
||||
signal.cursor_over = cast(b8) pos_within_range2( signal.cursor_pos, box.computed.bounds )
|
||||
|
||||
left_pressed := pressed( input.mouse.left )
|
||||
left_released := released( input.mouse.left )
|
||||
|
||||
mouse_clickable := UI_BoxFlag.Mouse_Clickable in box.flags
|
||||
keyboard_clickable := UI_BoxFlag.Keyboard_Clickable in box.flags
|
||||
|
||||
if mouse_clickable && signal.cursor_over && left_pressed
|
||||
{
|
||||
ui.hot = box.key
|
||||
ui.active = box.key
|
||||
ui.active_mouse[MouseBtn.Left] = box.key
|
||||
ui.drag_start_mouse = signal.cursor_pos
|
||||
ui.last_pressed_key = box.key
|
||||
|
||||
signal.pressed = true
|
||||
// TODO(Ed) : Support double-click detection
|
||||
}
|
||||
|
||||
if mouse_clickable && signal.cursor_over && left_released
|
||||
{
|
||||
ui.active = UI_Key(0)
|
||||
ui.active_mouse[MouseBtn.Left] = UI_Key(0)
|
||||
signal.released = true
|
||||
signal.left_clicked = true
|
||||
|
||||
ui.last_clicked = box.key
|
||||
}
|
||||
|
||||
if mouse_clickable && ! signal.cursor_over && left_released {
|
||||
ui.hot = UI_Key(0)
|
||||
ui.active = UI_Key(0)
|
||||
ui.active_mouse[MouseBtn.Left] = UI_Key(0)
|
||||
signal.released = true
|
||||
signal.left_clicked = false
|
||||
}
|
||||
|
||||
if keyboard_clickable
|
||||
{
|
||||
// TODO(Ed) : Add keyboard interaction support
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
}
|
||||
|
||||
if signal.cursor_over &&
|
||||
ui.hot == UI_Key(0) || ui.hot == box.key &&
|
||||
ui.active == UI_Key(0) || ui.active == box.key
|
||||
{
|
||||
ui.hot = box.key
|
||||
}
|
||||
|
||||
style_preset := UI_StylePreset.Default
|
||||
if box.key == ui.hot {
|
||||
style_preset = UI_StylePreset.Hovered
|
||||
}
|
||||
if box.key == ui.active {
|
||||
style_preset = UI_StylePreset.Focused
|
||||
}
|
||||
if UI_BoxFlag.Disabled in box.flags {
|
||||
style_preset = UI_StylePreset.Disabled
|
||||
}
|
||||
box.style = & box.theme.array[style_preset]
|
||||
|
||||
return signal
|
||||
}
|
||||
|
||||
ui_style_ref :: proc( box_state : UI_StylePreset ) -> (^ UI_Style) {
|
||||
return & stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state]
|
||||
}
|
||||
|
||||
ui_style_set :: proc ( style : UI_Style, box_state : UI_StylePreset ) {
|
||||
stack_peek_ref( & get_state().ui_context.theme_stack ).array[box_state] = style
|
||||
}
|
||||
|
||||
ui_style_set_layout :: proc ( layout : UI_Layout, preset : UI_StylePreset ) {
|
||||
stack_peek_ref( & get_state().ui_context.theme_stack ).array[preset].layout = layout
|
||||
}
|
||||
|
||||
ui_style_theme_push :: proc( preset : UI_StyleTheme ) {
|
||||
stack_push( & get_state().ui_context.theme_stack, preset )
|
||||
}
|
||||
|
||||
ui_style_theme_pop :: proc() {
|
||||
stack_pop( & get_state().ui_context.theme_stack )
|
||||
}
|
||||
|
||||
@(deferred_none = ui_style_theme_pop)
|
||||
ui_style_theme :: proc( preset : UI_StyleTheme ) {
|
||||
ui_style_theme_push( preset )
|
||||
}
|
||||
|
||||
ui_style_theme_set_layout :: proc ( layout : UI_Layout ) {
|
||||
for & preset in stack_peek_ref( & get_state().ui_context.theme_stack ).array {
|
||||
preset.layout = layout
|
||||
}
|
||||
}
|
||||
|
||||
ui_set_layout :: proc {
|
||||
ui_style_set_layout,
|
||||
ui_style_theme_set_layout,
|
||||
}
|
||||
|
Reference in New Issue
Block a user