aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorSan Jacobs2023-08-17 01:48:00 +0200
committerSan Jacobs2023-08-17 01:48:00 +0200
commita7c2d13bcca784350d851b665b8a19697e113b34 (patch)
treeb29207528bc4cbc4ef5d2d162c0a8d53b47da677 /lib
parent80362dbf454bb4bc5b19deb438b7c485240ef367 (diff)
downloadsatscalc-a7c2d13bcca784350d851b665b8a19697e113b34.tar.gz
satscalc-a7c2d13bcca784350d851b665b8a19697e113b34.tar.bz2
satscalc-a7c2d13bcca784350d851b665b8a19697e113b34.zip
Screw everything, we do oui now
Diffstat (limited to 'lib')
-rw-r--r--lib/oui/oui.odin1186
-rw-r--r--lib/oui/rect.odin245
2 files changed, 1431 insertions, 0 deletions
diff --git a/lib/oui/oui.odin b/lib/oui/oui.odin
new file mode 100644
index 0000000..d14f369
--- /dev/null
+++ b/lib/oui/oui.odin
@@ -0,0 +1,1186 @@
+package oui
+
+import "core:mem"
+import "core:fmt"
+import "core:time"
+import "core:slice"
+
+// 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
+// Animation
+
+MAX_DATASIZE :: 4096
+MAX_DEPTH :: 64
+MAX_INPUT_EVENTS :: 64
+CLICK_THRESHOLD :: time.Millisecond * 250
+MAX_CONSUME :: 256
+
+Input_Event :: struct {
+ key: int,
+ char: rune,
+ repeat: bool,
+ call: Call,
+}
+
+Mouse_Button :: enum {
+ Left,
+ Middle,
+ Right,
+}
+Mouse_Buttons :: bit_set[Mouse_Button]
+
+Context :: struct {
+ buttons: Mouse_Buttons,
+ last_buttons: Mouse_Buttons,
+ button_capture: Mouse_Button,
+
+ 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,
+
+ active_item: Item,
+ focus_item: Item,
+ focus_redirect_item: Item, // redirect messages if no item is focused to this item
+ last_hot_item: Item,
+ last_click_item: Item,
+ hot_item: Item,
+ clicked_item: Item,
+
+ state: State,
+ stage: Stage,
+ active_key: int,
+ active_key_repeat: bool,
+ active_char: rune,
+ mods: u32, // keyboard mods
+
+ // 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,
+
+ count: int,
+ last_count: int,
+ event_count: int,
+ data_size: int,
+
+ // items data
+ items: []Item_Raw,
+ last_items: []Item_Raw,
+ item_map: []Item, // last -> current matches
+ item_sort: []Item,
+
+ // buffer data
+ // TODO could be an arena
+ buffer: []byte,
+
+ // input event buffer
+ events: [MAX_INPUT_EVENTS]Input_Event,
+}
+
+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_Down,
+ Key_Up,
+ Char,
+}
+
+I2 :: [2]int
+Item :: int
+
+Callback :: proc(Item, Call) -> int
+
+Item_Raw :: struct {
+ handle: rawptr, // item handle
+
+ // callback class will always be called / set usually - user can override class calls
+ callback: Callback,
+
+ state: Item_State,
+ ignore: bool,
+
+ // item state | makes this item UNIQUE
+ layout: Layout,
+ ratio: Ratio,
+ z_index: int,
+
+ // tree info
+ parent: Item,
+ first_kid: Item,
+ next_item: Item,
+
+ // layout final
+ rect: Rect,
+
+ // 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
+ hot: f32,
+ active: f32,
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// CONTEXT MANAGEMENT
+////////////////////////////////////////////////////////////////////////////////
+
+ui: ^Context
+
+@private
+context_clear_data :: proc() #no_bounds_check {
+ ui.last_count = ui.count
+ ui.count = 0
+ ui.data_size = 0
+ ui.hot_item = -1
+
+ // swap buffers
+ ui.items, ui.last_items = ui.last_items, ui.items
+
+ // set map
+ for i in 0..<ui.last_count {
+ ui.item_map[i] = -1
+ }
+}
+
+context_create :: proc(item_capacity, buffer_capacity: int) -> ^Context {
+ ctx := new(Context)
+ ctx.buffer = make([]byte, buffer_capacity)
+ ctx.items = make([]Item_Raw, item_capacity)
+ ctx.last_items = make([]Item_Raw, item_capacity)
+ ctx.item_map = make([]Item, item_capacity)
+ ctx.item_sort = make([]Item, item_capacity)
+
+ ctx.stage = .Process
+
+ old_ctx := ui
+ context_make_current(ctx)
+ context_clear_data()
+ context_clear_state()
+ context_make_current(old_ctx)
+ return ctx
+}
+
+context_make_current :: #force_inline proc(ctx: ^Context) {
+ ui = ctx
+}
+
+context_destroy :: proc(ctx: ^Context) {
+ if ui == ctx {
+ context_make_current(nil)
+ }
+
+ delete(ctx.buffer)
+ delete(ctx.items)
+ delete(ctx.last_items)
+ delete(ctx.item_map)
+ free(ctx)
+}
+
+context_get :: #force_inline proc() -> ^Context {
+ return ui
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// INPUT CONTROL
+////////////////////////////////////////////////////////////////////////////////
+
+@private
+add_input_event :: proc(event: Input_Event) #no_bounds_check {
+ if ui.event_count == MAX_INPUT_EVENTS {
+ return
+ }
+
+ ui.events[ui.event_count] = event
+ ui.event_count += 1
+}
+
+@private
+clear_input_events :: proc() {
+ ui.event_count = 0
+ ui.scroll = {}
+}
+
+set_cursor :: #force_inline proc(x, y: int) {
+ ui.cursor = { x, y }
+}
+
+set_cursor_handle_callback :: proc(callback: proc(new: int)) {
+ ui.cursor_handle_callback = callback
+}
+
+get_cursor :: #force_inline proc() -> I2 {
+ return ui.cursor
+}
+
+get_cursor_start :: #force_inline proc() -> I2 {
+ return ui.cursor_start
+}
+
+get_cursor_delta :: #force_inline proc() -> I2 {
+ return ui.cursor - ui.cursor_start
+}
+
+get_cursor_delta_frame :: #force_inline proc() -> I2 {
+ return ui.cursor_delta_frame
+}
+
+set_button :: proc(button: Mouse_Button, enabled: bool) {
+ if enabled {
+ incl(&ui.buttons, button)
+ } else {
+ excl(&ui.buttons, button)
+ }
+}
+
+set_mods :: proc(mods: u32, enabled: bool) {
+ if enabled {
+ ui.mods |= mods
+ } else {
+ ui.mods = mods
+ }
+}
+
+get_last_button :: proc(button: Mouse_Button) -> bool {
+ return button in ui.last_buttons
+}
+
+get_button :: proc(button: Mouse_Button) -> bool {
+ return button in ui.buttons
+}
+
+button_pressed :: proc(button: Mouse_Button) -> bool {
+ return !get_last_button(button) && get_button(button)
+}
+
+button_released :: proc(button: Mouse_Button) -> bool {
+ return get_last_button(button) && !get_button(button)
+}
+
+get_clicks :: #force_inline proc() -> int {
+ return ui.clicks
+}
+
+set_key :: proc(key: int, down: bool, repeat: bool) {
+ add_input_event({ key, 0, repeat, down ? .Key_Down : .Key_Up })
+}
+
+set_char :: proc(value: rune) {
+ add_input_event({ 0, value, false, .Char })
+}
+
+set_scroll :: proc(x, y: int) {
+ ui.scroll += { x, y }
+}
+
+get_scroll :: #force_inline proc() -> I2 {
+ return ui.scroll
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// STAGES
+////////////////////////////////////////////////////////////////////////////////
+
+begin_layout :: proc() {
+ assert(ui.stage == .Process)
+ context_clear_data()
+ ui.stage = .Layout
+}
+
+end_layout :: proc() #no_bounds_check {
+ assert(ui.stage == .Layout)
+
+ if ui.count > 0 {
+ compute_size(0)
+ arrange(0, nil, 0)
+
+ if ui.last_count > 0 {
+ map_items(0, 0)
+
+ // remap hot/activeness of matched items
+ for i in 0..<ui.last_count {
+ old := ui.item_map[i]
+
+ if old != -1 {
+ pold := &ui.last_items[old]
+ pcurr := &ui.items[i]
+ pcurr.hot = pold.hot
+ pcurr.active = pold.active
+ }
+ }
+ }
+ }
+
+ validate_state_items()
+ if ui.count > 0 {
+ update_hot_item()
+ }
+
+ ui.stage = .Post_Layout
+
+ // update hot/active for animation
+ speed := f32(0.05)
+ for i in 0..<ui.count {
+ p := &ui.items[i]
+ p.hot = clamp(p.hot + (i == ui.hot_item ? speed : -speed), 0, 1)
+ p.active = clamp(p.active + (i == ui.active_item ? speed : -speed), 0, 1)
+ }
+}
+
+update_hot_item :: proc() {
+ if ui.count == 0 {
+ return
+ }
+
+ ui.hot_item = find_item(0, &ui.items[0], ui.cursor.x, ui.cursor.y)
+}
+
+process_button :: proc(
+ button: Mouse_Button,
+ hot_item: ^int,
+ active_item: ^int,
+ focus_item: ^int,
+) -> bool {
+ if get_button(button) {
+ hot_item^ = -1
+ active_item^ = ui.hot_item
+
+ if active_item^ != focus_item^ {
+ focus_item^ = -1
+ ui.focus_item = -1
+ }
+
+ if active_item^ >= 0 {
+ diff := time.since(ui.click_time)
+ if diff > CLICK_THRESHOLD {
+ ui.clicks = 0
+ }
+
+ ui.clicks += 1
+ ui.last_click_item = active_item^
+ ui.click_time = time.now()
+
+ switch button {
+ case .Left: item_callback(active_item^, .Left_Down)
+ case .Middle: item_callback(active_item^, .Middle_Down)
+ case .Right: item_callback(active_item^, .Right_Down)
+ }
+ }
+
+ // only capture if wanted
+ ui.button_capture = button
+ ui.state = .Capture
+ return true
+ }
+
+ return false
+}
+
+process :: proc() {
+ assert(ui.stage != .Layout)
+ if ui.stage == .Process {
+ update_hot_item()
+ }
+ ui.stage = .Process
+
+ if ui.count == 0 {
+ clear_input_events()
+ return
+ }
+
+ hot_item := ui.last_hot_item
+ active_item := ui.active_item
+ focus_item := ui.focus_item
+ cursor_handle := ui.cursor_handle
+
+ // send all keyboard events
+ if focus_item >= 0 {
+ for i in 0..<ui.event_count {
+ event := ui.events[i]
+ ui.active_key = event.key
+ ui.active_key_repeat = event.repeat
+ ui.active_char = event.char
+
+ // consume char calls when active
+ if event.call == .Char && ui.consume_focused {
+ if ui.consume_index < MAX_CONSUME {
+ // TODO proper utf8 insertion
+ ui.consume[ui.consume_index] = u8(event.char)
+ ui.consume_index += 1
+ }
+ } else {
+ item_callback(focus_item, event.call)
+ }
+
+ // check for escape
+ if event.key == ui.escape_key && ui.mods == 0 {
+ ui.focus_item = -1
+ }
+ }
+ } else {
+ ui.focus_item = -1
+ }
+
+ // use redirect instead
+ if focus_item == -1 {
+ item := ui.focus_redirect_item
+
+ for i in 0..<ui.event_count {
+ event := ui.events[i]
+ ui.active_key = event.key
+ ui.active_key_repeat = event.repeat
+ ui.active_char = event.char
+ item_callback(item, event.call)
+ }
+ }
+
+ // apply scroll callback
+ if ui.scroll != {} {
+ item_callback(hot_item, .Scroll)
+ }
+
+ clear_input_events()
+
+ hot := ui.hot_item
+ ui.clicked_item = -1
+
+ switch ui.state {
+ case .Idle:
+ ui.cursor_start = ui.cursor
+
+ left := process_button(.Left, &hot_item, &active_item, &focus_item)
+ middle := process_button(.Middle, &hot_item, &active_item, &focus_item)
+ right := process_button(.Right, &hot_item, &active_item, &focus_item)
+
+ if !left && !right && !middle {
+ hot_item = hot
+ }
+
+ case .Capture:
+ if !get_button(ui.button_capture) {
+ if active_item >= 0 {
+ switch ui.button_capture {
+ case .Left: item_callback(active_item, .Left_Up)
+ case .Middle: item_callback(active_item, .Middle_Up)
+ case .Right: item_callback(active_item, .Right_Up)
+ }
+
+ if active_item == hot {
+ switch ui.button_capture {
+ case .Left: item_callback(active_item, .Left_Hot_Up)
+ case .Middle: item_callback(active_item, .Middle_Hot_Up)
+ case .Right: item_callback(active_item, .Right_Hot_Up)
+ }
+ ui.clicked_item = active_item
+ }
+ }
+
+ active_item = -1
+ ui.state = .Idle
+ } else {
+ if active_item >= 0 {
+ switch ui.button_capture {
+ case .Left: item_callback(active_item, .Left_Capture)
+ case .Middle: item_callback(active_item, .Middle_Capture)
+ case .Right: item_callback(active_item, .Right_Capture)
+ }
+ }
+
+ hot_item = hot == active_item ? hot : -1
+ }
+ }
+
+ // look for possible cursor handle
+ if hot_item != -1 {
+ wanted_handle := item_callback(hot_item, .Cursor_Handle)
+ if wanted_handle != -1 {
+ ui.cursor_handle = wanted_handle
+ } else {
+ // change back to zero - being the default arrow type
+ if ui.cursor_handle != 0 {
+ ui.cursor_handle = 0
+ }
+ }
+ }
+
+ // change of cursor handle
+ if cursor_handle != ui.cursor_handle {
+ if ui.cursor_handle_callback != nil {
+ ui.cursor_handle_callback(ui.cursor_handle)
+ }
+ }
+
+ ui.cursor_delta_frame = ui.cursor_last_frame - ui.cursor
+ ui.cursor_last_frame = ui.cursor
+ ui.last_hot_item = hot_item
+ ui.active_item = active_item
+ ui.last_buttons = ui.buttons
+}
+
+context_clear_state :: proc() {
+ ui.last_hot_item = -1
+ ui.active_item = -1
+ ui.focus_item = -1
+ ui.last_click_item = -1
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// UI DECLARATION
+////////////////////////////////////////////////////////////////////////////////
+
+item_make :: proc() -> Item {
+ assert(ui.stage == .Layout)
+ assert(ui.count < len(ui.items))
+
+ idx := ui.count
+ ui.count += 1
+
+ item := &ui.items[idx]
+ mem.zero_item(item)
+
+ item.first_kid = -1
+ item.next_item = -1
+
+ return idx
+}
+
+item_callback :: proc(item: Item, call: Call) -> int #no_bounds_check {
+ pitem := &ui.items[item]
+
+ if pitem.callback == nil {
+ return -1
+ }
+
+ return pitem.callback(item, call)
+}
+
+// call the callback for the parent of the item
+item_callback_parent :: proc(item: Item, call: Call) -> int #no_bounds_check {
+ pitem := &ui.items[item]
+ return item_callback(pitem.parent, call)
+}
+
+set_frozen :: proc(item: Item, enable: bool) {
+ pitem := &ui.items[item]
+ if enable {
+ pitem.state = .Frozen
+ } else {
+ pitem.state = .Cold
+ }
+}
+
+set_handle :: proc(item: Item, handle: rawptr) {
+ pitem := &ui.items[item]
+ pitem.handle = handle
+}
+
+alloc_handle :: proc(item: Item, size: int, loc := #caller_location) -> rawptr {
+ pitem := &ui.items[item]
+ assert(pitem.handle == nil)
+ assert(ui.data_size + size <= len(ui.buffer))
+ pitem.handle = &ui.buffer[ui.data_size]
+ ui.data_size += size
+ return pitem.handle
+}
+
+alloc_typed :: proc(item: Item, $T: typeid) -> ^T {
+ return cast(^T) alloc_handle(item, size_of(T))
+}
+
+set_callback :: #force_inline proc(item: Item, callback: Callback) {
+ pitem := &ui.items[item]
+ pitem.callback = callback
+}
+
+item_append :: proc(item: Item, sibling: Item) -> Item {
+ assert(sibling > 0)
+ pitem := &ui.items[item]
+ psibling := &ui.items[sibling]
+ psibling.parent = pitem.parent
+ // TODO cut append
+ psibling.next_item = pitem.next_item
+ pitem.next_item = sibling
+ return sibling
+}
+
+item_insert :: proc(item: Item, child: Item) -> Item {
+ assert(child > 0)
+ pparent := &ui.items[item]
+ pchild := &ui.items[child]
+
+ // set cut direction
+ pchild.parent = item
+ pchild.layout_cut_self = pparent.layout_cut_children
+
+ if pparent.first_kid < 0 {
+ pparent.first_kid = child
+ } else {
+ item_append(last_child(item), child)
+ }
+
+ return child
+}
+
+insert_front :: item_insert
+
+item_insert_back :: proc(item: Item, child: Item) -> Item {
+ assert(child > 0)
+ pparent := &ui.items[item]
+ pchild := &ui.items[child]
+ pchild.parent = item
+ pchild.layout_cut_self = pparent.layout_cut_children
+
+ pchild.next_item = pparent.first_kid
+ pparent.first_kid = child
+ return child
+}
+
+set_size :: proc(item: Item, w, h: int) {
+ pitem := &ui.items[item]
+ pitem.layout_size = { w, h }
+}
+
+set_height :: proc(item: Item, h: int) {
+ pitem := &ui.items[item]
+ pitem.layout_size.y = h
+}
+
+set_width :: proc(item: Item, w: int) {
+ pitem := &ui.items[item]
+ pitem.layout_size.x = w
+}
+
+set_ratio :: proc(item: Item, width, height: f32) {
+ pitem := &ui.items[item]
+ w := clamp(width, 0, 1)
+ h := clamp(height, 0, 1)
+ pitem.layout_ratio = { w, h }
+
+ if w != 0 && h != 0 {
+ pitem.ratio = .XY
+ } else {
+ if w != 0 {
+ pitem.ratio = .X
+ } else if h != 0 {
+ pitem.ratio = .Y
+ }
+ }
+}
+
+set_gap :: proc(item: Item, gap: int) {
+ pitem := &ui.items[item]
+ pitem.layout_cut_gap = gap
+}
+
+set_margin :: proc(item: Item, margin: int) {
+ pitem := &ui.items[item]
+ pitem.layout_margin = margin
+}
+
+set_offset :: proc(item: Item, x, y: int) {
+ pitem := &ui.items[item]
+ pitem.layout_offset = { x, y }
+}
+
+// set a custom layouting method - default is by cut
+set_layout :: proc(item: Item, layout: Layout) {
+ pitem := &ui.items[item]
+ pitem.layout = layout
+}
+
+// set the cut direction on the item - only applies to children
+set_cut :: proc(item: Item, cut: Cut) {
+ pitem := &ui.items[item]
+ pitem.layout_cut_children = cut
+}
+
+set_z :: proc(item: Item, z_index: int) {
+ pitem := &ui.items[item]
+ pitem.z_index = z_index
+}
+
+set_ignore :: proc(item: Item) {
+ pitem := &ui.items[item]
+ pitem.ignore = true
+}
+
+focus :: proc(item: Item, consume := false) {
+ assert(item >= -1 && item < ui.count)
+ assert(ui.stage != .Layout)
+ ui.focus_item = item
+ ui.consume_focused = consume
+ ui.consume_index = 0
+}
+
+consume_result :: proc() -> string {
+ return transmute(string) mem.Raw_String { &ui.consume[0], ui.consume_index }
+}
+
+focus_redirect :: proc(item: Item) {
+ ui.focus_redirect_item = item
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ITERATION
+////////////////////////////////////////////////////////////////////////////////
+
+last_child :: proc(item: Item) -> Item {
+ item := first_child(item)
+
+ if item < 0 {
+ return -1
+ }
+
+ for {
+ next_item := next_sibling(item)
+ if next_item < 0 {
+ return item
+ }
+ item = next_item
+ }
+
+ return -1
+}
+
+first_child :: proc(item: Item) -> Item {
+ return ui.items[item].first_kid
+}
+
+next_sibling :: proc(item: Item) -> Item {
+ return ui.items[item].next_item
+}
+
+get_parent :: proc(item: Item) -> Item {
+ return ui.items[item].parent
+}
+
+// NOTE: uses temp allocator, since item_sort gets reused and the output needs to be stable!
+// return z sorted list of children
+children_sorted :: proc(item: Item) -> []Item {
+ count: int
+
+ // loop through children and push items
+ kid := first_child(item)
+ for kid > 0 {
+ ui.item_sort[count] = kid
+ kid = next_sibling(kid)
+ count += 1
+ }
+
+ // SHITTY since we need to refetch the items, could maybe perform sorting better
+ list := ui.item_sort[:count]
+ slice.sort_by(list, proc(a, b: Item) -> bool {
+ aa := ui.items[a]
+ bb := ui.items[b]
+ return aa.z_index < bb.z_index
+ })
+
+ return slice.clone(list, context.temp_allocator)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// QUERYING
+////////////////////////////////////////////////////////////////////////////////
+
+get_item_count :: #force_inline proc() -> int {
+ return ui.count
+}
+
+get_alloc_size :: #force_inline proc() -> int {
+ return ui.data_size
+}
+
+get_handle :: #force_inline proc(item: Item) -> rawptr {
+ pitem := ui.items[item]
+ return pitem.handle
+}
+
+get_hot_item :: #force_inline proc() -> Item {
+ return ui.hot_item
+}
+
+get_focused_item :: #force_inline proc() -> Item {
+ return ui.focus_item
+}
+
+find_item :: proc(
+ item: Item,
+ pitem: ^Item_Raw,
+ x, y: int,
+ loc := #caller_location,
+) -> Item #no_bounds_check {
+ // TODO frozen
+ // if pitem.state == .Frozen {
+ // return -1
+ // }
+
+ kid := pitem.first_kid
+ for kid >= 0 {
+ pkid := &ui.items[kid]
+
+ // fetch ignore status
+ ignore := pkid.ignore
+ if item_callback(kid, .Find_Ignore) >= 0 {
+ ignore = true
+ }
+
+ if !ignore && rect_contains(pkid.rect, x, y) {
+ return find_item(kid, pkid, x, y)
+ }
+
+ kid = pkid.next_item
+ }
+
+ return item
+}
+
+get_key :: proc() -> int {
+ return ui.active_key
+}
+
+get_key_repeat :: proc() -> bool {
+ return ui.active_key_repeat
+}
+
+get_char :: proc() -> rune {
+ return ui.active_char
+}
+
+get_mods :: proc() -> u32 {
+ return ui.mods
+}
+
+get_rect :: proc(item: Item) -> Rect #no_bounds_check {
+ return ui.items[item].rect
+}
+
+contains :: proc(item: Item, x, y: int) -> bool #no_bounds_check {
+ return rect_contains(ui.items[item].rect, x, y)
+}
+
+get_width :: #force_inline proc(item: Item) -> int #no_bounds_check {
+ return ui.items[item].layout_size.x
+}
+
+get_height :: #force_inline proc(item: Item) -> int #no_bounds_check {
+ return ui.items[item].layout_size.y
+}
+
+recover_item :: proc(old_item: Item) -> Item #no_bounds_check {
+ assert(old_item >= -1 && old_item < ui.last_count)
+ if old_item == -1 {
+ return -1
+ }
+ return ui.item_map[old_item]
+}
+
+remap_item :: proc(old_item, new_item: Item) #no_bounds_check {
+ assert(old_item >= 0 && old_item < ui.last_count)
+ assert(new_item >= -1 && new_item < ui.count)
+ ui.item_map[old_item] = new_item
+}
+
+get_last_item_count :: proc() -> int {
+ return ui.last_count
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// PRIVATE
+////////////////////////////////////////////////////////////////////////////////
+
+@private
+compare_items :: proc(p1, p2: ^Item_Raw) -> bool {
+ // return (p1.flags & MASK_COMPARE) == (p2.flags & MASK_COMPARE)
+ return p1.layout == p2.layout && p1.ratio == p2.ratio
+}
+
+@private
+map_items :: proc(i1, i2: Item) -> bool #no_bounds_check {
+ p1 := &ui.last_items[i1]
+ if i2 == -1 {
+ return false
+ }
+
+ p2 := &ui.items[i2]
+
+ if !compare_items(p1, p2) {
+ return false
+ }
+
+ count := 0
+ failed := 0
+ kid1 := p1.first_kid
+ kid2 := p2.first_kid
+
+ for kid1 != -1 {
+ pkid1 := &ui.last_items[kid1]
+ count += 1
+
+ if !map_items(kid1, kid2) {
+ failed = count
+ break
+ }
+
+ kid1 = pkid1.next_item
+
+ if kid2 != -1 {
+ kid2 = ui.items[kid2].next_item
+ }
+ }
+
+ if count > 0 && failed == 1 {
+ return false
+ }
+
+ // same item
+ ui.item_map[i1] = i2
+ return true
+}
+
+@private
+validate_state_items :: proc() {
+ ui.last_hot_item = recover_item(ui.last_hot_item)
+ ui.active_item = recover_item(ui.active_item)
+ ui.focus_item = recover_item(ui.focus_item)
+ ui.last_click_item = recover_item(ui.last_click_item)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// OTHER
+////////////////////////////////////////////////////////////////////////////////
+
+// true if the item match the active
+is_active :: #force_inline proc(item: Item) -> bool {
+ return ui.active_item == item
+}
+
+// true if the item match the hot
+is_hot :: #force_inline proc(item: Item) -> bool {
+ return ui.last_hot_item == item
+}
+
+// true if the item match the focused
+is_focused :: #force_inline proc(item: Item) -> bool {
+ return ui.focus_item == item
+}
+
+// true if the item match the clicked (HOT_UP)
+is_clicked :: #force_inline proc(item: Item) -> bool {
+ return ui.clicked_item == item
+}
+
+// get the latest appended item
+get_latest :: #force_inline proc() -> int {
+ return ui.count - 1
+}
+
+// shorthand for is_clicked(get_latest())
+latest_clicked :: #force_inline proc() -> bool {
+ return ui.clicked_item == ui.count - 1
+}
+
+// float activeness of an item 0 | 0.5 | 1
+activeness :: proc(item: Item) -> f32 {
+ return ui.active_item == item ? 1 : (ui.last_hot_item == item ? 0.5 : 0)
+}
+
+// get hot animation value
+get_hot :: proc(item: Item) -> f32 #no_bounds_check {
+ pitem := &ui.items[item]
+ return pitem.hot;
+}
+
+// get active animation value
+get_active :: proc(item: Item) -> f32 #no_bounds_check {
+ pitem := &ui.items[item]
+ return pitem.active;
+}
+
+// hot + active activeness
+get_activeness :: proc(item: Item) -> f32 #no_bounds_check {
+ pitem := &ui.items[item]
+ return max(pitem.hot * 0.5, pitem.active)
+}
+
+// compute the size of an item
+// optional HSIZED / VSIZED for custom sizes
+compute_size :: proc(item: Item) #no_bounds_check {
+ pitem := &ui.items[item]
+
+ // if flags_check(pitem.flags, ITEM_HSIZED) {
+ // pitem.layout_size.x = item_callback(item, ITEM_HSIZED)
+ // }
+
+ // if flags_check(pitem.flags, ITEM_VSIZED) {
+ // pitem.layout_size.y = item_callback(item, ITEM_VSIZED)
+ // }
+
+ pitem.rect.r = pitem.layout_size.x
+ pitem.rect.b = pitem.layout_size.y
+
+ // iterate children
+ kid := first_child(item)
+ for kid > 0 {
+ compute_size(kid)
+ kid = next_sibling(kid)
+ }
+}
+
+// layouts items based on rect-cut by default or custom ones
+arrange :: proc(item: Item, layout: ^Rect, gap: int) #no_bounds_check {
+ pitem := &ui.items[item]
+
+ // check for wanted ratios -> size conversion which depend on parent rect
+ switch pitem.ratio {
+ case .None:
+ case .X: pitem.layout_size.x = int(rect_widthf(layout^) * pitem.layout_ratio.x)
+ case .Y: pitem.layout_size.y = int(rect_heightf(layout^) * pitem.layout_ratio.y)
+ case .XY:
+ pitem.layout_size.x = int(rect_widthf(layout^) * pitem.layout_ratio.x)
+ pitem.layout_size.y = int(rect_heightf(layout^) * pitem.layout_ratio.y)
+ }
+
+ switch pitem.layout {
+ // DEFAULT
+ case .Cut:
+ // directionality
+ switch pitem.layout_cut_self {
+ case .Left: pitem.rect = rect_cut_left(layout, pitem.layout_size.x)
+ case .Right: pitem.rect = rect_cut_right(layout, pitem.layout_size.x)
+ case .Top: pitem.rect = rect_cut_top(layout, pitem.layout_size.y)
+ case .Bottom: pitem.rect = rect_cut_bottom(layout, pitem.layout_size.y)
+ case .Fill:
+ pitem.rect = layout^
+ layout^ = {}
+ }
+
+ // apply gapping
+ if gap > 0 {
+ switch pitem.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(&pitem.rect, pitem.layout_offset, pitem.layout_size)
+
+ case .Relative:
+ rect_sized(&pitem.rect, { layout.l, layout.t } + pitem.layout_offset, pitem.layout_size)
+ }
+
+ // layout children with this resultant rect for LAYOUT_CUT
+ layout_with := pitem.rect
+ kid := first_child(item)
+
+ if pitem.layout_margin > 0 {
+ layout_with = rect_margin(layout_with, pitem.layout_margin)
+ }
+
+ for kid > 0 {
+ arrange(kid, &layout_with, pitem.layout_cut_gap)
+ kid = next_sibling(kid)
+ }
+} \ No newline at end of file
diff --git a/lib/oui/rect.odin b/lib/oui/rect.odin
new file mode 100644
index 0000000..23f2e6d
--- /dev/null
+++ b/lib/oui/rect.odin
@@ -0,0 +1,245 @@
+package oui
+
+import "core:math"
+
+Rect :: struct {
+ l, r, t, b: int,
+}
+
+RECT_INF :: Rect {
+ max(int),
+ -max(int),
+ max(int),
+ -max(int),
+}
+
+// build a rectangle from multiple
+rect_inf_push :: proc(rect: ^Rect, other: Rect) {
+ rect.t = min(rect.t, other.t)
+ rect.l = min(rect.l, other.l)
+ rect.b = max(rect.b, other.b)
+ rect.r = max(rect.r, other.r)
+}
+
+rect_one :: #force_inline proc(a: int) -> Rect {
+ return { a, a, a, a }
+}
+
+rect_one_inv :: #force_inline proc(a: int) -> Rect {
+ return { a, -a, a, -a }
+}
+
+rect_negate :: #force_inline proc(a: Rect) -> Rect {
+ return {
+ -a.l,
+ -a.r,
+ -a.t,
+ -a.b,
+ }
+}
+
+rect_valid :: #force_inline proc(a: Rect) -> bool {
+ return a.r > a.l && a.b > a.t
+}
+
+rect_invalid :: #force_inline proc(rect: Rect) -> bool {
+ return !rect_valid(rect)
+}
+
+rect_width_invalid :: #force_inline proc(rect: Rect) -> bool {
+ return rect.r < rect.l
+}
+
+rect_height_invalid :: #force_inline proc(rect: Rect) -> bool {
+ return rect.b < rect.t
+}
+
+rect_wh :: #force_inline proc(x, y, w, h: int) -> Rect {
+ return { x, x + w, y, y + h }
+}
+
+rect_center :: #force_inline proc(a: Rect) -> (x, y: f32) {
+ return f32(a.l) + f32(a.r - a.l) / 2, f32(a.t) + f32(a.b - a.t) / 2
+}
+
+// width
+rect_width :: #force_inline proc(a: Rect) -> int {
+ return (a.r - a.l)
+}
+rect_widthf :: #force_inline proc(a: Rect) -> f32 {
+ return f32(a.r - a.l)
+}
+rect_width_halfed :: #force_inline proc(a: Rect) -> int {
+ return (a.r - a.l) / 2
+}
+rect_widthf_halfed :: #force_inline proc(a: Rect) -> f32 {
+ return f32(a.r - a.l) / 2
+}
+
+// height
+rect_height :: #force_inline proc(a: Rect) -> int {
+ return (a.b - a.t)
+}
+rect_heightf :: #force_inline proc(a: Rect) -> f32 {
+ return f32(a.b - a.t)
+}
+rect_height_halfed :: #force_inline proc(a: Rect) -> int {
+ return (a.b - a.t) / 2
+}
+rect_heightf_halfed :: #force_inline proc(a: Rect) -> f32 {
+ return f32(a.b - a.t) / 2
+}
+
+// width / height by option
+rect_opt_v :: #force_inline proc(a: Rect, vertical: bool) -> int {
+ return vertical ? rect_height(a) : rect_width(a)
+}
+rect_opt_h :: #force_inline proc(a: Rect, horizontal: bool) -> int {
+ return horizontal ? rect_width(a) : rect_height(a)
+}
+rect_opt_vf :: #force_inline proc(a: Rect, vertical: bool) -> f32 {
+ return vertical ? rect_heightf(a) : rect_widthf(a)
+}
+rect_opt_hf :: #force_inline proc(a: Rect, horizontal: bool) -> f32 {
+ return horizontal ? rect_widthf(a) : rect_heightf(a)
+}
+
+rect_xxyy :: #force_inline proc(x, y: int) -> Rect {
+ return { x, x, y, y }
+}
+
+rect_intersection :: proc(a, b: Rect) -> Rect {
+ a := a
+ if a.l < b.l do a.l = b.l
+ if a.t < b.t do a.t = b.t
+ if a.r > b.r do a.r = b.r
+ if a.b > b.b do a.b = b.b
+ return a
+}
+
+// smallest rectangle
+rect_bounding :: proc(a, b: Rect) -> Rect {
+ a := a
+ if a.l > b.l do a.l = b.l
+ if a.t > b.t do a.t = b.t
+ if a.r < b.r do a.r = b.r
+ if a.b < b.b do a.b = b.b
+ return a
+}
+
+rect_contains :: proc(a: Rect, x, y: int) -> bool {
+ return a.l <= x && a.r > x && a.t <= y && a.b > y
+}
+
+rect_offset :: proc(rect: ^Rect, x, y: int) {
+ rect.l += x
+ rect.r += x
+ rect.t += y
+ rect.b += y
+}
+
+rect_sized :: #force_inline proc(rect: ^Rect, pos: [2]int, size: [2]int) {
+ rect.l = pos.x
+ rect.r = pos.x + size.x
+ rect.t = pos.y
+ rect.b = pos.y + size.y
+}
+
+// rect cutting with HARD CUT, will result in invalid rectangles when out of size
+
+rect_cut_left :: proc(rect: ^Rect, a: int) -> (res: Rect) {
+ res = rect^
+ res.r = rect.l + a
+ rect.l = res.r
+ return
+}
+
+rect_cut_right :: proc(rect: ^Rect, a: int) -> (res: Rect) {
+ res = rect^
+ res.l = rect.r - a
+ rect.r = res.l
+ return
+}
+
+rect_cut_top :: proc(rect: ^Rect, a: int) -> (res: Rect) {
+ res = rect^
+ res.b = rect.t + a
+ rect.t = res.b
+ return
+}
+
+rect_cut_bottom :: proc(rect: ^Rect, a: int) -> (res: Rect) {
+ res = rect^
+ res.t = rect.b - a
+ rect.b = res.t
+ return
+}
+
+// add another rect as padding
+rect_padding :: proc(a, b: Rect) -> Rect {
+ a := a
+ a.l += b.l
+ a.t += b.t
+ a.r -= b.r
+ a.b -= b.b
+ return a
+}
+
+// add another rect as padding
+rect_margin :: proc(a: Rect, value: int) -> Rect {
+ a := a
+ a.l += value
+ a.t += value
+ a.r -= value
+ a.b -= value
+ return a
+}
+
+rect_add :: proc(a, b: Rect) -> Rect {
+ a := a
+ a.l += b.l
+ a.t += b.t
+ a.r += b.r
+ a.b += b.b
+ return a
+}
+
+rect_translate :: proc(a, b: Rect) -> Rect {
+ a := a
+ a.l += b.l
+ a.t += b.t
+ a.r += b.l
+ a.b += b.t
+ return a
+}
+
+rect_overlap :: proc(a, b: Rect) -> bool {
+ return b.r >= a.l && b.l <= a.r && b.b >= a.t && b.t <= a.b
+}
+
+rect_inside :: proc(a, b: Rect) -> bool {
+ return b.r >= a.l && b.l <= a.r && b.t >= a.t && b.b <= a.b
+}
+
+// cuts out rect b from a and returns the left regions
+rect_cut_out_rect :: proc(a, b: Rect) -> (res: [4]Rect) {
+ // top
+ res[0] = a
+ res[0].b = b.t
+
+ // bottom
+ res[1] = a
+ res[1].t = b.b
+
+ // middle
+ last := rect_intersection(res[0], res[1])
+
+ // left
+ res[2] = last
+ res[2].r = b.l
+
+ // right
+ res[3] = last
+ res[3].l = b.r
+ return
+}