1815 lines
47 KiB
Odin
1815 lines
47 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 "core:fmt"
|
|
import "core:math"
|
|
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)
|
|
}
|
|
|
|
@(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}
|