gutter_runner/game/ui/microui.odin

2206 lines
57 KiB
Odin

/*
** Original work: Copyright (c) 2020 rxi
** Modified work: Copyright (c) 2020 oskarnp
** Modified work: Copyright (c) 2021 gingerBill
**
** Permission is hereby granted, free of charge, to any person obtaining a copy
** of this software and associated documentation files (the "Software"), to
** deal in the Software without restriction, including without limitation the
** rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
** sell copies of the Software, and to permit persons to whom the Software is
** furnished to do so, subject to the following conditions:
**
** The above copyright notice and this permission notice shall be included in
** all copies or substantial portions of the Software.
**
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
** FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
** IN THE SOFTWARE.
*/
// Extended version of microui to support more drawing operations
package ui
import "base:builtin"
import "base:intrinsics"
import "base:runtime"
import "core:fmt"
import "core:math"
import "core:mem"
import "core:reflect"
import "core:sort"
import "core:strconv"
import "core:strings"
import textedit "core:text/edit"
COMMAND_LIST_SIZE :: #config(MICROUI_COMMAND_LIST_SIZE, 256 * 1024)
ROOT_LIST_SIZE :: #config(MICROUI_ROOT_LIST_SIZE, 32)
CONTAINER_STACK_SIZE :: #config(MICROUI_CONTAINER_STACK_SIZE, 32)
CLIP_STACK_SIZE :: #config(MICROUI_CLIP_STACK_SIZE, 32)
ID_STACK_SIZE :: #config(MICROUI_ID_STACK_SIZE, 32)
LAYOUT_STACK_SIZE :: #config(MICROUI_LAYOUT_STACK_SIZE, 16)
LINE_STACK_SIZE :: #config(MICROUI_LINE_STACK_SIZE, 16)
STYLE_STACK_SIZE :: #config(MICROUI_STYLE_STACK_SIZE, 16)
CONTAINER_POOL_SIZE :: #config(MICROUI_CONTAINER_POOL_SIZE, 48)
TREENODE_POOL_SIZE :: #config(MICROUI_TREENODE_POOL_SIZE, 48)
MAX_WIDTHS :: #config(MICROUI_MAX_WIDTHS, 16)
SLIDER_FMT :: #config(MICROUI_SLIDER_FMT, "%.2f")
MAX_FMT :: #config(MICROUI_MAX_FMT, 127)
MAX_TEXT_STORE :: #config(MICROUI_MAX_TEXT_STORE, 1024)
MAX_LINE_SEGMENTS :: #config(MICROUI_LINE_SEGMENTS_POOL_SIZE, 4096)
Clip :: enum u32 {
NONE,
PART,
ALL,
}
Color_Type :: enum u32 {
TEXT,
SELECTION_BG,
BORDER,
WINDOW_BG,
TITLE_BG,
TITLE_TEXT,
PANEL_BG,
BUTTON,
BUTTON_HOVER = BUTTON + 1,
BUTTON_FOCUS = BUTTON + 2,
BASE,
BASE_HOVER = BASE + 1,
BASE_FOCUS = BASE + 2,
SCROLL_BASE,
SCROLL_THUMB,
}
Icon :: enum u32 {
NONE,
CLOSE,
CHECK,
COLLAPSED,
EXPANDED,
RESIZE,
}
Result :: enum u32 {
ACTIVE,
SUBMIT,
CHANGE,
}
Result_Set :: bit_set[Result;u32]
Opt :: enum u32 {
ALIGN_CENTER,
ALIGN_RIGHT,
NO_INTERACT,
NO_FRAME,
NO_RESIZE,
NO_SCROLL,
NO_CLOSE,
NO_TITLE,
HOLD_FOCUS,
AUTO_SIZE,
POPUP,
CLOSED,
EXPANDED,
}
Options :: distinct bit_set[Opt;u32]
Mouse :: enum u32 {
LEFT,
RIGHT,
MIDDLE,
}
Mouse_Set :: distinct bit_set[Mouse;u32]
Key :: enum u32 {
SHIFT,
CTRL,
ALT,
BACKSPACE,
DELETE,
RETURN,
LEFT,
RIGHT,
HOME,
END,
A,
X,
C,
V,
}
Key_Set :: distinct bit_set[Key;u32]
Id :: distinct u32
Real :: f32
Font :: distinct rawptr
Vec2 :: distinct [2]i32
Vec2f :: [2]f32
Rect :: struct {
x, y, w, h: i32,
}
Color :: [4]u8
Frame_Index :: distinct i32
Pool_Item :: struct {
id: Id,
last_update: Frame_Index,
}
Command_Variant :: union {
^Command_Jump,
^Command_Clip,
^Command_Rect,
^Command_Text,
^Command_Icon,
^Command_Line,
}
Command :: struct {
variant: Command_Variant,
size: i32,
}
Command_Jump :: struct {
using command: Command,
dst: rawptr,
}
Command_Clip :: struct {
using command: Command,
rect: Rect,
}
Command_Rect :: struct {
using command: Command,
rect: Rect,
color: Color,
}
Command_Text :: struct {
using command: Command,
font: Font,
pos: Vec2,
color: Color,
str: string, /* + string data (VLA) */
font_size: i32,
}
Command_Icon :: struct {
using command: Command,
rect: Rect,
id: Icon,
color: Color,
}
Command_Line :: struct {
using command: Command,
first_segment: i32,
num_segments: i32,
color: Color,
}
Layout_Type :: enum {
NONE = 0,
RELATIVE = 1,
ABSOLUTE = 2,
}
Layout :: struct {
body, next: Rect,
position, size, max: Vec2,
widths: [MAX_WIDTHS]i32,
items, item_index, next_row: i32,
next_type: Layout_Type,
indent: i32,
}
Line :: struct {
first_segment: i32,
color: Color,
rect: Rect,
}
Container :: struct {
head, tail: ^Command,
rect, body: Rect,
content_size: Vec2,
scroll: Vec2,
zindex: i32,
open: b32,
}
Style :: struct {
font: Font,
font_size: i32,
size: Vec2,
padding: i32,
spacing: i32,
indent: i32,
title_height: i32,
footer_height: i32,
scrollbar_size: i32,
thumb_size: i32,
colors: [Color_Type]Color,
}
Context :: struct {
/* callbacks */
text_size: proc(font: Font, font_size: i32, str: string) -> [2]i32,
draw_frame: proc(ctx: ^Context, rect: Rect, colorid: Color_Type),
/* core state */
default_style: Style,
hover_id, focus_id, last_id: Id,
last_rect: Rect,
last_zindex: i32,
updated_focus: b32,
frame: Frame_Index,
hover_root, next_hover_root: ^Container,
scroll_target: ^Container,
number_edit_buf: [MAX_FMT]u8,
number_edit_len: int,
number_edit_id: Id,
/* stacks */
command_list: Stack(u8, COMMAND_LIST_SIZE),
root_list: Stack(^Container, ROOT_LIST_SIZE),
container_stack: Stack(^Container, CONTAINER_STACK_SIZE),
clip_stack: Stack(Rect, CLIP_STACK_SIZE),
id_stack: Stack(Id, ID_STACK_SIZE),
layout_stack: Stack(Layout, LAYOUT_STACK_SIZE),
lines_stack: Stack(Line, LINE_STACK_SIZE),
style_stack: Stack(Style, STYLE_STACK_SIZE),
/* retained state pools */
container_pool: [CONTAINER_POOL_SIZE]Pool_Item,
containers: [CONTAINER_POOL_SIZE]Container,
treenode_pool: [TREENODE_POOL_SIZE]Pool_Item,
/* Arbitrary drawing pools, cleared every frame */
line_segments_pool: [MAX_LINE_SEGMENTS]Vec2f,
line_segments_num: i32,
/* input state */
mouse_pos, last_mouse_pos: Vec2,
mouse_delta, scroll_delta: Vec2,
mouse_down_bits: Mouse_Set,
mouse_pressed_bits: Mouse_Set,
mouse_released_bits: Mouse_Set,
key_down_bits, key_pressed_bits: Key_Set,
_text_store: [MAX_TEXT_STORE]u8,
text_input: strings.Builder, // uses `_text_store` as backing store with nil_allocator.
textbox_state: textedit.State,
textbox_offset: i32,
}
Stack :: struct($T: typeid, $N: int) {
idx: i32,
items: [N]T,
}
push :: #force_inline proc(stk: ^$T/Stack($V, $N), val: V) {
assert(stk.idx < len(stk.items))
stk.items[stk.idx] = val
stk.idx += 1
}
pop :: #force_inline proc(stk: ^$T/Stack($V, $N)) {
assert(stk.idx > 0)
stk.idx -= 1
}
unclipped_rect := Rect{0, 0, 0x1000000, 0x1000000}
default_style := Style {
font = nil,
size = {68, 10},
font_size = 16,
padding = 5,
spacing = 4,
indent = 24,
title_height = 24,
footer_height = 20,
scrollbar_size = 12,
thumb_size = 8,
colors = {
.TEXT = {230, 230, 230, 255},
.SELECTION_BG = {90, 90, 90, 255},
.BORDER = {25, 25, 25, 255},
.WINDOW_BG = {50, 50, 50, 255},
.TITLE_BG = {25, 25, 25, 255},
.TITLE_TEXT = {240, 240, 240, 255},
.PANEL_BG = {0, 0, 0, 0},
.BUTTON = {75, 75, 75, 255},
.BUTTON_HOVER = {95, 95, 95, 255},
.BUTTON_FOCUS = {115, 115, 115, 255},
.BASE = {30, 30, 30, 255},
.BASE_HOVER = {35, 35, 35, 255},
.BASE_FOCUS = {40, 40, 40, 255},
.SCROLL_BASE = {43, 43, 43, 255},
.SCROLL_THUMB = {30, 30, 30, 255},
},
}
expand_rect :: proc(rect: Rect, n: i32) -> Rect {
return Rect{rect.x - n, rect.y - n, rect.w + n * 2, rect.h + n * 2}
}
intersect_rects :: proc(r1, r2: Rect) -> Rect {
x1 := max(r1.x, r2.x)
y1 := max(r1.y, r2.y)
x2 := min(r1.x + r1.w, r2.x + r2.w)
y2 := min(r1.y + r1.h, r2.y + r2.h)
if x2 < x1 {x2 = x1}
if y2 < y1 {y2 = y1}
return Rect{x1, y1, x2 - x1, y2 - y1}
}
rect_overlaps_vec2 :: proc(r: Rect, p: Vec2) -> bool {
return p.x >= r.x && p.x < r.x + r.w && p.y >= r.y && p.y < r.y + r.h
}
rect_from_point_extent :: proc(p, e: Vec2) -> Rect {
return Rect{x = p.x - e.x, y = p.y - e.y, w = e.x * 2, h = e.y * 2}
}
get_style :: proc(ctx: ^Context) -> ^Style {
if ctx.style_stack.idx > 0 {
return &ctx.style_stack.items[ctx.style_stack.idx - 1]
}
return &ctx.default_style
}
push_style :: proc(ctx: ^Context, style: Style) {
push(&ctx.style_stack, style)
}
pop_style :: proc(ctx: ^Context) {
pop(&ctx.style_stack)
}
push_font_size_style :: proc(ctx: ^Context, font_size: i32) {
style := get_style(ctx)^
style.font_size = font_size
push_style(ctx, style)
}
@(private)
default_draw_frame :: proc(ctx: ^Context, rect: Rect, colorid: Color_Type) {
draw_rect(ctx, rect, get_style(ctx).colors[colorid])
if colorid == .SCROLL_BASE || colorid == .SCROLL_THUMB || colorid == .TITLE_BG {
return
}
if get_style(ctx).colors[.BORDER].a != 0 { /* draw border */
draw_box(ctx, expand_rect(rect, 1), get_style(ctx).colors[.BORDER])
}
}
init :: proc(
ctx: ^Context,
set_clipboard: proc(user_data: rawptr, text: string) -> (ok: bool) = nil,
get_clipboard: proc(user_data: rawptr) -> (text: string, ok: bool) = nil,
clipboard_user_data: rawptr = nil,
) {
ctx^ = {} // zero memory
ctx.draw_frame = default_draw_frame
ctx.default_style = default_style
ctx.text_input = strings.builder_from_bytes(ctx._text_store[:])
ctx.textbox_state.set_clipboard = set_clipboard
ctx.textbox_state.get_clipboard = get_clipboard
ctx.textbox_state.clipboard_user_data = clipboard_user_data
}
begin :: proc(ctx: ^Context) {
assert(ctx.text_size != nil, "ctx.text_size is not set")
ctx.command_list.idx = 0
ctx.root_list.idx = 0
ctx.line_segments_num = 0
ctx.scroll_target = nil
ctx.hover_root = ctx.next_hover_root
ctx.next_hover_root = nil
ctx.mouse_delta.x = ctx.mouse_pos.x - ctx.last_mouse_pos.x
ctx.mouse_delta.y = ctx.mouse_pos.y - ctx.last_mouse_pos.y
ctx.frame += 1
}
end :: proc(ctx: ^Context) {
/* check stacks */
assert(ctx.container_stack.idx == 0)
assert(ctx.clip_stack.idx == 0)
assert(ctx.id_stack.idx == 0)
assert(ctx.layout_stack.idx == 0)
assert(ctx.lines_stack.idx == 0)
assert(ctx.style_stack.idx == 0)
/* handle scroll input */
if ctx.scroll_target != nil {
ctx.scroll_target.scroll.x += ctx.scroll_delta.x
ctx.scroll_target.scroll.y += ctx.scroll_delta.y
}
/* unset focus if focus id was not touched this frame */
if !ctx.updated_focus {
ctx.focus_id = 0
}
ctx.updated_focus = false
/* bring hover root to front if mouse was pressed */
if mouse_pressed(ctx) &&
ctx.next_hover_root != nil &&
ctx.next_hover_root.zindex < ctx.last_zindex &&
ctx.next_hover_root.zindex >= 0 {
bring_to_front(ctx, ctx.next_hover_root)
}
/* reset input state */
ctx.key_pressed_bits = {} // clear
strings.builder_reset(&ctx.text_input)
ctx.mouse_pressed_bits = {} // clear
ctx.mouse_released_bits = {} // clear
ctx.scroll_delta = Vec2{0, 0}
ctx.last_mouse_pos = ctx.mouse_pos
/* sort root containers by zindex */
n := ctx.root_list.idx
sort.quick_sort_proc(ctx.root_list.items[:n], proc(a, b: ^Container) -> int {
return int(a.zindex) - int(b.zindex)
})
/* set root container jump commands */
for i: i32 = 0; i < n; i += 1 {
cnt := ctx.root_list.items[i]
/* if this is the first container then make the first command jump to it.
** otherwise set the previous container's tail to jump to this one */
if i == 0 {
cmd := (^Command_Jump)(&ctx.command_list.items[0])
cmd.dst = rawptr(uintptr(cnt.head) + size_of(Command_Jump))
} else {
prev := ctx.root_list.items[i - 1]
prev.tail.variant.(^Command_Jump).dst = rawptr(
uintptr(cnt.head) + size_of(Command_Jump),
)
}
/* make the last container's tail jump to the end of command list */
if i == n - 1 {
cnt.tail.variant.(^Command_Jump).dst = rawptr(
&ctx.command_list.items[ctx.command_list.idx],
)
}
}
}
set_focus :: proc(ctx: ^Context, id: Id) {
ctx.focus_id = id
ctx.updated_focus = true
}
get_id :: proc {
get_id_string,
get_id_bytes,
get_id_rawptr,
get_id_uintptr,
}
get_id_string :: #force_inline proc(ctx: ^Context, str: string) -> Id {return get_id_bytes(
ctx,
transmute([]byte)str,
)}
get_id_rawptr :: #force_inline proc(
ctx: ^Context,
data: rawptr,
size: int,
) -> Id {return get_id_bytes(ctx, ([^]u8)(data)[:size])}
get_id_uintptr :: #force_inline proc(ctx: ^Context, ptr: uintptr) -> Id {
ptr := ptr
return get_id_bytes(ctx, ([^]u8)(&ptr)[:size_of(ptr)])
}
get_id_bytes :: proc(ctx: ^Context, bytes: []byte) -> Id {
/* 32bit fnv-1a hash */
HASH_INITIAL :: 2166136261
hash :: proc(hash: ^Id, data: []byte) {
size := len(data)
cptr := ([^]u8)(raw_data(data))
for ; size > 0; size -= 1 {
hash^ = Id(u32(hash^) ~ u32(cptr[0])) * 16777619
cptr = cptr[1:]
}
}
idx := ctx.id_stack.idx
res := ctx.id_stack.items[idx - 1] if idx > 0 else HASH_INITIAL
hash(&res, bytes)
ctx.last_id = res
return res
}
push_id :: proc {
push_id_string,
push_id_bytes,
push_id_rawptr,
push_id_uintptr,
}
push_id_string :: #force_inline proc(ctx: ^Context, str: string) {push(
&ctx.id_stack,
get_id(ctx, str),
)}
push_id_rawptr :: #force_inline proc(ctx: ^Context, data: rawptr, size: int) {push(
&ctx.id_stack,
get_id(ctx, data, size),
)}
push_id_uintptr :: #force_inline proc(ctx: ^Context, ptr: uintptr) {push(
&ctx.id_stack,
get_id(ctx, ptr),
)}
push_id_bytes :: #force_inline proc(ctx: ^Context, bytes: []byte) {push(
&ctx.id_stack,
get_id(ctx, bytes),
)}
pop_id :: proc(ctx: ^Context) {
pop(&ctx.id_stack)
}
push_clip_rect :: proc(ctx: ^Context, rect: Rect) {
last := get_clip_rect(ctx)
push(&ctx.clip_stack, intersect_rects(rect, last))
}
pop_clip_rect :: proc(ctx: ^Context) {
pop(&ctx.clip_stack)
}
get_clip_rect :: proc(ctx: ^Context) -> Rect {
assert(ctx.clip_stack.idx > 0)
return ctx.clip_stack.items[ctx.clip_stack.idx - 1]
}
check_clip :: proc(ctx: ^Context, r: Rect) -> Clip {
cr := get_clip_rect(ctx)
if r.x > cr.x + cr.w || r.x + r.w < cr.x || r.y > cr.y + cr.h || r.y + r.h < cr.y {
return .ALL
}
if r.x >= cr.x && r.x + r.w <= cr.x + cr.w && r.y >= cr.y && r.y + r.h <= cr.y + cr.h {
return .NONE
}
return .PART
}
get_layout :: proc(ctx: ^Context) -> ^Layout {
return &ctx.layout_stack.items[ctx.layout_stack.idx - 1]
}
@(private)
push_layout :: proc(ctx: ^Context, body: Rect, scroll: Vec2) {
layout: Layout
layout.body = Rect{body.x - scroll.x, body.y - scroll.y, body.w, body.h}
layout.max = Vec2{-0x1000000, -0x1000000}
push(&ctx.layout_stack, layout)
layout_row(ctx, {0})
}
@(private)
pop_container :: proc(ctx: ^Context) {
cnt := get_current_container(ctx)
layout := get_layout(ctx)
cnt.content_size.x = layout.max.x - layout.body.x
cnt.content_size.y = layout.max.y - layout.body.y
/* pop container, layout and id */
pop(&ctx.container_stack)
pop(&ctx.layout_stack)
pop_id(ctx)
}
get_current_container :: proc(ctx: ^Context) -> ^Container {
assert(ctx.container_stack.idx > 0)
return ctx.container_stack.items[ctx.container_stack.idx - 1]
}
@(private)
internal_get_container :: proc(ctx: ^Context, id: Id, opt: Options) -> ^Container {
/* try to get existing container from pool */
idx, ok := pool_get(ctx, ctx.container_pool[:], id)
if ok {
if ctx.containers[idx].open || .CLOSED not_in opt {
pool_update(ctx, &ctx.container_pool[idx])
}
return &ctx.containers[idx]
}
if .CLOSED in opt {return nil}
/* container not found in pool: init new container */
idx = pool_init(ctx, ctx.container_pool[:], id)
cnt := &ctx.containers[idx]
cnt^ = {} // clear memory
cnt.open = true
bring_to_front(ctx, cnt)
return cnt
}
get_container :: proc(ctx: ^Context, name: string, opt := Options{}) -> ^Container {
id := get_id(ctx, name)
return internal_get_container(ctx, id, opt)
}
bring_to_front :: proc(ctx: ^Context, cnt: ^Container) {
ctx.last_zindex += 1
cnt.zindex = ctx.last_zindex
}
/*============================================================================
** pool
**============================================================================*/
pool_init :: proc(ctx: ^Context, items: []Pool_Item, id: Id) -> int {
f := ctx.frame
n := -1
for _, i in items {
if items[i].last_update < f {
f = items[i].last_update
n = i
}
}
assert(n > -1)
items[n].id = id
pool_update(ctx, &items[n])
return n
}
pool_get :: proc(ctx: ^Context, items: []Pool_Item, id: Id) -> (int, bool) {
for _, i in items {
if items[i].id == id {
return i, true
}
}
return -1, false
}
pool_update :: proc(ctx: ^Context, item: ^Pool_Item) {
item.last_update = ctx.frame
}
/*============================================================================
** input handlers
**============================================================================*/
input_mouse_move :: proc(ctx: ^Context, x, y: i32) {
ctx.mouse_pos = Vec2{x, y}
}
input_mouse_down :: proc(ctx: ^Context, x, y: i32, btn: Mouse) {
input_mouse_move(ctx, x, y)
ctx.mouse_down_bits += {btn}
ctx.mouse_pressed_bits += {btn}
}
input_mouse_up :: proc(ctx: ^Context, x, y: i32, btn: Mouse) {
input_mouse_move(ctx, x, y)
ctx.mouse_down_bits -= {btn}
ctx.mouse_released_bits += {btn}
}
input_scroll :: proc(ctx: ^Context, x, y: i32) {
ctx.scroll_delta.x += x
ctx.scroll_delta.y += y
}
input_key_down :: proc(ctx: ^Context, key: Key) {
ctx.key_pressed_bits += {key}
ctx.key_down_bits += {key}
}
input_key_up :: proc(ctx: ^Context, key: Key) {
ctx.key_down_bits -= {key}
}
input_text :: proc(ctx: ^Context, text: string) {
strings.write_string(&ctx.text_input, text)
}
/*============================================================================
** commandlist
**============================================================================*/
push_command :: proc(ctx: ^Context, $Type: typeid, extra_size := 0) -> ^Type {
size := i32(size_of(Type) + extra_size)
cmd := transmute(^Type)&ctx.command_list.items[ctx.command_list.idx]
assert(ctx.command_list.idx + size < COMMAND_LIST_SIZE)
ctx.command_list.idx += size
cmd.variant = cmd
cmd.size = size
return cmd
}
next_command :: proc "contextless" (ctx: ^Context, pcmd: ^^Command) -> bool {
cmd := pcmd^
defer pcmd^ = cmd
if cmd != nil {
cmd = (^Command)(uintptr(cmd) + uintptr(cmd.size))
} else {
cmd = (^Command)(&ctx.command_list.items[0])
}
invalid_command :: #force_inline proc "contextless" (ctx: ^Context) -> ^Command {
return (^Command)(&ctx.command_list.items[ctx.command_list.idx])
}
for cmd != invalid_command(ctx) {
if jmp, ok := cmd.variant.(^Command_Jump); ok {
cmd = (^Command)(jmp.dst)
continue
}
return true
}
return false
}
next_command_iterator :: proc "contextless" (
ctx: ^Context,
pcm: ^^Command,
) -> (
Command_Variant,
bool,
) {
if next_command(ctx, pcm) {
return pcm^.variant, true
}
return nil, false
}
@(private)
push_jump :: proc(ctx: ^Context, dst: ^Command) -> ^Command {
cmd := push_command(ctx, Command_Jump)
cmd.dst = dst
return cmd
}
set_clip :: proc(ctx: ^Context, rect: Rect) {
cmd := push_command(ctx, Command_Clip)
cmd.rect = rect
}
draw_rect :: proc(ctx: ^Context, rect: Rect, color: Color) {
rect := rect
rect = intersect_rects(rect, get_clip_rect(ctx))
if rect.w > 0 && rect.h > 0 {
cmd := push_command(ctx, Command_Rect)
cmd.rect = rect
cmd.color = color
}
}
draw_box :: proc(ctx: ^Context, rect: Rect, color: Color) {
draw_rect(ctx, Rect{rect.x + 1, rect.y, rect.w - 2, 1}, color)
draw_rect(ctx, Rect{rect.x + 1, rect.y + rect.h - 1, rect.w - 2, 1}, color)
draw_rect(ctx, Rect{rect.x, rect.y, 1, rect.h}, color)
draw_rect(ctx, Rect{rect.x + rect.w - 1, rect.y, 1, rect.h}, color)
}
draw_text :: proc(
ctx: ^Context,
font: Font,
font_size: i32,
str: string,
pos: Vec2,
color: Color,
) {
text_size := ctx.text_size(font, font_size, str)
rect := Rect{pos.x, pos.y, text_size.x, text_size.y}
clipped := check_clip(ctx, rect)
switch clipped {
case .NONE: // okay
case .ALL:
return
case .PART:
set_clip(ctx, get_clip_rect(ctx))
}
/* add command */
text_cmd := push_command(ctx, Command_Text, len(str))
text_cmd.pos = pos
text_cmd.color = color
text_cmd.font = font
text_cmd.font_size = font_size
/* copy string */
dst_str := ([^]byte)(text_cmd)[size_of(Command_Text):][:len(str)]
copy(dst_str, str)
text_cmd.str = string(dst_str)
/* reset clipping if it was set */
if clipped != .NONE {
set_clip(ctx, unclipped_rect)
}
}
draw_icon :: proc(ctx: ^Context, id: Icon, rect: Rect, color: Color) {
/* do clip command if the rect isn't fully contained within the cliprect */
clipped := check_clip(ctx, rect)
switch clipped {
case .NONE: // okay
case .ALL:
return
case .PART:
set_clip(ctx, get_clip_rect(ctx))
}
/* do icon command */
cmd := push_command(ctx, Command_Icon)
cmd.id = id
cmd.rect = rect
cmd.color = color
/* reset clipping if it was set */
if clipped != .NONE {
set_clip(ctx, unclipped_rect)
}
}
get_line :: proc(ctx: ^Context) -> ^Line {
return &ctx.lines_stack.items[ctx.lines_stack.idx - 1]
}
begin_line :: proc(ctx: ^Context, color: Color) {
line: Line
line.first_segment = ctx.line_segments_num
line.color = color
line.rect = layout_next(ctx)
push(&ctx.lines_stack, line)
}
end_line :: proc(ctx: ^Context) {
line: Line = get_line(ctx)^
pop(&ctx.lines_stack)
first_segment := line.first_segment
num_segments := ctx.line_segments_num - first_segment
clipped := check_clip(ctx, line.rect)
switch clipped {
case .NONE:
case .ALL:
return
case .PART:
set_clip(ctx, get_clip_rect(ctx))
}
cmd := push_command(ctx, Command_Line)
cmd.first_segment = first_segment
cmd.num_segments = num_segments
cmd.color = line.color
if clipped != .NONE {
set_clip(ctx, unclipped_rect)
}
}
push_line_point :: proc(ctx: ^Context, p: Vec2f) {
line := get_line(ctx)
origin := Vec2f{f32(line.rect.x), f32(line.rect.y)}
size := Vec2f{f32(line.rect.w), f32(line.rect.h)}
ctx.line_segments_pool[ctx.line_segments_num] = p * size + origin
ctx.line_segments_num += 1
}
get_line_segments :: proc(ctx: ^Context, first_segment: i32, num_segments: i32) -> []Vec2f {
return ctx.line_segments_pool[first_segment:first_segment + num_segments]
}
/*============================================================================
** layout
**============================================================================*/
layout_begin_column :: proc(ctx: ^Context) {
push_layout(ctx, layout_next(ctx), Vec2{0, 0})
}
layout_end_column :: proc(ctx: ^Context) {
b := get_layout(ctx)
pop(&ctx.layout_stack)
/* inherit position/next_row/max from child layout if they are greater */
a := get_layout(ctx)
a.position.x = max(a.position.x, b.position.x + b.body.x - a.body.x)
a.next_row = max(a.next_row, b.next_row + b.body.y - a.body.y)
a.max.x = max(a.max.x, b.max.x)
a.max.y = max(a.max.y, b.max.y)
}
@(deferred_in = layout_end_column)
layout_column :: proc(ctx: ^Context) -> bool {
layout_begin_column(ctx)
return true
}
layout_row :: proc(ctx: ^Context, widths: []i32, height: i32 = 0) {
layout := get_layout(ctx)
items := len(widths)
if len(widths) > 0 {
items = copy(layout.widths[:], widths[:])
}
layout.items = i32(items)
layout.position = Vec2{layout.indent, layout.next_row}
layout.size.y = height
layout.item_index = 0
}
layout_row_items :: proc(ctx: ^Context, items: i32, height: i32 = 0) {
layout := get_layout(ctx)
layout.items = items
layout.position = Vec2{layout.indent, layout.next_row}
layout.size.y = height
layout.item_index = 0
}
layout_width :: proc(ctx: ^Context, width: i32) {
get_layout(ctx).size.x = width
}
layout_height :: proc(ctx: ^Context, height: i32) {
get_layout(ctx).size.y = height
}
layout_set_next :: proc(ctx: ^Context, r: Rect, relative: bool) {
layout := get_layout(ctx)
layout.next = r
layout.next_type = .RELATIVE if relative else .ABSOLUTE
}
layout_next :: proc(ctx: ^Context) -> (res: Rect) {
layout := get_layout(ctx)
style := get_style(ctx)
defer ctx.last_rect = res
if layout.next_type != .NONE {
/* handle rect set by `layout_set_next` */
type := layout.next_type
layout.next_type = .NONE
res = layout.next
if type == .ABSOLUTE {
return
}
} else {
/* handle next row */
if layout.item_index == layout.items {
layout_row_items(ctx, layout.items, layout.size.y)
}
/* position */
res.x = layout.position.x
res.y = layout.position.y
/* size */
res.w = layout.items > 0 ? layout.widths[layout.item_index] : layout.size.x
res.h = layout.size.y
if res.w == 0 {res.w = style.size.x + style.padding * 2}
if res.h == 0 {res.h = style.size.y + style.padding * 2}
if res.w < 0 {res.w += layout.body.w - res.x + 1}
if res.h < 0 {res.h += layout.body.h - res.y + 1}
layout.item_index += 1
}
/* update position */
layout.position.x += res.w + style.spacing
layout.next_row = max(layout.next_row, res.y + res.h + style.spacing)
/* apply body offset */
res.x += layout.body.x
res.y += layout.body.y
/* update max position */
layout.max.x = max(layout.max.x, res.x + res.w)
layout.max.y = max(layout.max.y, res.y + res.h)
return
}
/*============================================================================
** controls
**============================================================================*/
@(private)
in_hover_root :: proc(ctx: ^Context) -> bool {
for i := ctx.container_stack.idx - 1; i >= 0; i -= 1 {
if ctx.container_stack.items[i] == ctx.hover_root {
return true
}
/* only root containers have their `head` field set; stop searching if we've
** reached the current root container */
if ctx.container_stack.items[i].head != nil {
break
}
}
return false
}
draw_control_frame :: proc(
ctx: ^Context,
id: Id,
rect: Rect,
colorid: Color_Type,
opt := Options{},
) {
if .NO_FRAME in opt {
return
}
assert(colorid == .BUTTON || colorid == .BASE)
colorid := colorid
colorid = Color_Type(
int(colorid) + int((ctx.focus_id == id) ? 2 : (ctx.hover_id == id) ? 1 : 0),
)
ctx.draw_frame(ctx, rect, colorid)
}
draw_control_text :: proc(
ctx: ^Context,
str: string,
rect: Rect,
colorid: Color_Type,
opt := Options{},
) {
pos: Vec2
font := get_style(ctx).font
font_size := get_style(ctx).font_size
ts := ctx.text_size(font, font_size, str)
push_clip_rect(ctx, rect)
pos.y = rect.y + (rect.h - ts.y) / 2
if .ALIGN_CENTER in opt {
pos.x = rect.x + (rect.w - ts.x) / 2
} else if .ALIGN_RIGHT in opt {
pos.x = rect.x + rect.w - ts.x - get_style(ctx).padding
} else {
pos.x = rect.x + get_style(ctx).padding
}
draw_text(ctx, font, font_size, str, pos, get_style(ctx).colors[colorid])
pop_clip_rect(ctx)
}
mouse_over :: proc(ctx: ^Context, rect: Rect) -> bool {
return(
rect_overlaps_vec2(rect, ctx.mouse_pos) &&
rect_overlaps_vec2(get_clip_rect(ctx), ctx.mouse_pos) &&
in_hover_root(ctx) \
)
}
update_control :: proc(ctx: ^Context, id: Id, rect: Rect, opt := Options{}) {
mouseover := mouse_over(ctx, rect)
if ctx.focus_id == id {
ctx.updated_focus = true
}
if .NO_INTERACT in opt {
return
}
if mouseover && !mouse_down(ctx) {
ctx.hover_id = id
}
if ctx.focus_id == id {
if mouse_pressed(ctx) && !mouseover {
set_focus(ctx, 0)
}
if !mouse_down(ctx) && .HOLD_FOCUS not_in opt {
set_focus(ctx, 0)
}
}
if ctx.hover_id == id {
if mouse_pressed(ctx) {
set_focus(ctx, id)
} else if !mouseover {
ctx.hover_id = 0
}
}
}
text :: proc(ctx: ^Context, text: string) {
text := text
style := get_style(ctx)
font := style.font
font_size := style.font_size
color := style.colors[.TEXT]
layout_begin_column(ctx)
layout_row(ctx, {-1}, ctx.text_size(font, font_size, text).y)
for len(text) > 0 {
w: i32
start: int
end: int = len(text)
r := layout_next(ctx)
for ch, i in text {
if ch == ' ' || ch == '\n' {
word := text[start:i]
w += ctx.text_size(font, font_size, word).x
if w > r.w && start != 0 {
end = start
break
}
w += ctx.text_size(font, font_size, text[i:i + 1]).x
if ch == '\n' {
end = i + 1
break
}
start = i + 1
}
}
draw_text(ctx, font, font_size, text[:end], Vec2{r.x, r.y}, color)
text = text[end:]
}
layout_end_column(ctx)
}
label :: proc(ctx: ^Context, text: string, opt := Options{}) {
draw_control_text(ctx, text, layout_next(ctx), .TEXT, opt)
}
button :: proc(
ctx: ^Context,
label: string,
icon: Icon = .NONE,
opt: Options = {.ALIGN_CENTER},
) -> (
res: Result_Set,
) {
id := len(label) > 0 ? get_id(ctx, label) : get_id(ctx, uintptr(icon))
r := layout_next(ctx)
update_control(ctx, id, r, opt)
/* handle click */
if ctx.mouse_pressed_bits == {.LEFT} && ctx.focus_id == id {
res += {.SUBMIT}
}
/* draw */
draw_control_frame(ctx, id, r, .BUTTON, opt)
if len(label) > 0 {
draw_control_text(ctx, label, r, .TEXT, opt)
}
if icon != .NONE {
draw_icon(ctx, icon, r, get_style(ctx).colors[.TEXT])
}
return
}
checkbox :: proc(ctx: ^Context, label: string, state: ^bool) -> (res: Result_Set) {
id := get_id(ctx, uintptr(state))
r := layout_next(ctx)
box := Rect{r.x, r.y, r.h, r.h}
update_control(ctx, id, r, {})
/* handle click */
if .LEFT in ctx.mouse_released_bits && ctx.hover_id == id {
res += {.CHANGE}
state^ = !state^
}
/* draw */
draw_control_frame(ctx, id, box, .BASE, {})
if state^ {
draw_icon(ctx, .CHECK, box, get_style(ctx).colors[.TEXT])
}
r = Rect{r.x + box.w, r.y, r.w - box.w, r.h}
draw_control_text(ctx, label, r, .TEXT)
return
}
textbox_raw :: proc(
ctx: ^Context,
textbuf: []u8,
textlen: ^int,
id: Id,
r: Rect,
opt := Options{},
) -> (
res: Result_Set,
) {
update_control(ctx, id, r, opt | {.HOLD_FOCUS})
style := get_style(ctx)
font := style.font
font_size := style.font_size
if ctx.focus_id == id {
/* create a builder backed by the user's buffer */
builder := strings.builder_from_bytes(textbuf)
non_zero_resize(&builder.buf, textlen^)
ctx.textbox_state.builder = &builder
if ctx.textbox_state.id != u64(id) {
ctx.textbox_state.id = u64(id)
ctx.textbox_state.selection = {}
}
/* check selection bounds */
if ctx.textbox_state.selection[0] > textlen^ || ctx.textbox_state.selection[1] > textlen^ {
ctx.textbox_state.selection = {}
}
/* handle text input */
if strings.builder_len(ctx.text_input) > 0 {
if textedit.input_text(&ctx.textbox_state, strings.to_string(ctx.text_input)) > 0 {
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
}
/* handle ctrl+a */
if .A in ctx.key_pressed_bits &&
.CTRL in ctx.key_down_bits &&
.ALT not_in ctx.key_down_bits {
ctx.textbox_state.selection = {textlen^, 0}
}
/* handle ctrl+x */
if .X in ctx.key_pressed_bits &&
.CTRL in ctx.key_down_bits &&
.ALT not_in ctx.key_down_bits {
if textedit.cut(&ctx.textbox_state) {
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
}
/* handle ctrl+c */
if .C in ctx.key_pressed_bits &&
.CTRL in ctx.key_down_bits &&
.ALT not_in ctx.key_down_bits {
textedit.copy(&ctx.textbox_state)
}
/* handle ctrl+v */
if .V in ctx.key_pressed_bits &&
.CTRL in ctx.key_down_bits &&
.ALT not_in ctx.key_down_bits {
if textedit.paste(&ctx.textbox_state) {
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
}
/* handle left/right */
if .LEFT in ctx.key_pressed_bits {
move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left
if .SHIFT in ctx.key_down_bits {
textedit.select_to(&ctx.textbox_state, move)
} else {
textedit.move_to(&ctx.textbox_state, move)
}
}
if .RIGHT in ctx.key_pressed_bits {
move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right
if .SHIFT in ctx.key_down_bits {
textedit.select_to(&ctx.textbox_state, move)
} else {
textedit.move_to(&ctx.textbox_state, move)
}
}
/* handle home/end */
if .HOME in ctx.key_pressed_bits {
if .SHIFT in ctx.key_down_bits {
textedit.select_to(&ctx.textbox_state, .Start)
} else {
textedit.move_to(&ctx.textbox_state, .Start)
}
}
if .END in ctx.key_pressed_bits {
if .SHIFT in ctx.key_down_bits {
textedit.select_to(&ctx.textbox_state, .End)
} else {
textedit.move_to(&ctx.textbox_state, .End)
}
}
/* handle backspace/delete */
if .BACKSPACE in ctx.key_pressed_bits && textlen^ > 0 {
move: textedit.Translation = .Word_Left if .CTRL in ctx.key_down_bits else .Left
textedit.delete_to(&ctx.textbox_state, move)
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
if .DELETE in ctx.key_pressed_bits && textlen^ > 0 {
move: textedit.Translation = .Word_Right if .CTRL in ctx.key_down_bits else .Right
textedit.delete_to(&ctx.textbox_state, move)
textlen^ = strings.builder_len(builder)
res += {.CHANGE}
}
/* handle return */
if .RETURN in ctx.key_pressed_bits {
set_focus(ctx, 0)
res += {.SUBMIT}
}
/* handle click/drag */
if .LEFT in ctx.mouse_down_bits {
idx := textlen^
for i in 0 ..< textlen^ {
/* skip continuation bytes */
if textbuf[i] >= 0x80 && textbuf[i] < 0xc0 {
continue
}
if ctx.mouse_pos.x <
r.x +
ctx.textbox_offset +
ctx.text_size(font, font_size, string(textbuf[:i])).x {
idx = i
break
}
}
ctx.textbox_state.selection[0] = idx
if .LEFT in ctx.mouse_pressed_bits && .SHIFT not_in ctx.key_down_bits {
ctx.textbox_state.selection[1] = idx
}
}
}
textstr := string(textbuf[:textlen^])
/* draw */
draw_control_frame(ctx, id, r, .BASE, opt)
if ctx.focus_id == id {
text_color := style.colors[.TEXT]
sel_color := style.colors[.SELECTION_BG]
text_size := ctx.text_size(font, font_size, textstr)
textw := text_size.x
texth := text_size.y
headx := ctx.text_size(font, font_size, textstr[:ctx.textbox_state.selection[0]]).x
tailx := ctx.text_size(font, font_size, textstr[:ctx.textbox_state.selection[1]]).x
ofmin := max(get_style(ctx).padding - headx, r.w - textw - get_style(ctx).padding)
ofmax := min(r.w - headx - get_style(ctx).padding, get_style(ctx).padding)
ctx.textbox_offset = clamp(ctx.textbox_offset, ofmin, ofmax)
textx := r.x + ctx.textbox_offset
texty := r.y + (r.h - texth) / 2
push_clip_rect(ctx, r)
draw_rect(
ctx,
Rect{textx + min(headx, tailx), texty, abs(headx - tailx), texth},
sel_color,
)
draw_text(ctx, font, font_size, textstr, Vec2{textx, texty}, text_color)
draw_rect(ctx, Rect{textx + headx, texty, 1, texth}, text_color)
pop_clip_rect(ctx)
} else {
draw_control_text(ctx, textstr, r, .TEXT, opt)
}
return
}
@(private)
parse_real :: #force_inline proc(s: string) -> (Real, bool) {
f, ok := strconv.parse_f64(s)
return Real(f), ok
}
number_textbox :: proc(ctx: ^Context, value: ^Real, r: Rect, id: Id, fmt_string: string) -> bool {
if ctx.mouse_pressed_bits == {.LEFT} && .SHIFT in ctx.key_down_bits && ctx.hover_id == id {
ctx.number_edit_id = id
nstr := fmt.bprintf(ctx.number_edit_buf[:], fmt_string, value^)
ctx.number_edit_len = len(nstr)
}
if ctx.number_edit_id == id {
res := textbox_raw(ctx, ctx.number_edit_buf[:], &ctx.number_edit_len, id, r, {})
if .SUBMIT in res || ctx.focus_id != id {
value^, _ = parse_real(string(ctx.number_edit_buf[:ctx.number_edit_len]))
ctx.number_edit_id = 0
} else {
return true
}
}
return false
}
textbox :: proc(ctx: ^Context, buf: []u8, textlen: ^int, opt := Options{}) -> Result_Set {
id := get_id(ctx, uintptr(&buf[0]))
r := layout_next(ctx)
return textbox_raw(ctx, buf, textlen, id, r, opt)
}
slider :: proc(
ctx: ^Context,
value: ^Real,
low, high: Real,
step: Real = 0.0,
fmt_string: string = SLIDER_FMT,
opt: Options = {.ALIGN_CENTER},
) -> (
res: Result_Set,
) {
last := value^
v := last
id := get_id(ctx, uintptr(value))
base := layout_next(ctx)
/* handle text input mode */
if number_textbox(ctx, &v, base, id, fmt_string) {
return
}
/* handle normal mode */
update_control(ctx, id, base, opt)
/* handle input */
if ctx.focus_id == id && ctx.mouse_down_bits == {.LEFT} {
v = low + Real(ctx.mouse_pos.x - base.x) * (high - low) / Real(base.w)
if step != 0.0 {
v = math.floor((v + step / 2) / step) * step
}
}
/* clamp and store value, update res */
v = clamp(v, low, high);value^ = v
if last != v {
res += {.CHANGE}
}
/* draw base */
draw_control_frame(ctx, id, base, .BASE, opt)
/* draw thumb */
w := get_style(ctx).thumb_size
x := i32((v - low) * Real(base.w - w) / (high - low))
thumb := Rect{base.x + x, base.y, w, base.h}
draw_control_frame(ctx, id, thumb, .BUTTON, opt)
/* draw text */
text_buf: [4096]byte
draw_control_text(ctx, fmt.bprintf(text_buf[:], fmt_string, v), base, .TEXT, opt)
return
}
number :: proc(
ctx: ^Context,
value: ^Real,
step: Real,
fmt_string: string = SLIDER_FMT,
opt: Options = {.ALIGN_CENTER},
) -> (
res: Result_Set,
) {
id := get_id(ctx, uintptr(value))
base := layout_next(ctx)
last := value^
/* handle text input mode */
if number_textbox(ctx, value, base, id, fmt_string) {
return
}
/* handle normal mode */
update_control(ctx, id, base, opt)
/* handle input */
if ctx.focus_id == id && ctx.mouse_down_bits == {.LEFT} {
value^ += Real(ctx.mouse_delta.x) * step
}
/* set flag if value changed */
if value^ != last {
res += {.CHANGE}
}
/* draw base */
draw_control_frame(ctx, id, base, .BASE, opt)
/* draw text */
text_buf: [4096]byte
draw_control_text(ctx, fmt.bprintf(text_buf[:], fmt_string, value^), base, .TEXT, opt)
return
}
@(private)
_header :: proc(ctx: ^Context, label: string, is_treenode: bool, opt := Options{}) -> Result_Set {
id := get_id(ctx, label)
idx, active := pool_get(ctx, ctx.treenode_pool[:], id)
expanded := .EXPANDED in opt ? !active : active
layout_row(ctx, {-1})
r := layout_next(ctx)
update_control(ctx, id, r, {})
/* handle click */
if ctx.mouse_pressed_bits == {.LEFT} && ctx.focus_id == id {
active = !active
}
/* update pool ref */
if idx >= 0 {
if active {
pool_update(ctx, &ctx.treenode_pool[idx])
} else {
ctx.treenode_pool[idx] = {}
}
} else if active {
pool_init(ctx, ctx.treenode_pool[:], id)
}
/* draw */
if is_treenode {
if ctx.hover_id == id {
ctx.draw_frame(ctx, r, .BUTTON_HOVER)
}
} else {
draw_control_frame(ctx, id, r, .BUTTON)
}
style := get_style(ctx)
draw_icon(
ctx,
expanded ? .EXPANDED : .COLLAPSED,
Rect{r.x, r.y, r.h, r.h},
style.colors[.TEXT],
)
r.x += r.h - style.padding
r.w -= r.h - style.padding
draw_control_text(ctx, label, r, .TEXT)
return expanded ? {.ACTIVE} : {}
}
header :: proc(ctx: ^Context, label: string, opt := Options{}) -> Result_Set {
return _header(ctx, label, false, opt)
}
begin_treenode :: proc(ctx: ^Context, label: string, opt := Options{}) -> Result_Set {
res := _header(ctx, label, true, opt)
if .ACTIVE in res {
get_layout(ctx).indent += get_style(ctx).indent
push(&ctx.id_stack, ctx.last_id)
}
return res
}
end_treenode :: proc(ctx: ^Context) {
get_layout(ctx).indent -= get_style(ctx).indent
pop_id(ctx)
}
scoped_end_treenode :: proc(ctx: ^Context, _: string, _: Options, result_set: Result_Set) {
if result_set != nil {
end_treenode(ctx)
}
}
/* This is scoped and is intended to be use in the condition of a if-statement */
@(deferred_in_out = scoped_end_treenode)
treenode :: proc(ctx: ^Context, label: string, opt := Options{}) -> Result_Set {
return begin_treenode(ctx, label, opt)
}
@(private)
scrollbar :: proc(ctx: ^Context, cnt: ^Container, _b: ^Rect, cs: Vec2, id_string: string, i: int) {
b := (^struct {
pos, size: [2]i32,
})(_b)
#assert(size_of(b^) == size_of(_b^))
/* only add scrollbar if content size is larger than body */
maxscroll := cs[i] - b.size[i]
contentsize := b.size[i]
if maxscroll > 0 && contentsize > 0 {
id := get_id(ctx, id_string)
/* get sizing / positioning */
base := b^
base.pos[1 - i] = b.pos[1 - i] + b.size[1 - i]
base.size[1 - i] = get_style(ctx).scrollbar_size
/* handle input */
update_control(ctx, id, transmute(Rect)base)
if ctx.focus_id == id && .LEFT in ctx.mouse_down_bits {
cnt.scroll[i] += ctx.mouse_delta[i] * cs[i] / base.size[i]
}
/* clamp scroll to limits */
cnt.scroll[i] = clamp(cnt.scroll[i], 0, maxscroll)
/* draw base and thumb */
ctx.draw_frame(ctx, transmute(Rect)base, .SCROLL_BASE)
thumb := base
thumb.size[i] = max(get_style(ctx).thumb_size, base.size[i] * b.size[i] / cs[i])
thumb.pos[i] += cnt.scroll[i] * (base.size[i] - thumb.size[i]) / maxscroll
ctx.draw_frame(ctx, transmute(Rect)thumb, .SCROLL_THUMB)
/* set this as the scroll_target (will get scrolled on mousewheel) */
/* if the mouse is over it */
if mouse_over(ctx, transmute(Rect)b^) {
ctx.scroll_target = cnt
}
} else {
cnt.scroll[i] = 0
}
}
@(private)
scrollbars :: proc(ctx: ^Context, cnt: ^Container, body: ^Rect) {
sz := get_style(ctx).scrollbar_size
cs := cnt.content_size
cs.x += get_style(ctx).padding * 2
cs.y += get_style(ctx).padding * 2
push_clip_rect(ctx, body^)
/* resize body to make room for scrollbars */
if cs.y > cnt.body.h {body.w -= sz}
if cs.x > cnt.body.w {body.h -= sz}
/* to create a horizontal or vertical scrollbar almost-identical code is
** used; only the references to `x|y` `w|h` need to be switched */
scrollbar(ctx, cnt, body, cs, "!scrollbarv", 1) // 1 = y,h
scrollbar(ctx, cnt, body, cs, "!scrollbarh", 0) // 0 = x,w
pop_clip_rect(ctx)
}
@(private)
push_container_body :: proc(ctx: ^Context, cnt: ^Container, body: Rect, opt := Options{}) {
body := body
if .NO_SCROLL not_in opt {
scrollbars(ctx, cnt, &body)
}
push_layout(ctx, expand_rect(body, -get_style(ctx).padding), cnt.scroll)
cnt.body = body
}
@(private)
begin_root_container :: proc(ctx: ^Context, cnt: ^Container) {
push(&ctx.container_stack, cnt)
/* push container to roots list and push head command */
push(&ctx.root_list, cnt)
cnt.head = push_jump(ctx, nil)
/* set as hover root if the mouse is overlapping this container and it has a
** higher zindex than the current hover root */
if rect_overlaps_vec2(cnt.rect, ctx.mouse_pos) &&
(ctx.next_hover_root == nil || cnt.zindex > ctx.next_hover_root.zindex) {
ctx.next_hover_root = cnt
}
/* clipping is reset here in case a root-container is made within
** another root-containers's begin/end block; this prevents the inner
** root-container being clipped to the outer */
push(&ctx.clip_stack, unclipped_rect)
}
@(private)
end_root_container :: proc(ctx: ^Context) {
/* push tail 'goto' jump command and set head 'skip' command. the final steps
** on initing these are done in end() */
cnt := get_current_container(ctx)
cnt.tail = push_jump(ctx, nil)
cnt.head.variant.(^Command_Jump).dst = &ctx.command_list.items[ctx.command_list.idx]
/* pop base clip rect and container */
pop_clip_rect(ctx)
pop_container(ctx)
}
begin_window :: proc(ctx: ^Context, title: string, rect: Rect, opt := Options{}) -> bool {
assert(title != "", "missing window title")
id := get_id(ctx, title)
cnt := internal_get_container(ctx, id, opt)
if cnt == nil || !cnt.open {
return false
}
push(&ctx.id_stack, id)
rect := rect
if cnt.rect.w == 0 {
cnt.rect = rect
}
begin_root_container(ctx, cnt)
rect = cnt.rect
body := cnt.rect
/* draw frame */
if .NO_FRAME not_in opt {
ctx.draw_frame(ctx, rect, .WINDOW_BG)
}
/* do title bar */
if .NO_TITLE not_in opt {
tr := rect
tr.h = get_style(ctx).title_height
ctx.draw_frame(ctx, tr, .TITLE_BG)
/* do title text */
if .NO_TITLE not_in opt {
tid := get_id(ctx, "!title")
update_control(ctx, tid, tr, opt)
draw_control_text(ctx, title, tr, .TITLE_TEXT, opt)
if tid == ctx.focus_id && ctx.mouse_down_bits == {.LEFT} {
cnt.rect.x += ctx.mouse_delta.x
cnt.rect.y += ctx.mouse_delta.y
}
body.y += tr.h
body.h -= tr.h
}
/* do `close` button */
if .NO_CLOSE not_in opt {
cid := get_id(ctx, "!close")
r := Rect{tr.x + tr.w - tr.h, tr.y, tr.h, tr.h}
tr.w -= r.w
draw_icon(ctx, .CLOSE, r, get_style(ctx).colors[.TITLE_TEXT])
update_control(ctx, cid, r, opt)
if .LEFT in ctx.mouse_released_bits && cid == ctx.hover_id {
cnt.open = false
}
}
}
/* do `resize` handle */
if .NO_RESIZE not_in opt {
sz := get_style(ctx).footer_height
rid := get_id(ctx, "!resize")
r := Rect{rect.x + rect.w - sz, rect.y + rect.h - sz, sz, sz}
draw_icon(ctx, .RESIZE, r, get_style(ctx).colors[.TEXT])
update_control(ctx, rid, r, opt)
if rid == ctx.focus_id && .LEFT in ctx.mouse_down_bits {
cnt.rect.w = max(96, cnt.rect.w + ctx.mouse_delta.x)
cnt.rect.h = max(64, cnt.rect.h + ctx.mouse_delta.y)
}
body.h -= sz
}
push_container_body(ctx, cnt, body, opt)
/* resize to content size */
if .AUTO_SIZE in opt {
r := get_layout(ctx).body
cnt.rect.w = cnt.content_size.x + (cnt.rect.w - r.w)
cnt.rect.h = cnt.content_size.y + (cnt.rect.h - r.h)
}
/* close if this is a popup window and elsewhere was clicked */
if .POPUP in opt && mouse_pressed(ctx) && ctx.hover_root != cnt {
cnt.open = false
}
push_clip_rect(ctx, cnt.body)
return true
}
end_window :: proc(ctx: ^Context) {
pop_clip_rect(ctx)
end_root_container(ctx)
}
/* This is scoped and is intended to be use in the condition of a if-statement */
@(deferred_in_out = scoped_end_window)
window :: proc(ctx: ^Context, title: string, rect: Rect, opt := Options{}) -> bool {
return begin_window(ctx, title, rect, opt)
}
scoped_end_window :: proc(ctx: ^Context, _: string, _: Rect, _: Options, ok: bool) {
if ok {
end_window(ctx)
}
}
open_popup :: proc(ctx: ^Context, name: string) {
cnt := get_container(ctx, name)
/* set as hover root so popup isn't closed in begin_window() */
ctx.hover_root = cnt
ctx.next_hover_root = cnt
/* position at mouse cursor, open and bring-to-front */
cnt.rect = Rect{ctx.mouse_pos.x, ctx.mouse_pos.y, 1, 1}
cnt.open = true
bring_to_front(ctx, cnt)
}
begin_popup :: proc(ctx: ^Context, name: string) -> bool {
opt := Options{.POPUP, .AUTO_SIZE, .NO_RESIZE, .NO_SCROLL, .NO_TITLE, .CLOSED}
return begin_window(ctx, name, Rect{}, opt)
}
end_popup :: proc(ctx: ^Context) {
end_window(ctx)
}
/* This is scoped and is intended to be use in the condition of a if-statement */
@(deferred_in_out = scoped_end_popup)
popup :: proc(ctx: ^Context, name: string) -> bool {
return begin_popup(ctx, name)
}
scoped_end_popup :: proc(ctx: ^Context, _: string, ok: bool) {
if ok {
end_popup(ctx)
}
}
begin_panel :: proc(ctx: ^Context, name: string, opt := Options{}) {
assert(name != "", "missing panel name")
push_id(ctx, name)
cnt := internal_get_container(ctx, ctx.last_id, opt)
cnt.rect = layout_next(ctx)
if .NO_FRAME not_in opt {
ctx.draw_frame(ctx, cnt.rect, .PANEL_BG)
}
push(&ctx.container_stack, cnt)
push_container_body(ctx, cnt, cnt.rect, opt)
push_clip_rect(ctx, cnt.body)
}
end_panel :: proc(ctx: ^Context) {
pop_clip_rect(ctx)
pop_container(ctx)
}
keyval :: proc(ctx: ^Context, key: string, val: any, key_width := i32(100)) {
layout_row(ctx, {key_width, -1}, 0)
label(ctx, key)
label(ctx, fmt.tprintf("%v", val))
}
// inspect_array(ctx, ptr, n, info.elem_size, info.elem)
inspect_array :: proc(
ctx: ^Context,
name: string,
ptr: rawptr,
n: int,
elem_size: int,
type_info: ^runtime.Type_Info,
) {
if ptr == nil && n > 0 {
keyval(ctx, name, "nil")
return
}
if .ACTIVE in treenode(ctx, fmt.tprintf("%s len(%v)", name, n)) {
for i in 0 ..< n {
data := uintptr(ptr) + uintptr(elem_size * i)
inspect_value(ctx, fmt.tprintf("[%v]", i), any{rawptr(data), type_info.id})
}
}
}
inspect_struct :: proc(
ctx: ^Context,
name: string,
v: any,
info: runtime.Type_Info_Struct,
type_name: string,
) -> bool {
if .ACTIVE in treenode(ctx, name) {
if .raw_union in info.flags {
if type_name == "" {
keyval(ctx, name, "(raw union)")
} else {
keyval(ctx, name, fmt.tprintf("%v{}", type_name))
}
return true
}
is_soa := info.soa_kind != .None
// is_empty := info.field_count == 0
if is_soa {
base_type_name: string
if v, ok := info.soa_base_type.variant.(runtime.Type_Info_Named); ok {
base_type_name = v.name
}
actual_field_count := info.field_count
n := uintptr(info.soa_len)
if info.soa_kind == .Slice {
actual_field_count = info.field_count - 1 // len
n = uintptr((^int)(uintptr(v.data) + info.offsets[actual_field_count])^)
} else if info.soa_kind == .Dynamic {
actual_field_count = info.field_count - 3 // len, cap, allocator
n = uintptr((^int)(uintptr(v.data) + info.offsets[actual_field_count])^)
}
for index in 0 ..< n {
field_count := -1
for i in 0 ..< actual_field_count {
field_name := info.names[i]
field_count += 1
if info.soa_kind == .Fixed {
t := info.types[i].variant.(runtime.Type_Info_Array).elem
t_size := uintptr(t.size)
if reflect.is_any(t) {
keyval(ctx, field_name, "any{}")
} else {
data := rawptr(uintptr(v.data) + info.offsets[i] + index * t_size)
inspect_value(ctx, field_name, any{data, t.id})
}
} else {
t := info.types[i].variant.(runtime.Type_Info_Multi_Pointer).elem
t_size := uintptr(t.size)
if reflect.is_any(t) {
keyval(ctx, field_name, "any{}")
} else {
field_ptr := (^^byte)(uintptr(v.data) + info.offsets[i])^
data := rawptr(uintptr(field_ptr) + index * t_size)
inspect_value(ctx, field_name, any{data, t.id})
}
}
}
}
} else {
field_count := -1
for field_name, i in info.names[:info.field_count] {
field_count += 1
if t := info.types[i]; reflect.is_any(t) {
keyval(ctx, field_name, "any{}")
} else {
data := rawptr(uintptr(v.data) + info.offsets[i])
inspect_value(ctx, field_name, any{data, t.id})
}
}
}
return true
}
return false
}
is_type_numeric :: proc(type_info: ^runtime.Type_Info) -> bool {
#partial switch info in type_info.variant {
case runtime.Type_Info_Float, runtime.Type_Info_Integer:
return true
}
return false
}
inspect_value :: proc(ctx: ^Context, name: string, v: any, type_name: string = "") -> bool {
if v.data == nil || v.id == nil {
label(ctx, "<nil>")
return true
}
type_info := type_info_of(v.id)
switch info in type_info.variant {
case runtime.Type_Info_Any: // Ignore
case runtime.Type_Info_Parameters: // Ignore
case runtime.Type_Info_Named:
return inspect_value(ctx, name, any{v.data, info.base.id}, info.name)
case runtime.Type_Info_Boolean,
runtime.Type_Info_Integer,
runtime.Type_Info_Rune,
runtime.Type_Info_Float,
runtime.Type_Info_Complex,
runtime.Type_Info_Quaternion,
runtime.Type_Info_Simd_Vector,
runtime.Type_Info_Type_Id,
runtime.Type_Info_Bit_Set,
runtime.Type_Info_Matrix,
runtime.Type_Info_Bit_Field,
runtime.Type_Info_Procedure,
runtime.Type_Info_String,
runtime.Type_Info_Enum:
if len(type_name) != 0 {
keyval(ctx, name, fmt.tprintf("%s(%v)", type_name, v))
} else {
keyval(ctx, name, fmt.tprintf("%v", v))
}
case runtime.Type_Info_Pointer:
// if v.id == typeid_of(^runtime.Type_Info) {
// reflect.write_type(fi.writer, (^^runtime.Type_Info)(v.data)^, &fi.n)
// } else {
// ptr := (^rawptr)(v.data)^
// if verb != 'p' && info.elem != nil {
// a := any{ptr, info.elem.id}
// elem := runtime.type_info_base(info.elem)
// if elem != nil {
// #partial switch e in elem.variant {
// case runtime.Type_Info_Array,
// runtime.Type_Info_Slice,
// runtime.Type_Info_Dynamic_Array,
// runtime.Type_Info_Map:
// if ptr == nil {
// io.write_string(fi.writer, "<nil>", &fi.n)
// return
// }
// if fi.indirection_level < 1 {
// fi.indirection_level += 1
// defer fi.indirection_level -= 1
// io.write_byte(fi.writer, '&')
// fmt_value(fi, a, verb)
// return
// }
// case runtime.Type_Info_Struct,
// runtime.Type_Info_Union,
// runtime.Type_Info_Bit_Field:
// if ptr == nil {
// io.write_string(fi.writer, "<nil>", &fi.n)
// return
// }
// if fi.indirection_level < 1 {
// fi.indirection_level += 1
// defer fi.indirection_level -= 1
// io.write_byte(fi.writer, '&', &fi.n)
// fmt_value(fi, a, verb)
// return
// }
// }
// }
// }
// fmt_pointer(fi, ptr, verb)
// }
case runtime.Type_Info_Soa_Pointer:
// ptr := (^runtime.Raw_Soa_Pointer)(v.data)^
// fmt_soa_pointer(fi, ptr, verb)
case runtime.Type_Info_Multi_Pointer:
// ptr := (^rawptr)(v.data)^
// if ptr == nil {
// io.write_string(fi.writer, "<nil>", &fi.n)
// return
// }
// if verb != 'p' && info.elem != nil {
// a := any{ptr, info.elem.id}
// elem := runtime.type_info_base(info.elem)
// if elem != nil {
// if n, ok := fi.optional_len.?; ok {
// fi.optional_len = nil
// fmt_array(fi, ptr, n, elem.size, elem, verb)
// return
// } else if fi.use_nul_termination {
// fi.use_nul_termination = false
// fmt_array_nul_terminated(fi, ptr, -1, elem.size, elem, verb)
// return
// }
// #partial switch e in elem.variant {
// case runtime.Type_Info_Integer:
// switch verb {
// case 's', 'q':
// switch elem.id {
// case u8:
// fmt_cstring(fi, cstring(ptr), verb)
// return
// case u16, u32, rune:
// n := search_nul_termination(ptr, elem.size, -1)
// fmt_array(fi, ptr, n, elem.size, elem, verb)
// return
// }
// }
// case runtime.Type_Info_Array,
// runtime.Type_Info_Slice,
// runtime.Type_Info_Dynamic_Array,
// runtime.Type_Info_Map:
// if fi.indirection_level < 1 {
// fi.indirection_level += 1
// defer fi.indirection_level -= 1
// io.write_byte(fi.writer, '&', &fi.n)
// fmt_value(fi, a, verb)
// return
// }
// case runtime.Type_Info_Struct, runtime.Type_Info_Union:
// if fi.indirection_level < 1 {
// fi.indirection_level += 1
// defer fi.indirection_level -= 1
// io.write_byte(fi.writer, '&', &fi.n)
// fmt_value(fi, a, verb)
// return
// }
// }
// }
// }
// fmt_pointer(fi, ptr, verb)
case runtime.Type_Info_Enumerated_Array:
// fi.record_level += 1
// defer fi.record_level -= 1
// if fi.hash {
// io.write_byte(fi.writer, '[' if verb != 'w' else '{', &fi.n)
// io.write_byte(fi.writer, '\n', &fi.n)
// defer {
// fmt_write_indent(fi)
// io.write_byte(fi.writer, ']' if verb != 'w' else '}', &fi.n)
// }
// indent := fi.indent
// fi.indent += 1
// defer fi.indent = indent
// for i in 0 ..< info.count {
// fmt_write_indent(fi)
// idx, ok := stored_enum_value_to_string(info.index, info.min_value, i)
// if ok {
// io.write_byte(fi.writer, '.', &fi.n)
// io.write_string(fi.writer, idx, &fi.n)
// } else {
// io.write_i64(fi.writer, i64(info.min_value) + i64(i), 10, &fi.n)
// }
// io.write_string(fi.writer, " = ", &fi.n)
// data := uintptr(v.data) + uintptr(i * info.elem_size)
// fmt_arg(fi, any{rawptr(data), info.elem.id}, verb)
// io.write_string(fi.writer, ",\n", &fi.n)
// }
// } else {
// io.write_byte(fi.writer, '[' if verb != 'w' else '{', &fi.n)
// defer io.write_byte(fi.writer, ']' if verb != 'w' else '}', &fi.n)
// for i in 0 ..< info.count {
// if i > 0 {io.write_string(fi.writer, ", ", &fi.n)}
// idx, ok := stored_enum_value_to_string(info.index, info.min_value, i)
// if ok {
// io.write_byte(fi.writer, '.', &fi.n)
// io.write_string(fi.writer, idx, &fi.n)
// } else {
// io.write_i64(fi.writer, i64(info.min_value) + i64(i), 10, &fi.n)
// }
// io.write_string(fi.writer, " = ", &fi.n)
// data := uintptr(v.data) + uintptr(i * info.elem_size)
// fmt_arg(fi, any{rawptr(data), info.elem.id}, verb)
// }
// }
case runtime.Type_Info_Array:
n := info.count
ptr := v.data
if info.count <= 4 && is_type_numeric(info.elem) {
keyval(ctx, name, v)
} else {
inspect_array(ctx, name, ptr, n, info.elem_size, info.elem)
}
case runtime.Type_Info_Slice:
slice := cast(^mem.Raw_Slice)v.data
n := slice.len
ptr := slice.data
inspect_array(ctx, name, ptr, n, info.elem_size, info.elem)
case runtime.Type_Info_Dynamic_Array:
array := cast(^mem.Raw_Dynamic_Array)v.data
n := array.len
ptr := array.data
inspect_array(ctx, name, ptr, n, info.elem_size, info.elem)
case runtime.Type_Info_Map:
if .ACTIVE in treenode(ctx, name) {
m := (^mem.Raw_Map)(v.data)
if m != nil {
if info.map_info == nil {
return false
}
map_cap := uintptr(runtime.map_cap(m^))
ks, vs, hs, _, _ := runtime.map_kvh_data_dynamic(m^, info.map_info)
j := 0
for bucket_index in 0 ..< map_cap {
runtime.map_hash_is_valid(hs[bucket_index]) or_continue
j += 1
key := runtime.map_cell_index_dynamic(ks, info.map_info.ks, bucket_index)
value := runtime.map_cell_index_dynamic(vs, info.map_info.vs, bucket_index)
inspect_value(
ctx,
fmt.tprintf("%v", any{rawptr(key), info.key.id}),
any{rawptr(value), info.value.id},
)
}
}
} else {
return false
}
case runtime.Type_Info_Struct:
return inspect_struct(ctx, name, v, info, "")
case runtime.Type_Info_Union:
// fmt_union(fi, v, verb, info, type_info.size)
}
return true
}
@(private)
mouse_released :: #force_inline proc(ctx: ^Context) -> bool {return ctx.mouse_released_bits != nil}
@(private)
mouse_pressed :: #force_inline proc(ctx: ^Context) -> bool {return ctx.mouse_pressed_bits != nil}
@(private)
mouse_down :: #force_inline proc(ctx: ^Context) -> bool {return ctx.mouse_down_bits != nil}