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, Fractional, // 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 { ctx.buttons += {button} } else { if ctx.button_ignore != nil && (button in ctx.buttons) { ctx.button_ignore = nil } 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.. 0 { update_hot_item(ctx) } ctx.stage = .Post_Layout // update hot/active for animation speed := f32(0.50) for i in 0.. (res: ^Item) { for i in 0.. 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.. ^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) if kid.layout == .Fractional { kid.bounds.r = item.layout_size.x * kid.bounds.r } 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) case .Fractional: rect.sized(&item.bounds, 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..