|
@@ -0,0 +1,1694 @@
|
|
|
|
|
+/*
|
|
|
|
|
+** 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.
|
|
|
|
|
+*/
|
|
|
|
|
+
|
|
|
|
|
+package microui
|
|
|
|
|
+
|
|
|
|
|
+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)
|
|
|
|
|
+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)
|
|
|
|
|
+
|
|
|
|
|
+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
|
|
|
|
|
+Rect :: struct {
|
|
|
|
|
+ x, y, w, h: i32,
|
|
|
|
|
+}
|
|
|
|
|
+Color :: struct {
|
|
|
|
|
+ r, g, b, a: 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 :: 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) */
|
|
|
|
|
+}
|
|
|
|
|
+Command_Icon :: struct {
|
|
|
|
|
+ using command: Command,
|
|
|
|
|
+ rect: Rect,
|
|
|
|
|
+ id: Icon,
|
|
|
|
|
+ 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,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+Container :: struct {
|
|
|
|
|
+ head, tail: ^Command,
|
|
|
|
|
+ rect, body: Rect,
|
|
|
|
|
+ content_size: Vec2,
|
|
|
|
|
+ scroll: Vec2,
|
|
|
|
|
+ zindex: i32,
|
|
|
|
|
+ open: b32,
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+Style :: struct {
|
|
|
|
|
+ font: Font,
|
|
|
|
|
+ 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_width: proc(font: Font, str: string) -> i32,
|
|
|
|
|
+ text_height: proc(font: Font) -> i32,
|
|
|
|
|
+ draw_frame: proc(ctx: ^Context, rect: Rect, colorid: Color_Type),
|
|
|
|
|
+ /* core state */
|
|
|
|
|
+ _style: Style,
|
|
|
|
|
+ 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),
|
|
|
|
|
+ /* retained state pools */
|
|
|
|
|
+ container_pool: [CONTAINER_POOL_SIZE]Pool_Item,
|
|
|
|
|
+ containers: [CONTAINER_POOL_SIZE]Container,
|
|
|
|
|
+ treenode_pool: [TREENODE_POOL_SIZE]Pool_Item,
|
|
|
|
|
+ /* 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},
|
|
|
|
|
+ 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
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@(private)
|
|
|
|
|
+default_draw_frame :: proc(ctx: ^Context, rect: Rect, colorid: Color_Type) {
|
|
|
|
|
+ draw_rect(ctx, rect, ctx.style.colors[colorid])
|
|
|
|
|
+ if colorid == .SCROLL_BASE || colorid == .SCROLL_THUMB || colorid == .TITLE_BG {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ if ctx.style.colors[.BORDER].a != 0 { /* draw border */
|
|
|
|
|
+ draw_box(ctx, expand_rect(rect, 1), ctx.style.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._style = default_style
|
|
|
|
|
+ ctx.style = &ctx._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_width != nil, "ctx.text_width is not set")
|
|
|
|
|
+ assert(ctx.text_height != nil, "ctx.text_height is not set")
|
|
|
|
|
+ ctx.command_list.idx = 0
|
|
|
|
|
+ ctx.root_list.idx = 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)
|
|
|
|
|
+
|
|
|
|
|
+ /* 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, str: string, pos: Vec2, color: Color) {
|
|
|
|
|
+ rect := Rect{pos.x, pos.y, ctx.text_width(font, str), ctx.text_height(font)}
|
|
|
|
|
+ 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
|
|
|
|
|
+ /* 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)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/*============================================================================
|
|
|
|
|
+** 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 := ctx.style
|
|
|
|
|
+ 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 := ctx.style.font
|
|
|
|
|
+ tw := ctx.text_width(font, str)
|
|
|
|
|
+ push_clip_rect(ctx, rect)
|
|
|
|
|
+ pos.y = rect.y + (rect.h - ctx.text_height(font)) / 2
|
|
|
|
|
+ if .ALIGN_CENTER in opt {
|
|
|
|
|
+ pos.x = rect.x + (rect.w - tw) / 2
|
|
|
|
|
+ } else if .ALIGN_RIGHT in opt {
|
|
|
|
|
+ pos.x = rect.x + rect.w - tw - ctx.style.padding
|
|
|
|
|
+ } else {
|
|
|
|
|
+ pos.x = rect.x + ctx.style.padding
|
|
|
|
|
+ }
|
|
|
|
|
+ draw_text(ctx, font, str, pos, ctx.style.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
|
|
|
|
|
+ font := ctx.style.font
|
|
|
|
|
+ color := ctx.style.colors[.TEXT]
|
|
|
|
|
+ layout_begin_column(ctx)
|
|
|
|
|
+ layout_row(ctx, {-1}, ctx.text_height(font))
|
|
|
|
|
+ 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_width(font, word)
|
|
|
|
|
+ if w > r.w && start != 0 {
|
|
|
|
|
+ end = start
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ w += ctx.text_width(font, text[i:i + 1])
|
|
|
|
|
+ if ch == '\n' {
|
|
|
|
|
+ end = i + 1
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+ start = i + 1
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ draw_text(ctx, font, text[:end], Vec2{r.x, r.y}, color)
|
|
|
|
|
+ text = text[end:]
|
|
|
|
|
+ }
|
|
|
|
|
+ layout_end_column(ctx)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+label :: proc(ctx: ^Context, text: string) {
|
|
|
|
|
+ draw_control_text(ctx, text, layout_next(ctx), .TEXT)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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, ctx.style.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, ctx.style.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})
|
|
|
|
|
+
|
|
|
|
|
+ font := ctx.style.font
|
|
|
|
|
+
|
|
|
|
|
+ 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_width(font, string(textbuf[:i])) {
|
|
|
|
|
+ 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 := ctx.style.colors[.TEXT]
|
|
|
|
|
+ sel_color := ctx.style.colors[.SELECTION_BG]
|
|
|
|
|
+ textw := ctx.text_width(font, textstr)
|
|
|
|
|
+ texth := ctx.text_height(font)
|
|
|
|
|
+ headx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[0]])
|
|
|
|
|
+ tailx := ctx.text_width(font, textstr[:ctx.textbox_state.selection[1]])
|
|
|
|
|
+ ofmin := max(ctx.style.padding - headx, r.w - textw - ctx.style.padding)
|
|
|
|
|
+ ofmax := min(r.w - headx - ctx.style.padding, ctx.style.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, 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 := ctx.style.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)
|
|
|
|
|
+ }
|
|
|
|
|
+ draw_icon(
|
|
|
|
|
+ ctx,
|
|
|
|
|
+ expanded ? .EXPANDED : .COLLAPSED,
|
|
|
|
|
+ Rect{r.x, r.y, r.h, r.h},
|
|
|
|
|
+ ctx.style.colors[.TEXT],
|
|
|
|
|
+ )
|
|
|
|
|
+ r.x += r.h - ctx.style.padding
|
|
|
|
|
+ r.w -= r.h - ctx.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 += ctx.style.indent
|
|
|
|
|
+ push(&ctx.id_stack, ctx.last_id)
|
|
|
|
|
+ }
|
|
|
|
|
+ return res
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+end_treenode :: proc(ctx: ^Context) {
|
|
|
|
|
+ get_layout(ctx).indent -= ctx.style.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] = ctx.style.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(ctx.style.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 := ctx.style.scrollbar_size
|
|
|
|
|
+ cs := cnt.content_size
|
|
|
|
|
+ cs.x += ctx.style.padding * 2
|
|
|
|
|
+ cs.y += ctx.style.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, -ctx.style.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 = ctx.style.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, ctx.style.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 := ctx.style.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, ctx.style.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}
|