aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--lib/oui/oui.odin1010
-rw-r--r--lib/rect/rect.odin133
-rw-r--r--makefile2
-rw-r--r--src/main.odin256
-rw-r--r--src/time.odin24
-rw-r--r--src/ui_implementation.odin389
7 files changed, 1774 insertions, 43 deletions
diff --git a/.gitignore b/.gitignore
index 44f6d15..63c70cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
__pycache__
obj/*.o
bin/
+res/test.ics
a.out
a.exe
main.exe
@@ -13,4 +14,4 @@ satscalc.exe
satscalc32.exe
notes.txt
*.pdb
-remedy.rdbg
+remedy.rdbg \ No newline at end of file
diff --git a/lib/oui/oui.odin b/lib/oui/oui.odin
new file mode 100644
index 0000000..a1fbbf8
--- /dev/null
+++ b/lib/oui/oui.odin
@@ -0,0 +1,1010 @@
+package oui
+
+import "core:mem"
+import "core:fmt"
+import "core:time"
+import "core:hash"
+import "core:slice"
+import "../rect"
+
+// LAYOUTING IS CUSTOMIZED
+// 1. RectCut Layouting (directionality + cut distance)
+// 2. Absolute Layouting (set the rect yourself)
+// 3. Relative Layouting (set the rect relative to a parent)
+// 4. Custom Layouting (callback - roll your own thing) // TODO
+
+MAX_DATASIZE :: 4096
+MAX_DEPTH :: 64
+MAX_INPUT_EVENTS :: 64
+CLICK_THRESHOLD :: time.Millisecond * 250
+MAX_CONSUME :: 256
+
+RectI :: rect.RectI
+I2 :: [2]int
+
+Input_Event :: struct {
+ key: string,
+ char: rune,
+ call: Call,
+}
+
+Mouse_Button :: enum {
+ Left,
+ Middle,
+ Right,
+}
+Mouse_Buttons :: bit_set[Mouse_Button]
+
+Context :: struct {
+ // userdata
+ user_ptr: rawptr,
+
+ // mouse state
+ buttons: Mouse_Buttons,
+ last_buttons: Mouse_Buttons,
+ button_capture: Mouse_Button,
+ button_ignore: Maybe(Mouse_Button),
+
+ // cursor
+ cursor_start: I2,
+ cursor_last_frame: I2,
+ cursor_delta_frame: I2,
+ cursor: I2,
+ cursor_handle: int, // handle <-> looks
+ cursor_handle_callback: proc(new: int), // callback on handle change
+ scroll: I2,
+
+ // item ids
+ active_item: u32,
+ focus_item: u32,
+ focus_redirect_item: u32, // redirect messages if no item is focused to this item
+ last_hot_item: u32,
+ last_click_item: u32,
+ hot_item: u32,
+ clicked_item: u32,
+
+ // stages
+ state: State,
+ stage: Stage,
+
+ // key info
+ active_key: string,
+ active_char: rune,
+
+ // consume building
+ consume: [MAX_CONSUME]u8,
+ consume_focused: bool, // wether consuming is active
+ consume_index: int,
+
+ // defaults
+ // escape_key: int,
+
+ // click counting
+ clicks: int,
+ click_time: time.Time,
+
+ // items data
+ items: []Item,
+ items_index: int,
+ items_last_index: int,
+ last_items: []Item,
+ item_map: map[u32]Animation,
+ item_sort: []^Item, // temp array for storing sorted results before clone
+
+ // buffer data
+ // TODO could be an arena
+ buffer: []byte,
+ buffer_index: int,
+
+ // input event buffer
+ events: [MAX_INPUT_EVENTS]Input_Event,
+ event_count: int,
+
+ // id stack
+ ids: [32]u32,
+ ids_index: int,
+ last_id: u32,
+}
+
+State :: enum {
+ Idle,
+ Capture,
+}
+
+Stage :: enum {
+ Layout,
+ Post_Layout,
+ Process,
+}
+
+Item_State :: enum {
+ Cold,
+ Hot,
+ Active,
+ Frozen,
+}
+
+// Cut modes
+Cut :: enum {
+ Left,
+ Right,
+ Top,
+ Bottom,
+ Fill,
+}
+
+// ratio of parent size
+Ratio :: enum {
+ None,
+ X,
+ Y,
+ XY,
+}
+
+// layout modes | Default is CUT
+Layout :: enum {
+ Cut,
+ Absolute,
+ Relative,
+ // Custom,
+}
+
+// MOUSE calls:
+// Down / Up called 1 frame
+// Hot_Up called when active was over hot
+// Capture called while holding active down
+Call :: enum {
+ // left mouse button
+ Left_Down,
+ Left_Up,
+ Left_Hot_Up,
+ Left_Capture,
+
+ // right mouse button
+ Right_Down,
+ Right_Up,
+ Right_Hot_Up,
+ Right_Capture,
+
+ // middle mouse button
+ Middle_Down,
+ Middle_Up,
+ Middle_Hot_Up,
+ Middle_Capture,
+
+ // Scroll info
+ Scroll,
+
+ // Cursor Info
+ Cursor_Handle,
+
+ // FIND
+ Find_Ignore,
+
+ // Key / Char
+ Key,
+ Char,
+}
+
+Callback :: proc(^Context, ^Item, Call) -> int
+
+Item :: struct {
+ handle: rawptr, // item handle
+ callback: Callback, // callback class that can be used to do things based on events
+
+ // unique id
+ id: u32,
+ sort_children: bool,
+
+ state: Item_State,
+ ignore: bool,
+
+ // item state | makes this item UNIQUE
+ layout: Layout,
+ ratio: Ratio,
+ z_index: int,
+
+ // tree info
+ parent: ^Item,
+ first_item: ^Item,
+ next_item: ^Item,
+
+ // layout final
+ bounds: RectI,
+
+ // layout info
+ layout_offset: [2]int,
+ layout_margin: int,
+ layout_size: [2]int, // width/height
+ layout_ratio: [2]f32, // TODO could be merged to somewhere else
+
+ // cut info
+ layout_cut_self: Cut, // how the item will cut from the rect
+ layout_cut_children: Cut, // state that the children will inherit
+ layout_cut_gap: int, // how much to insert a gap after a cut
+
+ // persistent data per item - queryable after end_layout
+ anim: Animation,
+}
+
+Animation :: struct {
+ hot: f32,
+ active: f32,
+ trigger: f32,
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// CONTEXT MANAGEMENT
+////////////////////////////////////////////////////////////////////////////////
+
+@private
+context_clear_data :: proc(ctx: ^Context) #no_bounds_check {
+ ctx.items_last_index = ctx.items_index
+ ctx.items_index = 0
+ ctx.buffer_index = 0
+ ctx.hot_item = 0
+ ctx.ids_index = 0
+
+ // swap buffers
+ ctx.items, ctx.last_items = ctx.last_items, ctx.items
+}
+
+context_init :: proc(ctx: ^Context, item_capacity, buffer_capacity: int) {
+ ctx.buffer = make([]byte, buffer_capacity)
+ ctx.items = make([]Item, item_capacity)
+ ctx.last_items = make([]Item, item_capacity)
+ ctx.item_sort = make([]^Item, item_capacity)
+ ctx.item_map = make(map[u32]Animation, item_capacity)
+
+ ctx.stage = .Process
+
+ context_clear_data(ctx)
+ context_clear_state(ctx)
+}
+
+context_destroy :: proc(ctx: ^Context) {
+ delete(ctx.buffer)
+ delete(ctx.items)
+ delete(ctx.last_items)
+ delete(ctx.item_map)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// INPUT CONTROL
+////////////////////////////////////////////////////////////////////////////////
+
+@private
+add_input_event :: proc(ctx: ^Context, event: Input_Event) #no_bounds_check {
+ if ctx.event_count == MAX_INPUT_EVENTS {
+ return
+ }
+
+ ctx.events[ctx.event_count] = event
+ ctx.event_count += 1
+}
+
+@private
+clear_input_events :: proc(ctx: ^Context) {
+ ctx.event_count = 0
+ ctx.scroll = {}
+}
+
+set_cursor :: #force_inline proc(ctx: ^Context, x, y: int) {
+ ctx.cursor = { x, y }
+}
+
+set_cursor_handle_callback :: proc(ctx: ^Context, callback: proc(new: int)) {
+ ctx.cursor_handle_callback = callback
+}
+
+get_cursor :: #force_inline proc(ctx: ^Context) -> I2 {
+ return ctx.cursor
+}
+
+get_cursor_start :: #force_inline proc(ctx: ^Context) -> I2 {
+ return ctx.cursor_start
+}
+
+get_cursor_delta :: #force_inline proc(ctx: ^Context) -> I2 {
+ return ctx.cursor - ctx.cursor_start
+}
+
+get_cursor_delta_frame :: #force_inline proc(ctx: ^Context) -> I2 {
+ return ctx.cursor_delta_frame
+}
+
+set_button :: proc(ctx: ^Context, button: Mouse_Button, enabled: bool) {
+ if enabled {
+ incl(&ctx.buttons, button)
+ } else {
+ if ctx.button_ignore != nil && (button in ctx.buttons) {
+ ctx.button_ignore = nil
+ }
+
+ excl(&ctx.buttons, button)
+ }
+}
+
+button_pressed :: proc(ctx: ^Context, button: Mouse_Button) -> bool {
+ return button not_in ctx.last_buttons && button in ctx.buttons
+}
+
+button_released :: proc(ctx: ^Context, button: Mouse_Button) -> bool {
+ return button in ctx.last_buttons && button not_in ctx.buttons
+}
+
+get_clicks :: #force_inline proc(ctx: ^Context) -> int {
+ return ctx.clicks
+}
+
+set_key :: proc(ctx: ^Context, key: string) {
+ add_input_event(ctx, { key, 0, .Key })
+}
+
+set_char :: proc(ctx: ^Context, value: rune) {
+ add_input_event(ctx, { "", value, .Char })
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// STAGES
+////////////////////////////////////////////////////////////////////////////////
+
+begin_layout :: proc(ctx: ^Context) {
+ assert(ctx.stage == .Process)
+ context_clear_data(ctx)
+ ctx.stage = .Layout
+}
+
+end_layout :: proc(ctx: ^Context) #no_bounds_check {
+ assert(ctx.stage == .Layout)
+
+ if ctx.items_index > 0 {
+ root := item_root(ctx)
+ compute_size(root)
+ arrange(root, nil, 0)
+
+ if ctx.items_last_index > 0 {
+ // map old item content
+ clear(&ctx.item_map)
+ for i in 0..<ctx.items_last_index {
+ item := ctx.last_items[i]
+
+ if item.id != 0 {
+ ctx.item_map[item.id] = item.anim
+ }
+ }
+
+ // map new content
+ for i in 0..<ctx.items_index {
+ item := &ctx.items[i]
+
+ if item.id != 0 {
+ if old, ok := ctx.item_map[item.id]; ok {
+ item.anim = old
+ }
+ }
+ }
+ }
+ }
+
+ // validate_state_items(ctx)
+ if ctx.items_index > 0 {
+ update_hot_item(ctx)
+ }
+
+ ctx.stage = .Post_Layout
+
+ // update hot/active for animation
+ speed := f32(0.50)
+ for i in 0..<ctx.items_index {
+ p := &ctx.items[i]
+ p.anim.hot = clamp(p.anim.hot + (p.id == ctx.hot_item ? speed : -speed), 0, 1)
+ p.anim.active = clamp(p.anim.active + (p.id == ctx.active_item ? speed : -speed), 0, 1)
+ p.anim.trigger = max(p.anim.trigger - speed, 0)
+ }
+}
+
+update_hot_item :: proc(ctx: ^Context) {
+ if ctx.items_index == 0 {
+ return
+ }
+
+ item := find_item(ctx, item_root(ctx), ctx.cursor.x, ctx.cursor.y)
+ ctx.hot_item = item.id
+}
+
+// TODO this is shit
+temp_find :: proc(ctx: ^Context, id: u32) -> (res: ^Item) {
+ for i in 0..<ctx.items_index {
+ item := &ctx.items[i]
+ if item.id == id {
+ res = item
+ break
+ }
+ }
+
+ return
+}
+
+process_button :: proc(
+ ctx: ^Context,
+ button: Mouse_Button,
+ hot_item: ^u32,
+ active_item: ^u32,
+ focus_item: ^u32,
+) -> bool {
+ if button in ctx.buttons {
+ hot_item^ = 0
+ active_item^ = ctx.hot_item
+
+ if active_item^ != focus_item^ {
+ focus_item^ = 0
+ ctx.focus_item = 0
+ }
+
+ capture := -1
+ if active_item^ != 0 {
+ diff := time.since(ctx.click_time)
+ if diff > CLICK_THRESHOLD {
+ ctx.clicks = 0
+ }
+
+ ctx.clicks += 1
+ ctx.last_click_item = active_item^
+ ctx.click_time = time.now()
+
+ active := temp_find(ctx, active_item^)
+ switch button {
+ case .Left: capture = item_callback(ctx, active, .Left_Down)
+ case .Middle: capture = item_callback(ctx, active, .Middle_Down)
+ case .Right: capture = item_callback(ctx, active, .Right_Down)
+ }
+ }
+
+ // only capture if wanted
+ if capture != -1 || ctx.button_ignore != nil {
+ ctx.button_ignore = button
+ active_item^ = 0
+ focus_item^ = 0
+ return false
+ } else {
+ ctx.button_capture = button
+ ctx.state = .Capture
+ return true
+ }
+ }
+
+ return false
+}
+
+process :: proc(ctx: ^Context) {
+ assert(ctx.stage != .Layout)
+ if ctx.stage == .Process {
+ update_hot_item(ctx)
+ }
+ ctx.stage = .Process
+
+ if ctx.items_index == 0 {
+ clear_input_events(ctx)
+ return
+ }
+
+ hot_item := ctx.last_hot_item
+ active_item := ctx.active_item
+ focus_item := ctx.focus_item
+ cursor_handle := ctx.cursor_handle
+
+ // send all keyboard events
+ if focus_item != 0 {
+ for i in 0..<ctx.event_count {
+ event := ctx.events[i]
+ ctx.active_key = event.key
+ ctx.active_char = event.char
+
+ // consume char calls when active
+ if event.call == .Char && ctx.consume_focused {
+ if ctx.consume_index < MAX_CONSUME {
+ // TODO proper utf8 insertion
+ ctx.consume[ctx.consume_index] = u8(event.char)
+ ctx.consume_index += 1
+ }
+ } else {
+ focus := temp_find(ctx, focus_item)
+ item_callback(ctx, focus, event.call)
+ }
+
+ // TODO this
+ // // check for escape
+ // if event.key == ctx.escape_key {
+ // ctx.focus_item = nil
+ // }
+ }
+ } else {
+ ctx.focus_item = 0
+ }
+
+ // use redirect instead
+ if focus_item == 0 {
+ item := temp_find(ctx, ctx.focus_redirect_item)
+
+ for i in 0..<ctx.event_count {
+ event := ctx.events[i]
+ ctx.active_key = event.key
+ ctx.active_char = event.char
+ item_callback(ctx, item, event.call)
+ }
+ }
+
+ // apply scroll callback
+ if ctx.scroll != {} {
+ item := temp_find(ctx, ctx.hot_item)
+ item_callback(ctx, item, .Scroll)
+ }
+
+ clear_input_events(ctx)
+
+ hot := ctx.hot_item
+ ctx.clicked_item = 0
+
+ switch ctx.state {
+ case .Idle:
+ ctx.cursor_start = ctx.cursor
+
+ left := process_button(ctx, .Left, &hot_item, &active_item, &focus_item)
+ middle := process_button(ctx, .Middle, &hot_item, &active_item, &focus_item)
+ right := process_button(ctx, .Right, &hot_item, &active_item, &focus_item)
+
+ if !left && !right && !middle {
+ hot_item = hot
+ }
+
+ case .Capture:
+ if ctx.button_capture not_in ctx.buttons {
+ if active_item != 0 {
+ active := temp_find(ctx, active_item)
+ switch ctx.button_capture {
+ case .Left: item_callback(ctx, active, .Left_Up)
+ case .Middle: item_callback(ctx, active, .Middle_Up)
+ case .Right: item_callback(ctx, active, .Right_Up)
+ }
+
+ if active_item == hot {
+ switch ctx.button_capture {
+ case .Left: item_callback(ctx, active, .Left_Hot_Up)
+ case .Middle: item_callback(ctx, active, .Middle_Hot_Up)
+ case .Right: item_callback(ctx, active, .Right_Hot_Up)
+ }
+ ctx.clicked_item = active_item
+ }
+ }
+
+ active_item = 0
+ ctx.state = .Idle
+ } else {
+ active := temp_find(ctx, active_item)
+ if active_item != 0 {
+ switch ctx.button_capture {
+ case .Left: item_callback(ctx, active, .Left_Capture)
+ case .Middle: item_callback(ctx, active, .Middle_Capture)
+ case .Right: item_callback(ctx, active, .Right_Capture)
+ }
+ }
+
+ hot_item = hot == active_item ? hot : 0
+ }
+ }
+
+ // look for possible cursor handle
+ if hot_item != 0 {
+ hot := temp_find(ctx, hot_item)
+ wanted_handle := item_callback(ctx, hot, .Cursor_Handle)
+ if wanted_handle != -1 {
+ ctx.cursor_handle = wanted_handle
+ } else {
+ // change back to zero - being the default arrow type
+ if ctx.cursor_handle != 0 {
+ ctx.cursor_handle = 0
+ }
+ }
+ }
+
+ // change of cursor handle
+ if cursor_handle != ctx.cursor_handle {
+ if ctx.cursor_handle_callback != nil {
+ ctx.cursor_handle_callback(ctx.cursor_handle)
+ }
+ }
+
+ ctx.cursor_delta_frame = ctx.cursor_last_frame - ctx.cursor
+ ctx.cursor_last_frame = ctx.cursor
+ ctx.last_hot_item = hot_item
+ ctx.active_item = active_item
+ ctx.last_buttons = ctx.buttons
+}
+
+context_clear_state :: proc(ctx: ^Context) {
+ ctx.last_hot_item = 0
+ ctx.active_item = 0
+ ctx.focus_item = 0
+ ctx.last_click_item = 0
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// UI DECLARATION
+////////////////////////////////////////////////////////////////////////////////
+
+item_make :: proc(ctx: ^Context) -> ^Item {
+ assert(ctx.stage == .Layout)
+ assert(ctx.items_index < len(ctx.items))
+
+ item := &ctx.items[ctx.items_index]
+ ctx.items_index += 1
+ mem.zero_item(item)
+
+ item.first_item = nil
+ item.next_item = nil
+
+ return item
+}
+
+item_callback :: proc(ctx: ^Context, item: ^Item, call: Call) -> int #no_bounds_check {
+ if item == nil || item.callback == nil {
+ return -1
+ }
+
+ return item.callback(ctx, item, call)
+}
+
+set_frozen :: proc(item: ^Item, enable: bool) {
+ if enable {
+ item.state = .Frozen
+ } else {
+ item.state = .Cold
+ }
+}
+
+alloc_handle :: proc(ctx: ^Context, item: ^Item, size: int, loc := #caller_location) -> rawptr {
+ assert(item.handle == nil)
+ assert(ctx.buffer_index + size <= len(ctx.buffer))
+ item.handle = &ctx.buffer[ctx.buffer_index]
+ ctx.buffer_index += size
+ return item.handle
+}
+
+alloc_typed :: proc(ctx: ^Context, item: ^Item, $T: typeid) -> ^T {
+ return cast(^T) alloc_handle(ctx, item, size_of(T))
+}
+
+item_append :: proc(item, sibling: ^Item) -> ^Item {
+ sibling.parent = item.parent
+ // TODO cut append
+ sibling.next_item = item.next_item
+ item.next_item = sibling
+ return sibling
+}
+
+item_insert :: proc(parent, child: ^Item) -> ^Item {
+ // set cut direction
+ child.parent = parent
+ child.layout_cut_self = parent.layout_cut_children
+
+ if parent.first_item == nil {
+ parent.first_item = child
+ } else {
+ item_append(last_child(parent), child)
+ }
+
+ return child
+}
+
+insert_front :: item_insert
+
+item_insert_back :: proc(parent, child: ^Item) -> ^Item {
+ child.parent = parent
+ child.layout_cut_self = parent.layout_cut_children
+
+ child.next_item = parent.first_item
+ parent.first_item = child
+ return child
+}
+
+set_ratio :: proc(item: ^Item, width, height: f32) {
+ w := clamp(width, 0, 1)
+ h := clamp(height, 0, 1)
+ item.layout_ratio = { w, h }
+
+ if w != 0 && h != 0 {
+ item.ratio = .XY
+ } else {
+ if w != 0 {
+ item.ratio = .X
+ } else if h != 0 {
+ item.ratio = .Y
+ }
+ }
+}
+
+focus :: proc(ctx: ^Context, item: ^Item, consume := false) {
+ assert(item != nil && uintptr(item) < uintptr(&ctx.items[ctx.items_index]))
+ assert(ctx.stage != .Layout)
+ ctx.focus_item = item.id
+ ctx.consume_focused = consume
+ ctx.consume_index = 0
+}
+
+consume_result :: proc(ctx: ^Context) -> string {
+ return transmute(string) mem.Raw_String { &ctx.consume[0], ctx.consume_index }
+}
+
+consume_decrease :: proc(ctx: ^Context) {
+ if ctx.consume_index > 0 {
+ ctx.consume_index -= 1
+ }
+}
+
+focus_redirect :: proc(ctx: ^Context, item: ^Item) {
+ ctx.focus_redirect_item = item.id
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ITERATION
+////////////////////////////////////////////////////////////////////////////////
+
+last_child :: proc(item: ^Item) -> ^Item {
+ item := item.first_item
+
+ for item != nil {
+ next_item := item.next_item
+ if next_item == nil {
+ return item
+ }
+ item = next_item
+ }
+
+ return nil
+}
+
+// NOTE: uses temp allocator, since item_sort gets reused and the output needs to be stable!
+// return z sorted list of children
+children_list :: proc(ctx: ^Context, item: ^Item) -> []^Item {
+ count: int
+
+ // loop through children and push items
+ kid := item.first_item
+ for kid != nil {
+ ctx.item_sort[count] = kid
+ kid = kid.next_item
+ count += 1
+ }
+
+ list := ctx.item_sort[:count]
+
+ // optionally sort the children by z_index
+ if item.sort_children {
+ // sort and return a cloned list
+ slice.sort_by(list, proc(a, b: ^Item) -> bool {
+ return a.z_index < b.z_index
+ })
+ }
+
+ return slice.clone(list, context.temp_allocator)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// QUERYING
+////////////////////////////////////////////////////////////////////////////////
+
+find_item :: proc(
+ ctx: ^Context,
+ item: ^Item,
+ x, y: int,
+ loc := #caller_location,
+) -> ^Item #no_bounds_check {
+ // TODO frozen
+ // if pitem.state == .Frozen {
+ // return -1
+ // }
+
+ list := children_list(ctx, item)
+ for kid in list {
+ // fetch ignore status
+ ignore := kid.ignore
+ if item_callback(ctx, kid, .Find_Ignore) >= 0 {
+ ignore = true
+ }
+
+ if !ignore && rect.contains(kid.bounds, x, y) {
+ return find_item(ctx, kid, x, y)
+ }
+ }
+
+ return item
+}
+
+get_key :: proc(ctx: ^Context) -> string {
+ return ctx.active_key
+}
+
+get_char :: proc(ctx: ^Context) -> rune {
+ return ctx.active_char
+}
+
+contains :: proc(item: ^Item, x, y: int) -> bool #no_bounds_check {
+ return rect.contains(item.bounds, x, y)
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// OTHER
+////////////////////////////////////////////////////////////////////////////////
+
+// true if the item match the active
+is_active :: #force_inline proc(ctx: ^Context, item: ^Item) -> bool {
+ return ctx.active_item == item.id
+}
+
+// true if the item match the hot
+is_hot :: #force_inline proc(ctx: ^Context, item: ^Item) -> bool {
+ return ctx.last_hot_item == item.id
+}
+
+// true if the item match the focused
+is_focused :: #force_inline proc(ctx: ^Context, item: ^Item) -> bool {
+ return ctx.focus_item == item.id
+}
+
+// true if the item match the clicked (HOT_UP)
+is_clicked :: #force_inline proc(ctx: ^Context, item: ^Item) -> bool {
+ return ctx.clicked_item == item.id
+}
+
+// shorthand for is_clicked(get_latest())
+latest_clicked :: #force_inline proc(ctx: ^Context) -> bool {
+ item := &ctx.items[ctx.items_index - 1]
+ return ctx.clicked_item == item.id
+}
+
+// float activeness of an item 0 | 0.5 | 1
+activeness :: proc(ctx: ^Context, item: ^Item) -> f32 {
+ return ctx.active_item == item.id ? 1 : (ctx.last_hot_item == item.id ? 0.5 : 0)
+}
+
+// hot + active activeness
+activeness2 :: proc(item: ^Item) -> f32 #no_bounds_check {
+ return max(item.anim.hot * 0.5, item.anim.active)
+}
+
+// compute the size of an item
+// optional HSIZED / VSIZED for custom sizes
+compute_size :: proc(item: ^Item) #no_bounds_check {
+ item.bounds.r = item.layout_size.x
+ item.bounds.b = item.layout_size.y
+
+ // iterate children
+ kid := item.first_item
+ for kid != nil {
+ compute_size(kid)
+ kid = kid.next_item
+ }
+}
+
+// layouts items based on rect-cut by default or custom ones
+arrange :: proc(item: ^Item, layout: ^RectI, gap: int) #no_bounds_check {
+ // check for wanted ratios -> size conversion which depend on parent rect
+ switch item.ratio {
+ case .None:
+ case .X: item.layout_size.x = int(rect.widthf(layout^) * item.layout_ratio.x)
+ case .Y: item.layout_size.y = int(rect.heightf(layout^) * item.layout_ratio.y)
+ case .XY:
+ item.layout_size.x = int(rect.widthf(layout^) * item.layout_ratio.x)
+ item.layout_size.y = int(rect.heightf(layout^) * item.layout_ratio.y)
+ }
+
+ switch item.layout {
+ // DEFAULT
+ case .Cut:
+ // directionality
+ switch item.layout_cut_self {
+ case .Left: item.bounds = rect.cut_left(layout, item.layout_size.x)
+ case .Right: item.bounds = rect.cut_right(layout, item.layout_size.x)
+ case .Top: item.bounds = rect.cut_top(layout, item.layout_size.y)
+ case .Bottom: item.bounds = rect.cut_bottom(layout, item.layout_size.y)
+ case .Fill:
+ item.bounds = layout^
+ layout^ = {}
+ }
+
+ // apply gapping
+ if gap > 0 {
+ switch item.layout_cut_self {
+ case .Left: layout.l += gap
+ case .Right: layout.r -= gap
+ case .Top: layout.t += gap
+ case .Bottom: layout.b -= gap
+ case .Fill:
+ }
+ }
+
+ // case .Custom:
+ // item_callback(item, LAYOUT_CUSTOM)
+
+ case .Absolute:
+ rect.sized(&item.bounds, item.layout_offset, item.layout_size)
+
+ case .Relative:
+ rect.sized(&item.bounds, [2]int { layout.l, layout.t } + item.layout_offset, item.layout_size)
+ }
+
+ // layout children with this resultant rect for LAYOUT_CUT
+ layout_with := item.bounds
+ kid := item.first_item
+
+ if item.layout_margin > 0 {
+ layout_with = rect.margin(layout_with, item.layout_margin)
+ }
+
+ for kid != nil {
+ arrange(kid, &layout_with, item.layout_cut_gap)
+ kid = kid.next_item
+ }
+}
+
+item_root :: proc(ctx: ^Context) -> ^Item {
+ assert(ctx.items_index > 0)
+ return &ctx.items[0]
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// ID gen - same as microui
+////////////////////////////////////////////////////////////////////////////////
+
+gen_id_bytes :: proc(ctx: ^Context, input: []byte) -> (res: u32) {
+ seed := ctx.ids_index > 0 ? ctx.ids[ctx.ids_index - 1] : 2166136261
+ res = hash.fnv32a(input, seed)
+ ctx.last_id = res
+ return
+}
+gen_id_string :: proc(ctx: ^Context, input: string) -> u32 {
+ return gen_id_bytes(ctx, transmute([]byte) input)
+}
+gen_id :: proc { gen_id_bytes, gen_id_string }
+
+gen_idf :: proc(ctx: ^Context, format: string, args: ..any) -> u32 {
+ return gen_id_bytes(ctx, transmute([]byte) fmt.tprintf(format, ..args))
+}
+
+push_id_bytes :: proc(ctx: ^Context, input: []byte) -> (res: u32) {
+ res = gen_id_bytes(ctx, input)
+ ctx.ids[ctx.ids_index] = res
+ ctx.ids_index += 1
+ return
+}
+push_id_string :: proc(ctx: ^Context, input: string) -> (res: u32) {
+ res = gen_id_string(ctx, input)
+ ctx.ids[ctx.ids_index] = res
+ ctx.ids_index += 1
+ return
+}
+push_id :: proc { push_id_bytes, push_id_string }
+
+// push_id_latest :: proc(ctx: ^Context) {
+// ctx.ids[ctx.ids_index] = ctx.last_id
+// ctx.ids_index += 1
+// }
+
+pop_id :: proc(ctx: ^Context) {
+ if ctx.ids_index > 0 {
+ ctx.ids_index -= 1
+ }
+}
+
+print_ids :: proc(ctx: ^Context) {
+ fmt.eprintln("~~~~~~~~~~")
+ for i in 0..<ctx.ids_index {
+ for j in 0..<i {
+ fmt.eprint('\t')
+ }
+
+ id := ctx.ids[i]
+ fmt.eprintf("ID %d\n", id)
+ }
+} \ No newline at end of file
diff --git a/lib/rect/rect.odin b/lib/rect/rect.odin
new file mode 100644
index 0000000..197a966
--- /dev/null
+++ b/lib/rect/rect.odin
@@ -0,0 +1,133 @@
+package rect
+
+Rect :: struct($T: typeid) {
+ l, r, t, b: T,
+}
+
+RECTF_INF :: RectF { max(f32), -max(f32), max(f32), -max(f32) }
+
+RectF :: Rect(f32)
+RectI :: Rect(int)
+
+i2f :: proc(rect: RectI) -> RectF {
+ return { f32(rect.l), f32(rect.r), f32(rect.t), f32(rect.b) }
+}
+
+wh :: proc(x, y, w, h: $T) -> (res: Rect(T)) {
+ res.l = x
+ res.r = x + w
+ res.t = y
+ res.b = y + h
+ return
+}
+
+sized :: proc(rect: ^Rect($T), pos: [2]T, size: [2]T) {
+ rect.l = pos.x
+ rect.r = pos.x + size.x
+ rect.t = pos.y
+ rect.b = pos.y + size.y
+}
+
+overlap :: proc(a, b: Rect($T)) -> bool {
+ return b.r >= a.l && b.l <= a.r && b.b >= a.t && b.t <= a.b
+}
+
+inf_push :: proc(rect: ^Rect($T), pos: [2]T) {
+ rect.l = min(rect.l, pos.x)
+ rect.r = max(rect.r, pos.x)
+ rect.t = min(rect.t, pos.y)
+ rect.b = max(rect.b, pos.y)
+}
+
+// center :: proc(rect: Rect($T)) -> [2]T {
+// return { f32(rect.l) + f32(rect.r - rect.l) / 2, f32(rect.t) + f32(rect.b - rect.t) / 2 }
+// }
+
+valid :: proc(rect: Rect($T)) -> bool {
+ return (rect.r - rect.l) > 0 && (rect.b - rect.t) > 0
+}
+
+invalid :: #force_inline proc(rect: Rect($T)) -> bool { return !valid(rect) }
+
+intersection :: proc(a, b: Rect($T)) -> (res: Rect(T)) {
+ res = a
+ if a.l < b.l do res.l = b.l
+ if a.t < b.t do res.t = b.t
+ if a.r > b.r do res.r = b.r
+ if a.b > b.b do res.b = b.b
+ return
+}
+
+margin :: proc(a: Rect($T), value: T) -> Rect(T) {
+ a := a
+ a.l += value
+ a.t += value
+ a.r -= value
+ a.b -= value
+ return a
+}
+
+offset :: proc(rect: Rect($T), x, y: T) -> (res: Rect(T)) {
+ res = rect
+ res.l += x
+ res.r += x
+ res.t += y
+ res.b += y
+ return
+}
+
+contains :: proc(a: Rect($T), x, y: T) -> bool {
+ return a.l <= x && a.r > x && a.t <= y && a.b > y
+}
+
+widthf :: proc(a: RectI) -> f32 {
+ return f32(a.r - a.l)
+}
+
+heightf :: proc(a: RectI) -> f32 {
+ return f32(a.b - a.t)
+}
+
+widthi :: proc(a: RectI) -> int {
+ return int(a.r - a.l)
+}
+
+heighti :: proc(a: RectI) -> int {
+ return int(a.b - a.t)
+}
+
+splitv :: proc(rect: RectF) -> (left, right: RectF) {
+ left = rect
+ right = rect
+ left.r = rect.l + (rect.r - rect.l) / 2
+ right.l = left.r
+ return
+}
+
+cut_left :: proc(rect: ^Rect($T), a: T) -> (res: Rect(T)) {
+ res = rect^
+ res.r = rect.l + a
+ rect.l = res.r
+ return
+}
+
+cut_right :: proc(rect: ^Rect($T), a: T) -> (res: Rect(T)) {
+ res = rect^
+ res.l = rect.r - a
+ rect.r = res.l
+ return
+}
+
+cut_top :: proc(rect: ^Rect($T), a: T) -> (res: Rect(T)) {
+ res = rect^
+ res.b = rect.t + a
+ rect.t = res.b
+ return
+}
+
+cut_bottom :: proc(rect: ^Rect($T), a: T) -> (res: Rect(T)) {
+ res = rect^
+ res.t = rect.b - a
+ rect.b = res.t
+ return
+}
diff --git a/makefile b/makefile
index f9f3042..ec24455 100644
--- a/makefile
+++ b/makefile
@@ -1,2 +1,2 @@
main:
- odin run src/ -out:main.out
+ odin run src/ -debug -out:main.out
diff --git a/src/main.odin b/src/main.odin
index 8199e65..698f044 100644
--- a/src/main.odin
+++ b/src/main.odin
@@ -1,11 +1,24 @@
package main
+import "../lib/oui"
import "core:fmt"
import "core:math"
import "core:slice"
+import "core:runtime"
import "core:strings"
import rl "vendor:raylib"
+UBUNTU_MONO := #load("../res/UbuntuMono-Regular.ttf")
+
+font : rl.Font
+big_font : rl.Font
+small_font : rl.Font
+
+FRACT_MIN : f32 = 0.0
+FRACT_MAX : f32 = 1.0
+
+c0 : ^oui.Context
+
main :: proc() {
// TODO: Replace the dynamic array of Workday-pointers with
@@ -92,18 +105,20 @@ main :: proc() {
SetWindowMinSize(width, height)
// Loading fonts - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- font_size :: 18
- font: Font = LoadFontEx("res/UbuntuMono-Regular.ttf", font_size, nil, 0)
- defer UnloadFont(font)
-
- small_font_size :: 14
- small_font: Font = LoadFontEx("res/UbuntuMono-Regular.ttf", small_font_size, nil, 0)
- defer UnloadFont(small_font)
- big_font_size :: 24
- big_font: Font = LoadFontEx("res/UbuntuMono-Regular.ttf", big_font_size, nil, 0)
+ small_font = LoadFontFromMemory(".ttf", raw_data(UBUNTU_MONO), i32(len(UBUNTU_MONO)), 14, nil, 0)
+ font = LoadFontFromMemory(".ttf", raw_data(UBUNTU_MONO), i32(len(UBUNTU_MONO)), 18, nil, 0)
+ big_font = LoadFontFromMemory(".ttf", raw_data(UBUNTU_MONO), i32(len(UBUNTU_MONO)), 24, nil, 0)
defer UnloadFont(big_font)
+ defer UnloadFont(font)
+ defer UnloadFont(small_font)
+
+ // oui stuff
+ c0 = new(oui.Context)
+ defer free(c0)
+ oui.context_init(c0, 2048, 2048 * 8)
+ defer oui.context_destroy(c0)
// Setting up the timelines
@@ -122,6 +137,8 @@ main :: proc() {
for !WindowShouldClose() { // MAIN LOOP ---- MAIN LOOP ---- MAIN LOOP ---- MAIN LOOP
+ free_all(context.temp_allocator)
+
if IsWindowResized() {
height = GetScreenHeight()
width = GetScreenWidth()
@@ -142,16 +159,194 @@ 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(c0, int(mousePosition.x), int(mousePosition.y))
+ if rl.IsMouseButtonPressed(rl.MouseButton(0)) do oui.set_button(c0, .Left, true)
+ if rl.IsMouseButtonReleased(rl.MouseButton(0)) do oui.set_button(c0, .Left, false)
// DRAW
// ------------------------------------------
BeginDrawing()
- ClearBackground(BGCOLOR)
- 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);
+ ClearBackground(rl.RED)
+
+when true {
+ // hotloop
+ oui.begin_layout(c0)
+
+
+ master_container := panel()
+ master_container.id = oui.push_id(c0, "big_mr_boss_man") // Make ID for master thing just because.
+ // Does not need to be freed because master.
+ master_container.layout = .Absolute
+ master_container.sort_children = true
+ master_container.layout_size = {int(GetScreenWidth()), int(GetScreenHeight())}
+
+ {
+ top_bar := panel_line(master_container, theme.background_bar, 30)
+ top_bar.id = oui.push_id(c0, "top_bar") // Make ID for anything that will have children.
+ defer oui.pop_id(c0) // They must be pop'ed before the next equal-in-hiarchy item
+ top_bar.layout_margin = 10
+
+ oui.item_insert(top_bar, label("Date", font, sizings.date, .Center))
+ oui.item_insert(top_bar, label("Calltimes", font, sizings.call, .Center))
+
+ top_bar.layout_cut_children = .Right
+ oui.item_insert(top_bar, label("Price", font, sizings.price, .Center))
+ oui.item_insert(top_bar, label("Wrap", font, sizings.wrap, .Center))
+
+ top_bar.layout_cut_children = .Fill
+ oui.item_insert(top_bar, label("Timeline", font, 0, .Center))
+ }
+
+ {
+ bottom_bar := panel(theme.background_bar)
+ bottom_bar.id = oui.push_id(c0, "bottom_bar")
+ defer oui.pop_id(c0)
+ bottom_bar.layout_cut_children = .Left
+ master_container.layout_cut_children = .Bottom
+ bottom_bar.z_index = 10 // Makes this render over/after the middle section
+ bottom_bar.layout_size.y = 50
+ bottom_bar.layout_margin = 10 // Spacing from edges
+ bottom_bar.layout_cut_gap = 5 // Spacing between children
+ oui.item_insert(master_container, bottom_bar)
+
+ price_reason := label("Reason for price of highlighted timeblock", font, 300,)
+ bottom_bar.layout_cut_children = .Left
+ oui.item_insert(bottom_bar, price_reason)
+
+ totals := panel(theme.background_bar)
+ totals.layout_cut_children = .Top
+ totals.layout_size.x = 50
+ bottom_bar.layout_cut_children = .Right
+ oui.item_insert(bottom_bar, totals)
+
+ pre_sos_price := label("120 000 kr", small_font, 0, .Right)
+ pre_sos_price.layout_size.y = 11
+ oui.item_insert(totals, pre_sos_price)
+
+ post_sos_price := label("160 000 kr", big_font, 300, .Right)
+ post_sos_price.layout_size.y = 27
+ oui.item_insert(totals, post_sos_price)
+ }
+
+ {
+ middle_section := panel(theme.background)
+ middle_section.id = oui.push_id(c0, "middle_section")
+ defer oui.pop_id(c0)
+ middle_section.layout_margin = 10 // Spacing from edges
+ middle_section.layout_cut_gap = sizings.inter_timeline // Spacing between children
+ master_container.layout_cut_children = .Fill
+ oui.item_insert(master_container, middle_section)
+
+
+ // - - - - WORKDAYS - - - -
+
+ FRACT_MAX = workdays[0].fractions[0].start
+ FRACT_MIN = FRACT_MAX // TODO: Optimize this. It doesn't need to re-calculated every frame
+
+ for day, i in &workdays {
+
+ for fract, i in day.fractions {
+ if fract.start < FRACT_MIN do FRACT_MIN = fract.start
+ if fract.end > FRACT_MAX do FRACT_MAX = fract.end
+ if i+1 == day.total_timeblocks do break
+ }
+
+ line := panel_line(middle_section, theme.background)
+ line.layout_cut_children = .Left
+ line.layout_cut_gap = 0
+ line.layout_margin = 0
+ line.layout_size.y = sizings.timeline
+ oui.item_insert(line, label(dayprint(day.call), font, sizings.date, .Center))
+ oui.item_insert(line, label(clockprint(day.call), font, sizings.call, .Center))
+
+ line.layout_cut_children = .Right
+ oui.item_insert(line, label("3500 kr", font, sizings.price, .Center))
+ oui.item_insert(line, label(clockprint(day.wrap), font, sizings.wrap, .Center))
+
+ line.layout_cut_children = .Fill
+ timeline(line, &day)
+ }
+ new_workday := button("+", 100)
+ middle_section.layout_cut_children = .Top
+ oui.item_insert(middle_section, new_workday)
+ if oui.is_clicked(c0, new_workday) do fmt.println("NEW WORKDAY!")
+
+
+
+ // - - - - SIZINGS EDITOR - - - -
+ {
+ oui.item_insert(middle_section, label("Sizings Editor", big_font, 100, .Center))
+
+ // To loop over the members of a struct you need to do this goofy shit:
+ info := runtime.type_info_base(type_info_of(Sizings))
+ st := info.variant.(runtime.Type_Info_Struct)
+ root := uintptr(&sizings)
+ for offset, i in st.offsets {
+
+ line := panel_line(middle_section, theme.background, 25)
+ line.id = oui.push_id(c0, fmt.tprintf("sizings_line_%d", i))
+ defer oui.pop_id(c0)
+
+ oui.item_insert(line, label(st.names[i], font, ))
+
+ // To then access the member of the struct you're looping over
+ // you need to do this shit:
+ // v----------------------v
+ current_value := cast(^int) (root+offset)
+
+ oui.item_insert(line, slider_int(fmt.tprintf("sizings-%d", i), fmt.tprintf("%d", current_value^), 300, current_value, 0, 200))
+ }
+ output_button := button("output sizings", 100)
+ middle_section.layout_cut_children = .Top
+ oui.item_insert(middle_section, output_button)
+ if oui.is_clicked(c0, output_button) do fmt.printf("%#v", sizings)
+ }
+
+
+ // - - - - THEME EDITOR - - - -
+ {
+ oui.item_insert(middle_section, label("Theme Editor", big_font, 100, .Center))
+
+ // To loop over the members of a struct you need to do this goofy shit:
+ info := runtime.type_info_base(type_info_of(Theme))
+ st := info.variant.(runtime.Type_Info_Struct)
+ root := uintptr(&theme)
+ for offset, i in st.offsets {
+
+ line := panel_line(middle_section, theme.background, 25)
+ line.layout_cut_gap = 10
+ line.id = oui.push_id(c0, fmt.tprintf("line_%d", i))
+ defer oui.pop_id(c0)
+
+ oui.item_insert(line, label(st.names[i], font, ))
+
+ // To then access the member of the struct you're looping over
+ // you need to do this shit:
+ // v------------------------v
+ color_sliders(line, cast(^Color) (root+offset))
+ }
+ output_theme_button := button("output theme", 100)
+ middle_section.layout_cut_children = .Top
+ oui.item_insert(middle_section, output_theme_button)
+ if oui.is_clicked(c0, output_theme_button) do fmt.printf("%#v", theme)
+ }
+
+ }
+
+
+ oui.end_layout(c0)
+
+ ui_draw(master_container)
+
+ oui.process(c0)
+
+} else {
+ DrawTextEx(font, "Date", {20, 8}, f32(font.baseSize), 0, RAYWHITE);
+ DrawTextEx(font, "Calltime", {105, 8}, f32(font.baseSize), 0, RAYWHITE);
+ DrawTextEx(font, "Wraptime", {f32(width)-83, 8}, f32(font.baseSize), 0, RAYWHITE);
for day, i in workdays {
@@ -164,17 +359,17 @@ main :: proc() {
// (At least, given how lunch breaks are currently implemented,
// as holes in the workday)
- DrawRectangle(10, DAY_HEIGHT*i32(i+1)-4, width-20, DAY_HEIGHT-1, PBGCOLOR)
+ DrawRectangle(10, DAY_HEIGHT*i32(i+1)-4, width-20, DAY_HEIGHT-1, theme.background_bar)
for block, j in day.blocks {
if j == day.total_timeblocks do break
- block_color: = GREEN
+ block_color: = theme.price_100
switch {
case block.value > 2.1:
- block_color = PURPLE
+ block_color = theme.price_300
case block.value > 1.6:
- block_color = RED
+ block_color = theme.price_200
case block.value > 1.1:
- block_color = ORANGE
+ block_color = theme.price_150
}
DrawRectangle(TIMELINE_START+i32(math.round(day.fractions[j].start*f32(width+TIMELINE_END-TIMELINE_START))),
@@ -188,28 +383,21 @@ main :: proc() {
copy(wrap_text, clockprint(day.wrap))
copy(date_text, toString(day.call))
- text_height = math.round(f32(i+1)*DAY_HEIGHT+(DAY_HEIGHT-font_size)*0.25)
+ text_height := math.round(f32(i+1)*DAY_HEIGHT+(DAY_HEIGHT-f32(font.baseSize))*0.25)
- DrawTextEx(font, cstring(&date_text[0]), {20, text_height}, font_size, 0, RAYWHITE);
- DrawTextEx(font, cstring(&wrap_text[0]), {f32(width)-70, text_height}, font_size, 0, RAYWHITE);
+ DrawTextEx(font, cstring(&date_text[0]), {20, text_height}, f32(font.baseSize), 0, RAYWHITE);
+ DrawTextEx(font, cstring(&wrap_text[0]), {f32(width)-70, text_height}, f32(font.baseSize), 0, RAYWHITE);
if i == len(workdays)-1 {
- DrawTextEx(big_font, "+", {20, DAY_HEIGHT*f32(i+2)}, big_font_size, 0, RAYWHITE)
+ DrawTextEx(big_font, "+", {20, DAY_HEIGHT*f32(i+2)}, f32(big_font.baseSize), 0, RAYWHITE)
}
}
- DrawRectangle(0, height-50, width+10, 60, PBGCOLOR)
-
- 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);
+ DrawRectangle(0, height-50, width+10, 60, theme.background_bar)
+ DrawTextEx(small_font, total_sum, {f32(width)-120, f32(height)-43}, f32(small_font.baseSize), 0, RAYWHITE);
+ DrawTextEx(big_font, inc_soc, {f32(width)-120, f32(height)-29}, f32(big_font.baseSize), 0, RAYWHITE);
+}
EndDrawing()
}
}
-
-BGCOLOR : rl.Color : {30, 30, 30, 255}
-PBGCOLOR : rl.Color : {40, 40, 40, 255}
-
-DAY_HEIGHT :: 35
-TIMELINE_START :: 175
-TIMELINE_END :: -85
diff --git a/src/time.odin b/src/time.odin
index 468f5d8..90dca49 100644
--- a/src/time.odin
+++ b/src/time.odin
@@ -90,7 +90,7 @@ new_workday :: proc(previous_wrap : Moment,
// Paragraph 6.7 says that up to 2 hours of unused warned overtime counts as worktime,
// though so that at least one hour of the unused overtime is not counted.
// (It's unclear if an 8-hour day that ends 3 hours in counts as having 5 hours of unused overtime)
- max(clamp(sub(planned_wrap, {0, 1, 0}), wrap, add(wrap, {0, 2, 0})),
+ time_max(time_clamp(sub(planned_wrap, {0, 1, 0}), wrap, add(wrap, {0, 2, 0})),
add(call, {0, 4, 0})), 1, ""}
// ^ Minimum 4 hour day ^
@@ -170,7 +170,7 @@ new_workday :: proc(previous_wrap : Moment,
if getweekday(block.start) == .Sunday do upvalue(&block, 2, "Sunday") // Sundays are +100%
if !(less(call, Moment{0, 7, call.day, call.month, call.year}) &&
- less(min(add(call, Delta{0,8,0}), wrap), Moment{0, 17, call.day, call.month, call.year} )) {
+ less(time_min(add(call, Delta{0,8,0}), wrap), Moment{0, 17, call.day, call.month, call.year} )) {
// This was added for rule 6.11c, but in a world without a defined normal workday,
// that rule is already covered already by 6.11g, so this is empty.
}
@@ -489,7 +489,7 @@ maxDelta :: proc(delta_a: Delta, delta_b: Delta) -> Delta {
if sortable(delta_a) > sortable(delta_b) do return delta_a
return delta_b
}
-max :: proc{maxDelta, maxMoment}
+time_max :: proc{maxDelta, maxMoment}
minMoment :: proc(moment_a: Moment, moment_b: Moment) -> Moment {
if sortable(moment_a) < sortable(moment_b) do return moment_a
@@ -499,15 +499,15 @@ minDelta :: proc(delta_a: Delta, delta_b: Delta) -> Delta {
if sortable(delta_a) < sortable(delta_b) do return delta_a
return delta_b
}
-min :: proc{minDelta, minMoment}
+time_min :: proc{minDelta, minMoment}
clampMoment :: proc(moment: Moment, moment_min: Moment, moment_max: Moment) -> Moment {
- return min(max(moment, moment_min), moment_max)
+ return time_min(time_max(moment, moment_min), moment_max)
}
clampDelta :: proc(delta: Delta, delta_min: Delta, delta_max: Delta) -> Delta {
- return min(max(delta, delta_min), delta_max)
+ return time_min(time_max(delta, delta_min), delta_max)
}
-clamp :: proc{clampMoment, clampDelta}
+time_clamp :: proc{clampMoment, clampDelta}
greatMoment :: proc(moment_a: Moment, moment_b: Moment) -> bool {
return bool(sortable(moment_a) > sortable(moment_b))
@@ -702,6 +702,16 @@ clockprintTimeblock :: proc(block: Timeblock) -> string {
}
clockprint :: proc{clockprintTimeblock, clockprintMoment}
+dayprintMoment :: proc(moment: Moment) -> string {
+ using moment
+ return fmt.tprintf("%4i-%2i-%2i", year, month, day)
+}
+dayprintTimeblock :: proc(block: Timeblock) -> string {
+ using block
+ return fmt.tprintf("%s -> %s", dayprint(start), dayprint(end))
+}
+dayprint :: proc{dayprintTimeblock, dayprintMoment}
+
popBlock :: proc(workday: ^Workday, index: int, count: int = 1) {
using workday
when ODIN_DEBUG do fmt.printf("popBlock() running to remove %i block(s) from index %i\n", count, index)
diff --git a/src/ui_implementation.odin b/src/ui_implementation.odin
new file mode 100644
index 0000000..6839b57
--- /dev/null
+++ b/src/ui_implementation.odin
@@ -0,0 +1,389 @@
+package main
+
+import "../lib/oui"
+import "core:strings"
+import "core:fmt"
+import rl "vendor:raylib"
+
+
+Theme :: struct {
+ background: rl.Color,
+ background_bar: rl.Color,
+ button: rl.Color,
+ button_hover: rl.Color,
+ button_click: rl.Color,
+ base: rl.Color,
+ slider_bar: rl.Color,
+ text: rl.Color,
+ price_100: rl.Color,
+ price_150: rl.Color,
+ price_200: rl.Color,
+ price_300: rl.Color,
+}
+
+theme : Theme = {
+ background = {25 , 27 , 29 , 255,},
+ background_bar = {43 , 43 , 48 , 255,},
+ button = {91 , 91 , 204, 255,},
+ button_hover = {91 , 91 , 204, 255,},
+ button_click = {91 , 91 , 204, 255,},
+ base = {60 , 60 , 60 , 255,},
+ slider_bar = {91 , 91 , 204, 255,},
+ text = {255, 255, 255, 252,},
+ price_100 = {30 , 240, 30 , 255,},
+ price_150 = {240, 200, 30 , 255,},
+ price_200 = {240, 30 , 30 , 255,},
+ price_300 = {240, 30 , 240, 255,},
+}
+
+Sizings :: struct {
+ date: int,
+ call: int,
+ wrap: int,
+ price: int,
+ lunch: int,
+ timeline: int,
+ inter_timeline: int,
+}
+sizings : Sizings = {
+ date = 110,
+ call = 85,
+ wrap = 90,
+ price = 100,
+ lunch = 100,
+ timeline = 32,
+ inter_timeline = 5,
+}
+
+DAY_HEIGHT :: 35 // Only here for legacy UI
+TIMELINE_START :: 175 // Only here for legacy UI
+TIMELINE_END :: -85 // Only here for legacy UI
+
+Item :: oui.Item
+Call :: oui.Call
+
+Data_Element :: enum int {
+ Panel,
+ Button,
+ SliderU8,
+ SliderInt,
+ Label,
+ Text_Input,
+ Timeblock,
+ Timeline,
+// ...
+}
+
+Data_Head :: struct {
+ subtype: Data_Element,
+}
+
+Data_Panel :: struct {
+ using _: Data_Head,
+ color: rl.Color,
+}
+
+Data_Button :: struct {
+ using _: Data_Head,
+ text: string,
+ selected: bool,
+}
+
+Data_SliderInt :: struct {
+ using _: Data_Head,
+ text: string,
+ value: ^int,
+ min: int,
+ max: int,
+}
+
+Data_SliderU8 :: struct {
+ using _: Data_Head,
+ text: string,
+ value: ^u8,
+}
+
+Data_Label :: struct {
+ using _: Data_Head,
+ text: string,
+ font: rl.Font,
+ font_size: i32,
+ alignment: Text_Alignment,
+}
+
+Data_Timeline :: struct {
+ using _: Data_Head,
+ day: ^Workday,
+}
+
+panel :: proc(color : rl.Color = rl.RED) -> ^Item {
+ item := oui.item_make(c0)
+
+ data := oui.alloc_typed(c0, item, Data_Panel)
+ data.subtype = .Panel
+ data.color = color
+
+ return item
+}
+
+panel_line :: proc(parent: ^Item, color : rl.Color, height: int = 40) -> (item: ^Item) {
+ item = oui.item_make(c0)
+ item.layout_cut_children = .Left
+ item.layout_size.y = height
+
+ old := parent.layout_cut_children
+ parent.layout_cut_children = .Top
+ oui.item_insert(parent, item)
+ parent.layout_cut_children = old
+
+ data := oui.alloc_typed(c0, item, Data_Panel)
+ data.subtype = .Panel
+ data.color = color
+
+ return
+}
+
+button :: proc(text: string, width: int, selected := false) -> ^Item {
+ item := oui.item_make(c0)
+ item.layout_size = {width, 35}
+ item.callback = button_callback
+ item.id = oui.gen_id(c0, text)
+
+ data := oui.alloc_typed(c0, item, Data_Button)
+ data.subtype = .Button
+ data.text = text
+ data.selected = selected
+
+ return item
+}
+button_callback :: proc(ctxt: ^oui.Context, item: ^Item, event: Call) -> int {
+ data := cast(^Data_Button) item.handle
+
+ #partial switch event {
+ case .Cursor_Handle:
+ //return int(Cursor_Type.Hand)
+ }
+
+ return -1
+}
+
+slider_int :: proc(id: string, text: string, width: int, value: ^int, min: int, max: int) -> ^Item {
+ item := oui.item_make(c0)
+ item.layout_size = {width, 25}
+ item.id = oui.gen_id(c0, id)
+ item.callback = slider_int_callback
+
+ data := oui.alloc_typed(c0, item, Data_SliderInt)
+ data.subtype = .SliderInt
+ data.text = text
+ data.value = value
+ data.min = min
+ data.max = max
+
+ return item
+}
+slider_int_callback :: proc(ctxt: ^oui.Context, item: ^Item, event: Call) -> int {
+ data := cast(^Data_SliderInt) item.handle
+ rect := item.bounds
+
+ #partial switch event {
+ case .Left_Capture:
+ cursor_position := clamp(oui.get_cursor(c0).x, rect.l, rect.r)
+
+ data.value^ = int(f32(data.min) + (f32(data.max - data.min) * (f32(cursor_position - rect.l) / f32(rect.r - rect.l))))
+ }
+
+ return -1
+}
+
+slider_u8 :: proc(id: string, text: string, width: int, value: ^u8) -> ^Item {
+ item := oui.item_make(c0)
+ item.layout_size = {width, 25}
+ item.id = oui.gen_id(c0, id)
+ item.callback = slider_u8_callback
+
+ data := oui.alloc_typed(c0, item, Data_SliderU8)
+ data.subtype = .SliderU8
+ data.text = text
+ data.value = value
+
+ return item
+}
+slider_u8_callback :: proc(ctxt: ^oui.Context, item: ^Item, event: Call) -> int {
+ data := cast(^Data_SliderU8) item.handle
+ rect := item.bounds
+
+ #partial switch event {
+ case .Left_Capture:
+ cursor_position := clamp(oui.get_cursor(c0).x, rect.l, rect.r)
+
+ data.value^ = u8(255*(f32(cursor_position - rect.l) / f32(rect.r - rect.l)))
+ }
+
+ return -1
+}
+color_sliders :: proc(parent: ^Item, color: ^rl.Color) {
+ width :: 167
+ oui.item_insert(parent, slider_u8("slider_r", fmt.tprintf("%d", color.r), width, &color.r))
+ oui.item_insert(parent, slider_u8("slider_g", fmt.tprintf("%d", color.g), width, &color.g))
+ oui.item_insert(parent, slider_u8("slider_b", fmt.tprintf("%d", color.b), width, &color.b))
+ oui.item_insert(parent, slider_u8("slider_a", fmt.tprintf("%d", color.a), width, &color.a))
+}
+
+timeline :: proc(parent: ^Item, day: ^Workday) -> ^Item {
+ item := oui.item_make(c0)
+
+ data := oui.alloc_typed(c0, item, Data_Timeline)
+ data.subtype = .Timeline
+ data.day = day
+
+ oui.item_insert(parent, item)
+ return item
+}
+
+Text_Alignment :: enum int {
+ // Techically called justification, but text_alignment is more self-explanatory.
+ Left,
+ Right,
+ Center,
+}
+label :: proc(text: string, font: rl.Font, width: int = 150, alignment: Text_Alignment = .Left) -> ^Item {
+ item := oui.item_make(c0)
+ item.layout_size = {width, 25}
+
+ data := oui.alloc_typed(c0, item, Data_Label)
+ data.subtype = .Label
+ data.text = text
+ data.font = font
+ data.alignment = alignment
+
+ return item
+}
+calculate_text_alignment :: proc(text: cstring, font: rl.Font, alignment: Text_Alignment, rect: oui.RectI, spacing: f32 = 0) -> (output: [2]int) {
+
+ measurement := rl.MeasureTextEx(font, text, f32(font.baseSize), spacing)
+
+ switch alignment {
+ case .Left:
+ output.x = rect.l
+ case .Right:
+ output.x = rect.r - int(measurement.x)
+ case .Center:
+ output.x = (rect.l+(rect.r-rect.l)/2) - int(measurement.x/2)
+ }
+
+ output.y = (rect.t+(rect.b-rect.t)/2) - int(measurement.y/2)
+
+ return
+}
+i2f :: proc "contextless" (input: [2]int) -> rl.Vector2 {
+ return { f32(input.x), f32(input.y) }
+}
+f2i :: proc "contextless" (input: [2]f32) -> [2]int {
+ return { int(input.x), int(input.y) }
+}
+
+// recursive loop
+ui_draw_children :: proc(item: ^oui.Item) {
+ list := oui.children_list(c0, item)
+ for kid in list {
+ ui_draw(kid)
+ }
+}
+
+ui_draw :: proc(item: ^oui.Item) {
+ head := cast(^Data_Head) item.handle
+ rect := item.bounds
+
+ //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:
+ data := cast(^Data_Button) item.handle
+ text_spacing := clamp(item.anim.hot - item.anim.active, 0, item.anim.hot)*2
+ rl.DrawRectangle(i32(rect.l), i32(rect.t), i32(rect.r-rect.l), i32(rect.b-rect.t), theme.button)
+
+ text := strings.clone_to_cstring(data.text, context.temp_allocator)
+ position := calculate_text_alignment(text, font, .Center, rect, text_spacing)
+
+ rl.DrawTextEx(font, text, i2f(position), f32(font.baseSize), text_spacing, theme.text);
+
+ //fmt.println(item.anim)
+
+ case .SliderInt:
+ data := cast(^Data_SliderInt) head
+ rl.DrawRectangle(i32(rect.l), i32(rect.t), i32(rect.r-rect.l), i32(rect.b-rect.t), theme.base)
+ rl.DrawRectangle(i32(rect.l+1), i32(rect.t+1), i32(f32(rect.r-rect.l)*(f32(data.value^)/f32(data.max))-2), i32(rect.b-rect.t-2), theme.slider_bar)
+
+ text := strings.clone_to_cstring(data.text, context.temp_allocator)
+ position := calculate_text_alignment(text, font, .Center, rect)
+
+ rl.DrawTextEx(font, text, i2f(position), f32(font.baseSize), 0.0, theme.text);
+
+ case .SliderU8:
+ data := cast(^Data_SliderU8) head
+ rl.DrawRectangle(i32(rect.l), i32(rect.t), i32(rect.r-rect.l), i32(rect.b-rect.t), theme.base)
+ rl.DrawRectangle(i32(rect.l+1), i32(rect.t+1), i32(f32(rect.r-rect.l)*(f32(data.value^)/255)-2), i32(rect.b-rect.t-2), theme.slider_bar)
+
+ text := strings.clone_to_cstring(data.text, context.temp_allocator)
+ position := calculate_text_alignment(text, font, .Center, rect)
+
+ rl.DrawTextEx(font, text, i2f(position), f32(font.baseSize), 0.0, theme.text);
+
+ case .Panel:
+ data := cast(^Data_Panel) head
+ rl.DrawRectangle(i32(rect.l), i32(rect.t), i32(rect.r-rect.l), i32(rect.b-rect.t), data.color)
+ ui_draw_children(item)
+
+ case .Label:
+ data := cast(^Data_Label) item.handle
+ text := strings.clone_to_cstring(data.text, context.temp_allocator)
+ position := calculate_text_alignment(text, data.font, data.alignment, rect)
+
+ rl.DrawTextEx(data.font, text, i2f(position), f32(data.font.baseSize), 0.0, theme.text);
+
+ case .Timeline:
+ data := cast(^Data_Timeline) item.handle
+
+ width := int(f32(rect.r - rect.l)/(FRACT_MAX - FRACT_MIN))
+
+ for fracts, i in data.day.fractions {
+
+ color := theme.price_100
+ value := data.day.blocks[i].value
+
+ switch {
+ case value>2.1:
+ color = theme.price_300
+ case value>1.6:
+ color = theme.price_200
+ case value>1.1:
+ color = theme.price_150
+ }
+
+
+
+ rl.DrawRectangle(i32(rect.l + int(f32(width)*fracts.start) - int(f32(width)*FRACT_MIN)),
+ i32(rect.t),
+ i32(f32(width) * (fracts.end - fracts.start)+0.99),
+ i32(rect.b - rect.t),
+ color)
+ // Dark middle of blocks, glowing edge. Disabled for now.
+ /*rl.DrawRectangle(i32(rect.l + int(f32(width)*fracts.start) - int(f32(width)*FRACT_MIN) + 1),
+ i32(rect.t) + 1,
+ i32(f32(width+1) * (fracts.end - fracts.start)-1.01),
+ i32(rect.b - rect.t)-2,
+ {0,0,0,100})*/
+ if i+1 == data.day.total_timeblocks {
+ break
+ }
+ }
+ }
+} \ No newline at end of file