/* ** 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) 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) */ } 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, 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), lines_stack: Stack(Line, LINE_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}, 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.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) /* 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) } } 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 cmd := push_command(ctx, Command_Line) cmd.first_segment = first_segment cmd.num_segments = num_segments cmd.color = line.color } 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: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 := 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}