From a7c2d13bcca784350d851b665b8a19697e113b34 Mon Sep 17 00:00:00 2001 From: San Jacobs Date: Thu, 17 Aug 2023 01:48:00 +0200 Subject: Screw everything, we do oui now --- lib/oui/oui.odin | 1186 +++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/oui/rect.odin | 245 +++++++++++ src/main.odin | 127 +++++- 3 files changed, 1556 insertions(+), 2 deletions(-) create mode 100644 lib/oui/oui.odin create mode 100644 lib/oui/rect.odin 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.. ^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) + } +} \ 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 +} diff --git a/src/main.odin b/src/main.odin index bcbaeec..17517c7 100644 --- a/src/main.odin +++ b/src/main.odin @@ -1,4 +1,5 @@ package main +import "../lib/oui" import "core:fmt" import "core:math" @@ -78,6 +79,11 @@ main :: proc() { big_font: Font = LoadFontEx("res/UbuntuMono-Regular.ttf", big_font_size, nil, 0) defer UnloadFont(big_font) + // oui stuff + + c0 := oui.context_create(1028, 1028 * 8) + defer oui.context_destroy(c0) + oui.context_make_current(c0) // Setting up the timelines @@ -116,13 +122,42 @@ main :: proc() { // clicking will put a white border around the timeblock, // and display information about the block in the // bottom left of the screen. - + + mousePosition: = rl.GetMousePosition() + oui.set_cursor(int(mousePosition.x), int(mousePosition.y)) + oui.set_button(.Left, rl.IsMouseButtonPressed(rl.MouseButton(0))) // DRAW // ------------------------------------------ BeginDrawing() ClearBackground(BGCOLOR) + + // hotloop + oui.begin_layout() + + a_panel := panel() + oui.set_layout(a_panel, .Absolute) + oui.set_size(a_panel, 200, 200) + oui.set_offset(a_panel, 20, 20) + + a_button := button("Testerino", 50) + if oui.latest_clicked() { + fmt.println("CLICKO BOIO") + } + oui.set_cut(a_panel, .Left) + oui.set_height(a_button, 50) + oui.set_offset(a_button, 20, 20) + + oui.item_insert(a_panel, a_button) + + oui.end_layout() + + ui_draw(0) + // DRAW HERE OR BELOW + oui.process() + + /* DrawTextEx(font, "Date", {20, 8}, font_size, 0, RAYWHITE); DrawTextEx(font, "Calltime", {105, 8}, font_size, 0, RAYWHITE); DrawTextEx(font, "Wraptime", {f32(width)-83, 8}, font_size, 0, RAYWHITE); @@ -176,7 +211,7 @@ main :: proc() { DrawTextEx(small_font, total_sum, {f32(width)-120, f32(height)-43}, small_font_size, 0, RAYWHITE); DrawTextEx(big_font, inc_soc, {f32(width)-120, f32(height)-29}, big_font_size, 0, RAYWHITE); - + */ EndDrawing() } } @@ -187,3 +222,91 @@ PBGCOLOR : rl.Color : {40, 40, 40, 255} DAY_HEIGHT :: 35 TIMELINE_START :: 175 TIMELINE_END :: -85 + +Item :: oui.Item +Call :: oui.Call + +Data_Element :: enum int { + Panel, + Button, + Text_Input, + Timeblock, +// ... +} + +Data_Head :: struct { + subtype: Data_Element, +} + +Data_Panel :: struct { + using _: Data_Head, +} + +Data_Button :: struct { + using _: Data_Head, + text: string, + selected: bool, +} + +button_callback :: proc(item: Item, event: Call) -> int { + data := cast(^Data_Button) oui.get_handle(item) + + #partial switch event { + case .Cursor_Handle: + //return int(Cursor_Type.Hand) + } + + return -1 +} + +panel :: proc() -> Item { + item := oui.item_make() + + data := oui.alloc_typed(item, Data_Panel) + data.subtype = .Panel + + return item +} + +button :: proc(text: string, width: int, selected := false) -> Item { + item := oui.item_make() + oui.set_size(item, width, 35) + oui.set_callback(item, button_callback) + + data := oui.alloc_typed(item, Data_Button) + data.subtype = .Button + data.text = text + data.selected = selected + + return item +} + +// recursive loop +ui_draw_children :: proc(item: oui.Item) { + list := oui.children_sorted(item) + for kid in list { + ui_draw(kid) + } +} + +ui_draw :: proc(item: oui.Item) { + head := cast(^Data_Head) oui.get_handle(item) + rect := oui.get_rect(item) + + //fmt.println(rect, head, item) + + if head == nil { + ui_draw_children(item) + return + } + + #partial switch head.subtype { + //case .Panel_Root: + // ... render any type of item + case .Button: + rl.DrawRectangle(i32(rect.l), i32(rect.t), i32(rect.r-rect.l), i32(rect.b-rect.t), PBGCOLOR) + case .Panel: + rl.DrawRectangle(i32(rect.l), i32(rect.t), i32(rect.r-rect.l), i32(rect.b-rect.t), rl.RED) + ui_draw_children(item) + } +} \ No newline at end of file -- cgit v1.2.1