aboutsummaryrefslogtreecommitdiff
path: root/lib/oui/oui.odin
diff options
context:
space:
mode:
authorSan Jacobs2023-10-15 14:59:33 +0200
committerSan Jacobs2023-10-15 14:59:33 +0200
commit690d1102dedbbad955c34ba1a5ef9f4d15d82158 (patch)
tree887d6bea6a843952c7d8346a99131601f603e19f /lib/oui/oui.odin
parentb6dab385dbf9a626970f3673d345d0e8e6a62e1e (diff)
parent2e3a7e10756954dc5a99d617a1c0eef327d3adbb (diff)
downloadsatscalc-690d1102dedbbad955c34ba1a5ef9f4d15d82158.tar.gz
satscalc-690d1102dedbbad955c34ba1a5ef9f4d15d82158.tar.bz2
satscalc-690d1102dedbbad955c34ba1a5ef9f4d15d82158.zip
Merge branch 'oui' into odin
Diffstat (limited to 'lib/oui/oui.odin')
-rw-r--r--lib/oui/oui.odin1010
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