diff options
author | San Jacobs | 2023-10-15 14:59:33 +0200 |
---|---|---|
committer | San Jacobs | 2023-10-15 14:59:33 +0200 |
commit | 690d1102dedbbad955c34ba1a5ef9f4d15d82158 (patch) | |
tree | 887d6bea6a843952c7d8346a99131601f603e19f /lib/oui | |
parent | b6dab385dbf9a626970f3673d345d0e8e6a62e1e (diff) | |
parent | 2e3a7e10756954dc5a99d617a1c0eef327d3adbb (diff) | |
download | satscalc-690d1102dedbbad955c34ba1a5ef9f4d15d82158.tar.gz satscalc-690d1102dedbbad955c34ba1a5ef9f4d15d82158.tar.bz2 satscalc-690d1102dedbbad955c34ba1a5ef9f4d15d82158.zip |
Merge branch 'oui' into odin
Diffstat (limited to 'lib/oui')
-rw-r--r-- | lib/oui/oui.odin | 1010 |
1 files changed, 1010 insertions, 0 deletions
diff --git a/lib/oui/oui.odin b/lib/oui/oui.odin new file mode 100644 index 0000000..a1fbbf8 --- /dev/null +++ b/lib/oui/oui.odin @@ -0,0 +1,1010 @@ +package oui + +import "core:mem" +import "core:fmt" +import "core:time" +import "core:hash" +import "core:slice" +import "../rect" + +// LAYOUTING IS CUSTOMIZED +// 1. RectCut Layouting (directionality + cut distance) +// 2. Absolute Layouting (set the rect yourself) +// 3. Relative Layouting (set the rect relative to a parent) +// 4. Custom Layouting (callback - roll your own thing) // TODO + +MAX_DATASIZE :: 4096 +MAX_DEPTH :: 64 +MAX_INPUT_EVENTS :: 64 +CLICK_THRESHOLD :: time.Millisecond * 250 +MAX_CONSUME :: 256 + +RectI :: rect.RectI +I2 :: [2]int + +Input_Event :: struct { + key: string, + char: rune, + call: Call, +} + +Mouse_Button :: enum { + Left, + Middle, + Right, +} +Mouse_Buttons :: bit_set[Mouse_Button] + +Context :: struct { + // userdata + user_ptr: rawptr, + + // mouse state + buttons: Mouse_Buttons, + last_buttons: Mouse_Buttons, + button_capture: Mouse_Button, + button_ignore: Maybe(Mouse_Button), + + // cursor + cursor_start: I2, + cursor_last_frame: I2, + cursor_delta_frame: I2, + cursor: I2, + cursor_handle: int, // handle <-> looks + cursor_handle_callback: proc(new: int), // callback on handle change + scroll: I2, + + // item ids + active_item: u32, + focus_item: u32, + focus_redirect_item: u32, // redirect messages if no item is focused to this item + last_hot_item: u32, + last_click_item: u32, + hot_item: u32, + clicked_item: u32, + + // stages + state: State, + stage: Stage, + + // key info + active_key: string, + active_char: rune, + + // consume building + consume: [MAX_CONSUME]u8, + consume_focused: bool, // wether consuming is active + consume_index: int, + + // defaults + // escape_key: int, + + // click counting + clicks: int, + click_time: time.Time, + + // items data + items: []Item, + items_index: int, + items_last_index: int, + last_items: []Item, + item_map: map[u32]Animation, + item_sort: []^Item, // temp array for storing sorted results before clone + + // buffer data + // TODO could be an arena + buffer: []byte, + buffer_index: int, + + // input event buffer + events: [MAX_INPUT_EVENTS]Input_Event, + event_count: int, + + // id stack + ids: [32]u32, + ids_index: int, + last_id: u32, +} + +State :: enum { + Idle, + Capture, +} + +Stage :: enum { + Layout, + Post_Layout, + Process, +} + +Item_State :: enum { + Cold, + Hot, + Active, + Frozen, +} + +// Cut modes +Cut :: enum { + Left, + Right, + Top, + Bottom, + Fill, +} + +// ratio of parent size +Ratio :: enum { + None, + X, + Y, + XY, +} + +// layout modes | Default is CUT +Layout :: enum { + Cut, + Absolute, + Relative, + // Custom, +} + +// MOUSE calls: +// Down / Up called 1 frame +// Hot_Up called when active was over hot +// Capture called while holding active down +Call :: enum { + // left mouse button + Left_Down, + Left_Up, + Left_Hot_Up, + Left_Capture, + + // right mouse button + Right_Down, + Right_Up, + Right_Hot_Up, + Right_Capture, + + // middle mouse button + Middle_Down, + Middle_Up, + Middle_Hot_Up, + Middle_Capture, + + // Scroll info + Scroll, + + // Cursor Info + Cursor_Handle, + + // FIND + Find_Ignore, + + // Key / Char + Key, + Char, +} + +Callback :: proc(^Context, ^Item, Call) -> int + +Item :: struct { + handle: rawptr, // item handle + callback: Callback, // callback class that can be used to do things based on events + + // unique id + id: u32, + sort_children: bool, + + state: Item_State, + ignore: bool, + + // item state | makes this item UNIQUE + layout: Layout, + ratio: Ratio, + z_index: int, + + // tree info + parent: ^Item, + first_item: ^Item, + next_item: ^Item, + + // layout final + bounds: RectI, + + // layout info + layout_offset: [2]int, + layout_margin: int, + layout_size: [2]int, // width/height + layout_ratio: [2]f32, // TODO could be merged to somewhere else + + // cut info + layout_cut_self: Cut, // how the item will cut from the rect + layout_cut_children: Cut, // state that the children will inherit + layout_cut_gap: int, // how much to insert a gap after a cut + + // persistent data per item - queryable after end_layout + anim: Animation, +} + +Animation :: struct { + hot: f32, + active: f32, + trigger: f32, +} + +//////////////////////////////////////////////////////////////////////////////// +// CONTEXT MANAGEMENT +//////////////////////////////////////////////////////////////////////////////// + +@private +context_clear_data :: proc(ctx: ^Context) #no_bounds_check { + ctx.items_last_index = ctx.items_index + ctx.items_index = 0 + ctx.buffer_index = 0 + ctx.hot_item = 0 + ctx.ids_index = 0 + + // swap buffers + ctx.items, ctx.last_items = ctx.last_items, ctx.items +} + +context_init :: proc(ctx: ^Context, item_capacity, buffer_capacity: int) { + ctx.buffer = make([]byte, buffer_capacity) + ctx.items = make([]Item, item_capacity) + ctx.last_items = make([]Item, item_capacity) + ctx.item_sort = make([]^Item, item_capacity) + ctx.item_map = make(map[u32]Animation, item_capacity) + + ctx.stage = .Process + + context_clear_data(ctx) + context_clear_state(ctx) +} + +context_destroy :: proc(ctx: ^Context) { + delete(ctx.buffer) + delete(ctx.items) + delete(ctx.last_items) + delete(ctx.item_map) +} + +//////////////////////////////////////////////////////////////////////////////// +// INPUT CONTROL +//////////////////////////////////////////////////////////////////////////////// + +@private +add_input_event :: proc(ctx: ^Context, event: Input_Event) #no_bounds_check { + if ctx.event_count == MAX_INPUT_EVENTS { + return + } + + ctx.events[ctx.event_count] = event + ctx.event_count += 1 +} + +@private +clear_input_events :: proc(ctx: ^Context) { + ctx.event_count = 0 + ctx.scroll = {} +} + +set_cursor :: #force_inline proc(ctx: ^Context, x, y: int) { + ctx.cursor = { x, y } +} + +set_cursor_handle_callback :: proc(ctx: ^Context, callback: proc(new: int)) { + ctx.cursor_handle_callback = callback +} + +get_cursor :: #force_inline proc(ctx: ^Context) -> I2 { + return ctx.cursor +} + +get_cursor_start :: #force_inline proc(ctx: ^Context) -> I2 { + return ctx.cursor_start +} + +get_cursor_delta :: #force_inline proc(ctx: ^Context) -> I2 { + return ctx.cursor - ctx.cursor_start +} + +get_cursor_delta_frame :: #force_inline proc(ctx: ^Context) -> I2 { + return ctx.cursor_delta_frame +} + +set_button :: proc(ctx: ^Context, button: Mouse_Button, enabled: bool) { + if enabled { + incl(&ctx.buttons, button) + } else { + if ctx.button_ignore != nil && (button in ctx.buttons) { + ctx.button_ignore = nil + } + + excl(&ctx.buttons, button) + } +} + +button_pressed :: proc(ctx: ^Context, button: Mouse_Button) -> bool { + return button not_in ctx.last_buttons && button in ctx.buttons +} + +button_released :: proc(ctx: ^Context, button: Mouse_Button) -> bool { + return button in ctx.last_buttons && button not_in ctx.buttons +} + +get_clicks :: #force_inline proc(ctx: ^Context) -> int { + return ctx.clicks +} + +set_key :: proc(ctx: ^Context, key: string) { + add_input_event(ctx, { key, 0, .Key }) +} + +set_char :: proc(ctx: ^Context, value: rune) { + add_input_event(ctx, { "", value, .Char }) +} + +//////////////////////////////////////////////////////////////////////////////// +// STAGES +//////////////////////////////////////////////////////////////////////////////// + +begin_layout :: proc(ctx: ^Context) { + assert(ctx.stage == .Process) + context_clear_data(ctx) + ctx.stage = .Layout +} + +end_layout :: proc(ctx: ^Context) #no_bounds_check { + assert(ctx.stage == .Layout) + + if ctx.items_index > 0 { + root := item_root(ctx) + compute_size(root) + arrange(root, nil, 0) + + if ctx.items_last_index > 0 { + // map old item content + clear(&ctx.item_map) + for i in 0..<ctx.items_last_index { + item := ctx.last_items[i] + + if item.id != 0 { + ctx.item_map[item.id] = item.anim + } + } + + // map new content + for i in 0..<ctx.items_index { + item := &ctx.items[i] + + if item.id != 0 { + if old, ok := ctx.item_map[item.id]; ok { + item.anim = old + } + } + } + } + } + + // validate_state_items(ctx) + if ctx.items_index > 0 { + update_hot_item(ctx) + } + + ctx.stage = .Post_Layout + + // update hot/active for animation + speed := f32(0.50) + for i in 0..<ctx.items_index { + p := &ctx.items[i] + p.anim.hot = clamp(p.anim.hot + (p.id == ctx.hot_item ? speed : -speed), 0, 1) + p.anim.active = clamp(p.anim.active + (p.id == ctx.active_item ? speed : -speed), 0, 1) + p.anim.trigger = max(p.anim.trigger - speed, 0) + } +} + +update_hot_item :: proc(ctx: ^Context) { + if ctx.items_index == 0 { + return + } + + item := find_item(ctx, item_root(ctx), ctx.cursor.x, ctx.cursor.y) + ctx.hot_item = item.id +} + +// TODO this is shit +temp_find :: proc(ctx: ^Context, id: u32) -> (res: ^Item) { + for i in 0..<ctx.items_index { + item := &ctx.items[i] + if item.id == id { + res = item + break + } + } + + return +} + +process_button :: proc( + ctx: ^Context, + button: Mouse_Button, + hot_item: ^u32, + active_item: ^u32, + focus_item: ^u32, +) -> bool { + if button in ctx.buttons { + hot_item^ = 0 + active_item^ = ctx.hot_item + + if active_item^ != focus_item^ { + focus_item^ = 0 + ctx.focus_item = 0 + } + + capture := -1 + if active_item^ != 0 { + diff := time.since(ctx.click_time) + if diff > CLICK_THRESHOLD { + ctx.clicks = 0 + } + + ctx.clicks += 1 + ctx.last_click_item = active_item^ + ctx.click_time = time.now() + + active := temp_find(ctx, active_item^) + switch button { + case .Left: capture = item_callback(ctx, active, .Left_Down) + case .Middle: capture = item_callback(ctx, active, .Middle_Down) + case .Right: capture = item_callback(ctx, active, .Right_Down) + } + } + + // only capture if wanted + if capture != -1 || ctx.button_ignore != nil { + ctx.button_ignore = button + active_item^ = 0 + focus_item^ = 0 + return false + } else { + ctx.button_capture = button + ctx.state = .Capture + return true + } + } + + return false +} + +process :: proc(ctx: ^Context) { + assert(ctx.stage != .Layout) + if ctx.stage == .Process { + update_hot_item(ctx) + } + ctx.stage = .Process + + if ctx.items_index == 0 { + clear_input_events(ctx) + return + } + + hot_item := ctx.last_hot_item + active_item := ctx.active_item + focus_item := ctx.focus_item + cursor_handle := ctx.cursor_handle + + // send all keyboard events + if focus_item != 0 { + for i in 0..<ctx.event_count { + event := ctx.events[i] + ctx.active_key = event.key + ctx.active_char = event.char + + // consume char calls when active + if event.call == .Char && ctx.consume_focused { + if ctx.consume_index < MAX_CONSUME { + // TODO proper utf8 insertion + ctx.consume[ctx.consume_index] = u8(event.char) + ctx.consume_index += 1 + } + } else { + focus := temp_find(ctx, focus_item) + item_callback(ctx, focus, event.call) + } + + // TODO this + // // check for escape + // if event.key == ctx.escape_key { + // ctx.focus_item = nil + // } + } + } else { + ctx.focus_item = 0 + } + + // use redirect instead + if focus_item == 0 { + item := temp_find(ctx, ctx.focus_redirect_item) + + for i in 0..<ctx.event_count { + event := ctx.events[i] + ctx.active_key = event.key + ctx.active_char = event.char + item_callback(ctx, item, event.call) + } + } + + // apply scroll callback + if ctx.scroll != {} { + item := temp_find(ctx, ctx.hot_item) + item_callback(ctx, item, .Scroll) + } + + clear_input_events(ctx) + + hot := ctx.hot_item + ctx.clicked_item = 0 + + switch ctx.state { + case .Idle: + ctx.cursor_start = ctx.cursor + + left := process_button(ctx, .Left, &hot_item, &active_item, &focus_item) + middle := process_button(ctx, .Middle, &hot_item, &active_item, &focus_item) + right := process_button(ctx, .Right, &hot_item, &active_item, &focus_item) + + if !left && !right && !middle { + hot_item = hot + } + + case .Capture: + if ctx.button_capture not_in ctx.buttons { + if active_item != 0 { + active := temp_find(ctx, active_item) + switch ctx.button_capture { + case .Left: item_callback(ctx, active, .Left_Up) + case .Middle: item_callback(ctx, active, .Middle_Up) + case .Right: item_callback(ctx, active, .Right_Up) + } + + if active_item == hot { + switch ctx.button_capture { + case .Left: item_callback(ctx, active, .Left_Hot_Up) + case .Middle: item_callback(ctx, active, .Middle_Hot_Up) + case .Right: item_callback(ctx, active, .Right_Hot_Up) + } + ctx.clicked_item = active_item + } + } + + active_item = 0 + ctx.state = .Idle + } else { + active := temp_find(ctx, active_item) + if active_item != 0 { + switch ctx.button_capture { + case .Left: item_callback(ctx, active, .Left_Capture) + case .Middle: item_callback(ctx, active, .Middle_Capture) + case .Right: item_callback(ctx, active, .Right_Capture) + } + } + + hot_item = hot == active_item ? hot : 0 + } + } + + // look for possible cursor handle + if hot_item != 0 { + hot := temp_find(ctx, hot_item) + wanted_handle := item_callback(ctx, hot, .Cursor_Handle) + if wanted_handle != -1 { + ctx.cursor_handle = wanted_handle + } else { + // change back to zero - being the default arrow type + if ctx.cursor_handle != 0 { + ctx.cursor_handle = 0 + } + } + } + + // change of cursor handle + if cursor_handle != ctx.cursor_handle { + if ctx.cursor_handle_callback != nil { + ctx.cursor_handle_callback(ctx.cursor_handle) + } + } + + ctx.cursor_delta_frame = ctx.cursor_last_frame - ctx.cursor + ctx.cursor_last_frame = ctx.cursor + ctx.last_hot_item = hot_item + ctx.active_item = active_item + ctx.last_buttons = ctx.buttons +} + +context_clear_state :: proc(ctx: ^Context) { + ctx.last_hot_item = 0 + ctx.active_item = 0 + ctx.focus_item = 0 + ctx.last_click_item = 0 +} + +//////////////////////////////////////////////////////////////////////////////// +// UI DECLARATION +//////////////////////////////////////////////////////////////////////////////// + +item_make :: proc(ctx: ^Context) -> ^Item { + assert(ctx.stage == .Layout) + assert(ctx.items_index < len(ctx.items)) + + item := &ctx.items[ctx.items_index] + ctx.items_index += 1 + mem.zero_item(item) + + item.first_item = nil + item.next_item = nil + + return item +} + +item_callback :: proc(ctx: ^Context, item: ^Item, call: Call) -> int #no_bounds_check { + if item == nil || item.callback == nil { + return -1 + } + + return item.callback(ctx, item, call) +} + +set_frozen :: proc(item: ^Item, enable: bool) { + if enable { + item.state = .Frozen + } else { + item.state = .Cold + } +} + +alloc_handle :: proc(ctx: ^Context, item: ^Item, size: int, loc := #caller_location) -> rawptr { + assert(item.handle == nil) + assert(ctx.buffer_index + size <= len(ctx.buffer)) + item.handle = &ctx.buffer[ctx.buffer_index] + ctx.buffer_index += size + return item.handle +} + +alloc_typed :: proc(ctx: ^Context, item: ^Item, $T: typeid) -> ^T { + return cast(^T) alloc_handle(ctx, item, size_of(T)) +} + +item_append :: proc(item, sibling: ^Item) -> ^Item { + sibling.parent = item.parent + // TODO cut append + sibling.next_item = item.next_item + item.next_item = sibling + return sibling +} + +item_insert :: proc(parent, child: ^Item) -> ^Item { + // set cut direction + child.parent = parent + child.layout_cut_self = parent.layout_cut_children + + if parent.first_item == nil { + parent.first_item = child + } else { + item_append(last_child(parent), child) + } + + return child +} + +insert_front :: item_insert + +item_insert_back :: proc(parent, child: ^Item) -> ^Item { + child.parent = parent + child.layout_cut_self = parent.layout_cut_children + + child.next_item = parent.first_item + parent.first_item = child + return child +} + +set_ratio :: proc(item: ^Item, width, height: f32) { + w := clamp(width, 0, 1) + h := clamp(height, 0, 1) + item.layout_ratio = { w, h } + + if w != 0 && h != 0 { + item.ratio = .XY + } else { + if w != 0 { + item.ratio = .X + } else if h != 0 { + item.ratio = .Y + } + } +} + +focus :: proc(ctx: ^Context, item: ^Item, consume := false) { + assert(item != nil && uintptr(item) < uintptr(&ctx.items[ctx.items_index])) + assert(ctx.stage != .Layout) + ctx.focus_item = item.id + ctx.consume_focused = consume + ctx.consume_index = 0 +} + +consume_result :: proc(ctx: ^Context) -> string { + return transmute(string) mem.Raw_String { &ctx.consume[0], ctx.consume_index } +} + +consume_decrease :: proc(ctx: ^Context) { + if ctx.consume_index > 0 { + ctx.consume_index -= 1 + } +} + +focus_redirect :: proc(ctx: ^Context, item: ^Item) { + ctx.focus_redirect_item = item.id +} + +//////////////////////////////////////////////////////////////////////////////// +// ITERATION +//////////////////////////////////////////////////////////////////////////////// + +last_child :: proc(item: ^Item) -> ^Item { + item := item.first_item + + for item != nil { + next_item := item.next_item + if next_item == nil { + return item + } + item = next_item + } + + return nil +} + +// NOTE: uses temp allocator, since item_sort gets reused and the output needs to be stable! +// return z sorted list of children +children_list :: proc(ctx: ^Context, item: ^Item) -> []^Item { + count: int + + // loop through children and push items + kid := item.first_item + for kid != nil { + ctx.item_sort[count] = kid + kid = kid.next_item + count += 1 + } + + list := ctx.item_sort[:count] + + // optionally sort the children by z_index + if item.sort_children { + // sort and return a cloned list + slice.sort_by(list, proc(a, b: ^Item) -> bool { + return a.z_index < b.z_index + }) + } + + return slice.clone(list, context.temp_allocator) +} + +//////////////////////////////////////////////////////////////////////////////// +// QUERYING +//////////////////////////////////////////////////////////////////////////////// + +find_item :: proc( + ctx: ^Context, + item: ^Item, + x, y: int, + loc := #caller_location, +) -> ^Item #no_bounds_check { + // TODO frozen + // if pitem.state == .Frozen { + // return -1 + // } + + list := children_list(ctx, item) + for kid in list { + // fetch ignore status + ignore := kid.ignore + if item_callback(ctx, kid, .Find_Ignore) >= 0 { + ignore = true + } + + if !ignore && rect.contains(kid.bounds, x, y) { + return find_item(ctx, kid, x, y) + } + } + + return item +} + +get_key :: proc(ctx: ^Context) -> string { + return ctx.active_key +} + +get_char :: proc(ctx: ^Context) -> rune { + return ctx.active_char +} + +contains :: proc(item: ^Item, x, y: int) -> bool #no_bounds_check { + return rect.contains(item.bounds, x, y) +} + +//////////////////////////////////////////////////////////////////////////////// +// OTHER +//////////////////////////////////////////////////////////////////////////////// + +// true if the item match the active +is_active :: #force_inline proc(ctx: ^Context, item: ^Item) -> bool { + return ctx.active_item == item.id +} + +// true if the item match the hot +is_hot :: #force_inline proc(ctx: ^Context, item: ^Item) -> bool { + return ctx.last_hot_item == item.id +} + +// true if the item match the focused +is_focused :: #force_inline proc(ctx: ^Context, item: ^Item) -> bool { + return ctx.focus_item == item.id +} + +// true if the item match the clicked (HOT_UP) +is_clicked :: #force_inline proc(ctx: ^Context, item: ^Item) -> bool { + return ctx.clicked_item == item.id +} + +// shorthand for is_clicked(get_latest()) +latest_clicked :: #force_inline proc(ctx: ^Context) -> bool { + item := &ctx.items[ctx.items_index - 1] + return ctx.clicked_item == item.id +} + +// float activeness of an item 0 | 0.5 | 1 +activeness :: proc(ctx: ^Context, item: ^Item) -> f32 { + return ctx.active_item == item.id ? 1 : (ctx.last_hot_item == item.id ? 0.5 : 0) +} + +// hot + active activeness +activeness2 :: proc(item: ^Item) -> f32 #no_bounds_check { + return max(item.anim.hot * 0.5, item.anim.active) +} + +// compute the size of an item +// optional HSIZED / VSIZED for custom sizes +compute_size :: proc(item: ^Item) #no_bounds_check { + item.bounds.r = item.layout_size.x + item.bounds.b = item.layout_size.y + + // iterate children + kid := item.first_item + for kid != nil { + compute_size(kid) + kid = kid.next_item + } +} + +// layouts items based on rect-cut by default or custom ones +arrange :: proc(item: ^Item, layout: ^RectI, gap: int) #no_bounds_check { + // check for wanted ratios -> size conversion which depend on parent rect + switch item.ratio { + case .None: + case .X: item.layout_size.x = int(rect.widthf(layout^) * item.layout_ratio.x) + case .Y: item.layout_size.y = int(rect.heightf(layout^) * item.layout_ratio.y) + case .XY: + item.layout_size.x = int(rect.widthf(layout^) * item.layout_ratio.x) + item.layout_size.y = int(rect.heightf(layout^) * item.layout_ratio.y) + } + + switch item.layout { + // DEFAULT + case .Cut: + // directionality + switch item.layout_cut_self { + case .Left: item.bounds = rect.cut_left(layout, item.layout_size.x) + case .Right: item.bounds = rect.cut_right(layout, item.layout_size.x) + case .Top: item.bounds = rect.cut_top(layout, item.layout_size.y) + case .Bottom: item.bounds = rect.cut_bottom(layout, item.layout_size.y) + case .Fill: + item.bounds = layout^ + layout^ = {} + } + + // apply gapping + if gap > 0 { + switch item.layout_cut_self { + case .Left: layout.l += gap + case .Right: layout.r -= gap + case .Top: layout.t += gap + case .Bottom: layout.b -= gap + case .Fill: + } + } + + // case .Custom: + // item_callback(item, LAYOUT_CUSTOM) + + case .Absolute: + rect.sized(&item.bounds, item.layout_offset, item.layout_size) + + case .Relative: + rect.sized(&item.bounds, [2]int { layout.l, layout.t } + item.layout_offset, item.layout_size) + } + + // layout children with this resultant rect for LAYOUT_CUT + layout_with := item.bounds + kid := item.first_item + + if item.layout_margin > 0 { + layout_with = rect.margin(layout_with, item.layout_margin) + } + + for kid != nil { + arrange(kid, &layout_with, item.layout_cut_gap) + kid = kid.next_item + } +} + +item_root :: proc(ctx: ^Context) -> ^Item { + assert(ctx.items_index > 0) + return &ctx.items[0] +} + +//////////////////////////////////////////////////////////////////////////////// +// ID gen - same as microui +//////////////////////////////////////////////////////////////////////////////// + +gen_id_bytes :: proc(ctx: ^Context, input: []byte) -> (res: u32) { + seed := ctx.ids_index > 0 ? ctx.ids[ctx.ids_index - 1] : 2166136261 + res = hash.fnv32a(input, seed) + ctx.last_id = res + return +} +gen_id_string :: proc(ctx: ^Context, input: string) -> u32 { + return gen_id_bytes(ctx, transmute([]byte) input) +} +gen_id :: proc { gen_id_bytes, gen_id_string } + +gen_idf :: proc(ctx: ^Context, format: string, args: ..any) -> u32 { + return gen_id_bytes(ctx, transmute([]byte) fmt.tprintf(format, ..args)) +} + +push_id_bytes :: proc(ctx: ^Context, input: []byte) -> (res: u32) { + res = gen_id_bytes(ctx, input) + ctx.ids[ctx.ids_index] = res + ctx.ids_index += 1 + return +} +push_id_string :: proc(ctx: ^Context, input: string) -> (res: u32) { + res = gen_id_string(ctx, input) + ctx.ids[ctx.ids_index] = res + ctx.ids_index += 1 + return +} +push_id :: proc { push_id_bytes, push_id_string } + +// push_id_latest :: proc(ctx: ^Context) { +// ctx.ids[ctx.ids_index] = ctx.last_id +// ctx.ids_index += 1 +// } + +pop_id :: proc(ctx: ^Context) { + if ctx.ids_index > 0 { + ctx.ids_index -= 1 + } +} + +print_ids :: proc(ctx: ^Context) { + fmt.eprintln("~~~~~~~~~~") + for i in 0..<ctx.ids_index { + for j in 0..<i { + fmt.eprint('\t') + } + + id := ctx.ids[i] + fmt.eprintf("ID %d\n", id) + } +}
\ No newline at end of file |