SectrPrototype/code/ui_widgets.odin
Ed_ 595de438af WIP: Persistent order support for rooted boxes (top-most boxes)
I wasn't expecting it to be this to bad to support it...

Also:
* I renamed some of the files to group better with the virtual codebase view
2024-05-13 01:53:19 -04:00

410 lines
12 KiB
Odin

package sectr
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)
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)
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,
color_non_default : Color,
color_default : Color,
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,
handle_color_non_default : Color = Color_ResizeHandle,
handle_color_default : Color = Color_Transparent,
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.color_non_default = handle_color_non_default
resizable.color_default = handle_color_default
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,
color_non_default,
color_default,
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
// TODO(Ed): Add centered resize support (use center alignment on shift-click)
ui_resizable_handles :: proc( parent : ^UI_Widget, pos : ^Vec2, size : ^Vec2,
handle_width : f32 = 15,
handle_color_non_default : Color = Color_ResizeHandle,
handle_color_default : Color = Color_Transparent,
left := true,
right := true,
top := true,
bottom := true,
corner_tr := true,
corner_tl := true,
corner_br := true,
corner_bl := true,
compute_layout := true)
{
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)
flags := UI_BoxFlags { .Mouse_Clickable, .Focusable }
layout_bar_width := UI_Layout {
flags = { .Fixed_Width },
alignment = {1, 1},
margins = { handle_width, handle_width, 0, 0 },
size = range2({handle_width, 0}, {}),
}
style_bar := UI_Style {
bg_color = Color_ResizeHandle,
corner_radii = { 5, 0, 0, 0 }
}
theme_bar := to_ui_style_combo(style_bar)
theme_bar.default.bg_color = handle_color_default
theme_bar.default.corner_radii[0] = 0
ui_layout(layout_bar_width)
ui_style(theme_bar)
layout_bar_height := layout_bar_width
layout_bar_height.flags = {.Fixed_Height}
layout_bar_height.size.min = {0, handle_width}
layout_bar_height.margins = { 0, 0, handle_width, handle_width }
layout_bar_height.alignment = {0, 0}
context.user_ptr = & parent.label
name :: proc( ) -> StrRunesPair {
parent_label := (transmute(^string) context.user_ptr) ^
return str_intern(str_fmt_tmp("%v: %v", ))
}
if left do handle_left = ui_widget("Settings Menu: Resize Left Handle", flags )
if right {
handle_right = ui_widget("Settings Menu: Resize Right Handle", flags)
handle_right.layout.anchor.left = 1
handle_right.layout.alignment = { 0, 1 }
}
ui_layout(layout_bar_height)
ui_style_ref().default.bg_color = handle_color_default
if top do handle_top = ui_widget("Settings Menu: Resize Top Border", flags )
if bottom {
handle_bottom = ui_widget("Settings Menu: Resize Bottom Border", flags)
using handle_bottom.layout
anchor.top = 1
alignment = { 0, 1 }
}
layout_corner := UI_Layout {
flags = { .Fixed_Width, .Fixed_Height },
alignment = {1, 0},
size = range2({handle_width, handle_width}, {}),
}
style_corner := UI_Style {
bg_color = Color_ResizeHandle,
corner_radii = { 5, 0, 0, 0 },
}
ui_theme(layout_corner, style_corner)
ui_style_ref().default.bg_color = handle_color_default
if corner_tl do handle_corner_tl = ui_widget("Settings Menu: Corner TL", flags)
if corner_tr {
handle_corner_tr = ui_widget("Settings Menu: Corner TR", flags)
handle_corner_tr.layout.anchor = range2({1, 0}, {})
handle_corner_tr.layout.alignment = {0, 0}
}
if corner_bl {
handle_corner_bl = ui_widget("Settings Menu: Corner BL", flags)
handle_corner_bl.layout.anchor = range2({}, {0, 1})
handle_corner_bl.layout.alignment = { 1, 1 }
}
if corner_br {
handle_corner_br = ui_widget("Settings Menu: Corner BR", flags)
handle_corner_br.layout.anchor = range2({1, 0}, {0, 1})
handle_corner_br.layout.alignment = {0, 1}
}
process_handle_drag :: #force_inline proc ( handle : ^UI_Widget,
direction : Vec2,
size_delta : Vec2,
target_alignment : Vec2,
pos : ^Vec2,
size : ^Vec2,
alignment : ^Vec2, )
{
ui := get_state().ui_context
if ui.last_pressed_key != handle.key { return }
size_delta := size_delta
pos_adjust := size^ * (alignment^ - target_alignment)
@static was_dragging := false
using handle
if active
{
size^ += size_delta * direction
if pressed {
pos^ -= pos_adjust
}
else {
alignment^ = target_alignment
}
was_dragging = true
}
else if released && was_dragging
{
pos^ += pos_adjust
alignment^ = target_alignment
was_dragging = false
}
}
delta := get_state().input.mouse.delta
alignment := & parent.layout.alignment
if compute_layout do ui_box_compute_layout_children(parent)
if right do process_handle_drag( & handle_right, { 1, 0 }, delta, {0, 0}, pos, size, alignment )
if left do process_handle_drag( & handle_left, { -1, 0 }, delta, {1, 0}, pos, size, alignment )
if top do process_handle_drag( & handle_top, { 0, 1 }, delta, {0, 0}, pos, size, alignment )
if bottom do process_handle_drag( & handle_bottom, { 0, -1 }, delta, {0, 1}, pos, size, alignment )
if corner_tr do process_handle_drag( & handle_corner_tr, { 1, 1 }, delta, {0, 0}, pos, size, alignment )
if corner_tl do process_handle_drag( & handle_corner_tl, { -1, 1 }, delta, {1, 0}, pos, size, alignment )
if corner_br do process_handle_drag( & handle_corner_br, { 1, -1 }, delta, {0, 1}, pos, size, alignment )
if corner_bl do process_handle_drag( & handle_corner_bl, { -1, -1 }, delta, {1, 1}, pos, size, alignment )
}
#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
}
#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 }
}
#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 = {} ) -> (vbox : UI_VBox) {
// profile(#procedure)
vbox.direction = direction
vbox.box = ui_box_make( flags, label )
vbox.signal = ui_signal_from_box( vbox.box )
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)
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")