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.. ^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.. 0 { update_hot_item() } ui.stage = .Post_Layout // update hot/active for animation speed := f32(0.05) for i in 0.. 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..= 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) } }