diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4bb25ab..1ea9e8a 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -76,3 +76,7 @@ jobs: odin check sdl2/opengl $FLAGS odin check wgpu/microui -target:windows_amd64 $FLAGS + + odin check orca/breakout -target:orca_wasm32 $FLAGS + odin check orca/clock -target:orca_wasm32 $FLAGS + odin check orca/ui -target:orca_wasm32 $FLAGS \ No newline at end of file diff --git a/.gitignore b/.gitignore index cbb546f..95b17eb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,13 @@ -*.exe -*.ll -*.obj -*.pdb - -**.exp -tetroid.lib -build.bat \ No newline at end of file +*.exe +*.ll +*.obj +*.pdb + +**.exp +tetroid.lib +build.bat + +# orca samples + +orca_output +module.wasm \ No newline at end of file diff --git a/README.md b/README.md index 68a4497..2849362 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,9 @@ Assets and third-party libraries are provided under their own license. If in dou * [07-texturing](https://github.com/odin-lang/examples/tree/master/learn_metal/07-texturing) * [08-compute](https://github.com/odin-lang/examples/tree/master/learn_metal/08-compute) * [09-compute-to-render](https://github.com/odin-lang/examples/tree/master/learn_metal/09-compute-to-render) + +## Orca + +* [breakout](https://github.com/odin-lang/examples/tree/master/orca/breakout) +* [clock](https://github.com/odin-lang/examples/tree/master/orca/clock) +* [ui](https://github.com/odin-lang/examples/tree/master/orca/ui) \ No newline at end of file diff --git a/orca/breakout/data/Literata-SemiBoldItalic.ttf b/orca/breakout/data/Literata-SemiBoldItalic.ttf new file mode 100644 index 0000000..d49c441 Binary files /dev/null and b/orca/breakout/data/Literata-SemiBoldItalic.ttf differ diff --git a/orca/breakout/data/ball.png b/orca/breakout/data/ball.png new file mode 100644 index 0000000..c918ee5 Binary files /dev/null and b/orca/breakout/data/ball.png differ diff --git a/orca/breakout/data/brick.png b/orca/breakout/data/brick.png new file mode 100644 index 0000000..47705ca Binary files /dev/null and b/orca/breakout/data/brick.png differ diff --git a/orca/breakout/data/test_read.txt b/orca/breakout/data/test_read.txt new file mode 100644 index 0000000..5549f68 --- /dev/null +++ b/orca/breakout/data/test_read.txt @@ -0,0 +1 @@ +Hello, ressource file diff --git a/orca/breakout/data/underwater.jpg b/orca/breakout/data/underwater.jpg new file mode 100644 index 0000000..8a03630 Binary files /dev/null and b/orca/breakout/data/underwater.jpg differ diff --git a/orca/breakout/main.odin b/orca/breakout/main.odin new file mode 100644 index 0000000..e0fba0c --- /dev/null +++ b/orca/breakout/main.odin @@ -0,0 +1,484 @@ +package src + +/* +Original Source: https://github.com/orca-app/orca/blob/main/samples/breakout/src/main.c + +Can be run using in the local folder +1. odin.exe build main.odin -file -target:orca_wasm32 -out:module.wasm +2. orca bundle --name orca_output --resource-dir data module.wasm +3. orca_output\bin\orca_output.exe +*/ + +import "base:runtime" +import "core:fmt" +import "core:math" +import oc "core:sys/orca" + +NUM_BLOCKS_PER_ROW :: 7 +NUM_BLOCKS :: 42 // 7 * 6 +NUM_BLOCKS_TO_WIN :: NUM_BLOCKS - 2 +BLOCKS_WIDTH :: f32(810.0) +BLOCK_HEIGHT :: f32(30.0) +BLOCKS_PADDING :: f32(15.0) +BLOCKS_BOTTOM :: f32(300.0) +PADDLE_MAX_LAUNCH_ANGLE :: 0.7 + +BLOCK_WIDTH :: (BLOCKS_WIDTH - ((NUM_BLOCKS_PER_ROW + 1) * BLOCKS_PADDING)) / NUM_BLOCKS_PER_ROW + +// This is upside down from how it will actually be drawn. +velocity := oc.vec2{5, 5} + +//odinfmt: disable +blockHealth := [NUM_BLOCKS]int { + 0, + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, +} +score := 0 + +leftDown := false +rightDown := false + +frameSize := oc.vec2{100, 100} + +surface: oc.surface +renderer: oc.canvas_renderer +canvas: oc.canvas_context + +waterImage: oc.image +brickImage: oc.image +ballImage: oc.image +font: oc.font + +paddleColor := oc.color{{1, 0, 0, 1}, .RGB} +paddle := oc.rect{300, 50, 200, 24} +ball := oc.rect{200, 200, 20, 20} + +flip_y :: proc "contextless" (r: oc.rect) -> oc.mat2x3 { + return {1, 0, 0, 0, -1, 2 * r.y + r.w} +} + +flip_y_at :: proc "contextless" (pos: oc.vec2) -> oc.mat2x3 { + return {1, 0, 0, 0, -1, 2 * pos.y} +} + +main :: proc() { + oc.window_set_title("Breakout") + + renderer = oc.canvas_renderer_create() + surface = oc.canvas_surface_create(renderer) + canvas = oc.canvas_context_create() + + waterImage = oc.image_create_from_path(renderer, "/underwater.jpg", false) + brickImage = oc.image_create_from_path(renderer, "/brick.png", false) + ballImage = oc.image_create_from_path(renderer, "/ball.png", false) + + if oc.image_is_nil(waterImage) { + oc.log_error("couldn't load water image\n") + } + if oc.image_is_nil(brickImage) { + oc.log_error("couldn't load brick image\n") + } + if oc.image_is_nil(ballImage) { + oc.log_error("couldn't load ball image\n") + } + + ranges := [5]oc.unicode_range { + oc.UNICODE_BASIC_LATIN, + oc.UNICODE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, + oc.UNICODE_LATIN_EXTENDED_A, + oc.UNICODE_LATIN_EXTENDED_B, + oc.UNICODE_SPECIALS, + } + + font = oc.font_create_from_path("/Literata-SemiBoldItalic.ttf", 5, &ranges[0]) +} + +// @(export) +// oc_on_terminate :: proc "c" () { +// if score == NUM_BLOCKS_TO_WIN { +// oc.log_info("you win!\n") +// } else { +// oc.log_info("goodbye world!\n") +// } +// } + +@(export) +oc_on_resize :: proc "c" (width, height: u32) { + context = runtime.default_context() + oc.log_infof("frame resize %u, %u", width, height) + frameSize.x = f32(width) + frameSize.y = f32(height) +} + +@(export) +oc_on_key_down :: proc "c" (scan: oc.scan_code, key: oc.key_code) { + // oc.log_info("key down: %i", key) + oc.log_ext(.INFO, "keydown", "breakout.odin", 112, "key up: %i", key) + + if key == .LEFT { + leftDown = true + } + + if key == .RIGHT { + rightDown = true + } +} + +@(export) +oc_on_key_up :: proc "c" (scan: oc.scan_code, key: oc.key_code) { + // oc.log_info("key up: %i", key) + oc.log_ext(.INFO, "keyup", "breakout.odin", 126, "key up: %i", key) + + if key == .LEFT { + leftDown = false + } + + if key == .RIGHT { + rightDown = false + } +} + +block_rect :: proc "contextless" (i: int) -> oc.rect { + row := f32(i / NUM_BLOCKS_PER_ROW) + col := f32(i % NUM_BLOCKS_PER_ROW) + + return { + BLOCKS_PADDING + (BLOCKS_PADDING + BLOCK_WIDTH) * col, + BLOCKS_BOTTOM + (BLOCKS_PADDING + BLOCK_HEIGHT) * row, + BLOCK_WIDTH, + BLOCK_HEIGHT, + } +} + +// Returns a cardinal direction 1-8 for the collision with the block, or zero +// if no collision. 1 is straight up and directions proceed clockwise. +check_collision :: proc "contextless" (block: oc.rect) -> int { + // Note that all the logic for this game has the origin in the bottom left. + ballx2 := ball.x + ball.w + bally2 := ball.y + ball.h + blockx2 := block.x + block.w + blocky2 := block.y + block.h + + if ballx2 < block.x || blockx2 < ball.x || bally2 < block.y || blocky2 < ball.y { + // Ball is fully outside block + return 0 + } + + // If moving right, the ball can bounce off its top right corner, right + // side, or bottom right corner. Corner bounces occur if the block's bottom + // left corner is in the ball's top right quadrant, or if the block's top + // left corner is in the ball's bottom left quadrant. Otherwise, an edge + // bounce occurs if the block's left edge falls in either of the ball's + // right quadrants. + // + // This logic generalizes to other directions. + // + // We assume significant tunneling can't happen. + + ballCenter := oc.vec2{ball.x + ball.w / 2, ball.y + ball.h / 2} + + // Moving right + if velocity.x > 0 { + // Ball's top right corner + if ballCenter.x <= block.x && + block.x <= ballx2 && + ballCenter.y <= block.y && + block.y <= bally2 { + return 2 + } + + // Ball's bottom right corner + if ballCenter.x <= block.x && + block.x <= ballx2 && + ball.y <= blocky2 && + blocky2 <= ballCenter.y { + return 4 + } + + // Ball's right edge + if ballCenter.x <= block.x && block.x <= ballx2 { + return 3 + } + } + + // Moving up + if velocity.y > 0 { + // Ball's top left corner + if ball.x <= blockx2 && + blockx2 <= ballCenter.x && + ballCenter.y <= block.y && + block.y <= bally2 { + return 8 + } + + // Ball's top right corner + if ballCenter.x <= block.x && + block.x <= ballx2 && + ballCenter.y <= block.y && + block.y <= bally2 { + return 2 + } + + // Ball's top edge + if ballCenter.y <= block.y && block.y <= bally2 { + return 1 + } + } + + // Moving left + if velocity.x < 0 { + // Ball's bottom left corner + if ball.x <= blockx2 && + blockx2 <= ballCenter.x && + ball.y <= blocky2 && + blocky2 <= ballCenter.y { + return 6 + } + + // Ball's top left corner + if ball.x <= blockx2 && + blockx2 <= ballCenter.x && + ballCenter.y <= block.y && + block.y <= bally2 { + return 8 + } + + // Ball's left edge + if ball.x <= blockx2 && blockx2 <= ballCenter.x { + return 7 + } + } + + // Moving down + if velocity.y < 0 { + // Ball's bottom right corner + if ballCenter.x <= block.x && + block.x <= ballx2 && + ball.y <= blocky2 && + blocky2 <= ballCenter.y { + return 4 + } + + // Ball's bottom left corner + if ball.x <= blockx2 && + blockx2 <= ballCenter.x && + ball.y <= blocky2 && + blocky2 <= ballCenter.y { + return 6 + } + + // Ball's bottom edge + if ball.y <= blocky2 && blocky2 <= ballCenter.y { + return 5 + } + } + + return 0 +} + +@(export) +oc_on_frame_refresh :: proc "c" () { + context = runtime.default_context() + scratch := oc.scratch_begin() + defer oc.scratch_end(scratch) + + if leftDown { + paddle.x -= 10 + } else if rightDown { + paddle.x += 10 + } + paddle.x = clamp(paddle.x, 0, frameSize.x - paddle.w) + + ball.x += velocity.x + ball.y += velocity.y + ball.x = clamp(ball.x, 0, frameSize.x - ball.w) + ball.y = clamp(ball.y, 0, frameSize.y - ball.h) + + if ball.x + ball.w >= frameSize.x { + velocity.x = -velocity.x + } + if ball.x <= 0 { + velocity.x = -velocity.x + } + if ball.y + ball.h >= frameSize.y { + velocity.y = -velocity.y + } + + if ball.y <= paddle.y + paddle.h && + ball.x + ball.w >= paddle.x && + ball.x <= paddle.x + paddle.w && + velocity.y < 0 { + t := ((ball.x + ball.w / 2) - paddle.x) / paddle.w + launchAngle := math.lerp(f32(-PADDLE_MAX_LAUNCH_ANGLE), f32(PADDLE_MAX_LAUNCH_ANGLE), t) + speed := math.sqrt(velocity.x * velocity.x + velocity.y * velocity.y) + velocity = (oc.vec2){math.sin(launchAngle) * speed, math.cos(launchAngle) * speed} + ball.y = paddle.y + paddle.h + + oc.log_info("PONG!") + } + + if ball.y <= 0 { + ball.x = frameSize.x / 2. - ball.w + ball.y = frameSize.y / 2. - ball.h + } + + for i in 0 ..< NUM_BLOCKS { + if blockHealth[i] <= 0 { + continue + } + + r := block_rect(i) + result := check_collision(r) + if result != 0 { + oc.log_infof("Collision! direction=%d", result) + blockHealth[i] -= 1 + + if blockHealth[i] == 0 { + score += 1 + } + + vx := velocity.x + vy := velocity.y + + switch (result) { + case 1, 5: + velocity.y = -vy + case 3, 7: + velocity.x = -vx + case 2, 6: + velocity.x = -vy + velocity.y = -vx + case 4, 8: + velocity.x = vy + velocity.y = vx + } + } + } + + if score == NUM_BLOCKS_TO_WIN { + oc.request_quit() + } + + oc.canvas_context_select(canvas) + + oc.set_color_rgba(10.0 / 255.0, 31.0 / 255.0, 72.0 / 255.0, 1) + oc.clear() + + oc.image_draw(waterImage, (oc.rect){0, 0, frameSize.x, frameSize.y}) + + yUp := oc.mat2x3{1, 0, 0, 0, -1, frameSize.y} + + oc.matrix_multiply_push(yUp) + { + for i in 0 ..< NUM_BLOCKS { + if blockHealth[i] <= 0 { + continue + } + + r := block_rect(i) + + oc.set_image(brickImage) + oc.set_color_rgba(0.9, 0.9, 0.9, 1) + oc.rounded_rectangle_fill(r.x, r.y, r.w, r.h, 4) + oc.set_image(oc.image_nil()) + + oc.set_color_rgba(0.6, 0.6, 0.6, 1) + oc.set_width(2) + oc.rounded_rectangle_stroke(r.x, r.y, r.w, r.h, 4) + + fontSize := 18 + text := fmt.tprintf("%d", blockHealth[i]) + textRect := oc.font_text_metrics(font, f32(fontSize), text).ink + + textPos := oc.vec2 { + r.x + r.w / 2 - textRect.w / 2 - textRect.x, + r.y + r.h / 2 - textRect.h / 2 - textRect.y - textRect.h, //NOTE: we render with y-up so we need to flip bounding box coordinates. + } + + oc.set_color_rgba(0.9, 0.9, 0.9, 1) + oc.circle_fill(r.x + r.w / 2, r.y + r.h / 2, r.h / 2.5) + + oc.set_color_rgba(0, 0, 0, 1) + oc.set_font(font) + oc.set_font_size(18) + oc.move_to(textPos.x, textPos.y) + oc.matrix_multiply_push(flip_y_at(textPos)) + { + oc.text_outlines(text) + oc.fill() + } + oc.matrix_pop() + } + + oc.set_color(paddleColor) + oc.rounded_rectangle_fill(paddle.x, paddle.y, paddle.w, paddle.h, 4) + + oc.matrix_multiply_push(flip_y(ball)) + { + oc.image_draw(ballImage, ball) + } + oc.matrix_pop() + + // draw score text + { + oc.move_to(20, 20) + text := fmt.tprintf( + "Destroy all %d blocks to win! Current score: %d", + NUM_BLOCKS_TO_WIN, + score, + ) + textPos := oc.vec2{20, 20} + oc.matrix_multiply_push(flip_y_at(textPos)) + { + oc.set_color_rgba(0.9, 0.9, 0.9, 1) + oc.text_outlines(text) + oc.fill() + } + oc.matrix_pop() + } + } + oc.matrix_pop() + + oc.canvas_render(renderer, canvas, surface) + oc.canvas_present(renderer, surface) +} diff --git a/orca/clock/data/segoeui.ttf b/orca/clock/data/segoeui.ttf new file mode 100644 index 0000000..0f52cbd Binary files /dev/null and b/orca/clock/data/segoeui.ttf differ diff --git a/orca/clock/icon.png b/orca/clock/icon.png new file mode 100644 index 0000000..1091c1b Binary files /dev/null and b/orca/clock/icon.png differ diff --git a/orca/clock/main.odin b/orca/clock/main.odin new file mode 100644 index 0000000..0e516da --- /dev/null +++ b/orca/clock/main.odin @@ -0,0 +1,168 @@ +package src + +/* +Original Source: https://github.com/orca-app/orca/blob/main/samples/clock/src/main.c + +Can be run using in the local folder +1. odin.exe build main.odin -file -target:orca_wasm32 -out:module.wasm +2. orca bundle --name orca_output --resource-dir data module.wasm +3. orca_output\bin\orca_output.exe +*/ + +import "core:math" +import oc "core:sys/orca" + +clockNumberStrings := [?]string{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"} + +surface: oc.surface = {} +renderer: oc.canvas_renderer = {} +canvas: oc.canvas_context = {} +font: oc.font = {} +frameSize: oc.vec2 = {100, 100} +lastSeconds: f64 = 0 + +mat_transform :: proc "contextless" (x, y, radians: f32) -> oc.mat2x3 { + rotation := oc.mat2x3_rotate(radians) + translation := oc.mat2x3_translate(x, y) + return oc.mat2x3_mul_m(translation, rotation) +} + +main :: proc() { + oc.window_set_title("clock") + oc.window_set_size({400, 400}) + + renderer = oc.canvas_renderer_create() + surface = oc.canvas_surface_create(renderer) + canvas = oc.canvas_context_create() + + ranges := [?]oc.unicode_range { + oc.UNICODE_BASIC_LATIN, + oc.UNICODE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, + oc.UNICODE_LATIN_EXTENDED_A, + oc.UNICODE_LATIN_EXTENDED_B, + oc.UNICODE_SPECIALS, + } + + oc.font_create_from_path("segoeui.ttf", 5, &ranges[0]) +} + +@(export) +oc_on_resize :: proc "c" (width, height: u32) { + frameSize.x = f32(width) + frameSize.y = f32(height) +} + +// TODO replace this after round_f64 is fixed: https://github.com/odin-lang/Odin/issues/3856 +remainder_f64 :: proc "contextless" (x, y: f64) -> f64 { + return x - round_slow(x / y) * y +} + +round_slow :: proc "contextless" (x: f64) -> f64 { + t := math.trunc(x) + if abs(x - t) >= 0.5 { + return t + math.copy_sign(1, x) + } + return t +} + +mod_f64 :: proc "contextless" (x, y: f64) -> (n: f64) { + z := abs(y) + n = remainder_f64(abs(x), z) + if math.sign(n) < 0 { + n += z + } + return math.copy_sign(n, x) +} + +@(export) +oc_on_frame_refresh :: proc "c" () { + // context = runtime.default_context() + + oc.canvas_context_select(canvas) + oc.set_color_rgba(.05, .05, .05, 1) + oc.clear() + + timestampSecs := f64(oc.clock_time(.DATE)) + secs := mod_f64(timestampSecs, 60.0) + minutes := mod_f64(timestampSecs, 60.0 * 60.0) / 60.0 + hours := mod_f64(timestampSecs, 60.0 * 60.0 * 24) / (60.0 * 60.0) + hoursAs12Format := mod_f64(hours, 12.0) + + if lastSeconds != math.floor(secs) { + lastSeconds = math.floor(secs) + + // oc.log_infof( + // "current time: %.0f:%.0f:%.0f", + // math.floor(hours), + // math.floor(minutes), + // math.floor(secs), + // ) + } + + secondsRotation := (math.PI * 2) * (secs / 60.0) - (math.PI / 2) + minutesRotation := (math.PI * 2) * (minutes / 60.0) - (math.PI / 2) + hoursRotation := (math.PI * 2) * (hoursAs12Format / 12.0) - (math.PI / 2) + + centerX := frameSize.x / 2 + centerY := frameSize.y / 2 + clockRadius := min(frameSize.x, frameSize.y) * 0.5 * 0.85 + + DEFAULT_CLOCK_RADIUS :: 260 + uiScale := clockRadius / DEFAULT_CLOCK_RADIUS + + fontSize := 26 * uiScale + oc.set_font(font) + oc.set_font_size(fontSize) + + // clock backing + oc.set_color_rgba(1, 1, 1, 1) + oc.circle_fill(centerX, centerY, clockRadius) + + // clock face + for i in 0 ..< len(clockNumberStrings) { + str := clockNumberStrings[i] + textRect := oc.font_text_metrics(font, fontSize, str).ink + + angle := f32(i) * ((math.PI * 2) / f32(12.0)) - (math.PI / 2) + transform := mat_transform( + centerX - (textRect.w / 2) - textRect.x, + centerY - (textRect.h / 2) - textRect.y, + angle, + ) + + pos := oc.mat2x3_mul(transform, {clockRadius * 0.8, 0}) + + oc.set_color_rgba(0.2, 0.2, 0.2, 1) + oc.text_fill(pos.x, pos.y, str) + } + + // hours hand + oc.matrix_multiply_push(mat_transform(centerX, centerY, f32(hoursRotation))) + { + oc.set_color_rgba(.2, 0.2, 0.2, 1) + oc.rounded_rectangle_fill(0, -7.5 * uiScale, clockRadius * 0.5, 15 * uiScale, 5 * uiScale) + } + oc.matrix_pop() + + // minutes hand + oc.matrix_multiply_push(mat_transform(centerX, centerY, f32(minutesRotation))) + { + oc.set_color_rgba(.2, 0.2, 0.2, 1) + oc.rounded_rectangle_fill(0, -5 * uiScale, clockRadius * 0.7, 10 * uiScale, 5 * uiScale) + } + oc.matrix_pop() + + // seconds hand + oc.matrix_multiply_push(mat_transform(centerX, centerY, f32(secondsRotation))) + { + oc.set_color_rgba(1, 0.2, 0.2, 1) + oc.rounded_rectangle_fill(0, -2.5 * uiScale, clockRadius * 0.8, 5 * uiScale, 5 * uiScale) + } + oc.matrix_pop() + + oc.set_color_srgba(.2, 0.2, 0.2, 1) + oc.circle_fill(centerX, centerY, 10 * uiScale) + + oc.canvas_render(renderer, canvas, surface) + oc.canvas_present(renderer, surface) +} diff --git a/orca/ui/data/OpenSans-Bold.ttf b/orca/ui/data/OpenSans-Bold.ttf new file mode 100644 index 0000000..4a5bc39 Binary files /dev/null and b/orca/ui/data/OpenSans-Bold.ttf differ diff --git a/orca/ui/data/OpenSans-Regular.ttf b/orca/ui/data/OpenSans-Regular.ttf new file mode 100644 index 0000000..29e9e60 Binary files /dev/null and b/orca/ui/data/OpenSans-Regular.ttf differ diff --git a/orca/ui/main.odin b/orca/ui/main.odin new file mode 100644 index 0000000..23d4abd --- /dev/null +++ b/orca/ui/main.odin @@ -0,0 +1,915 @@ +package src + +/* +Original Source: https://github.com/orca-app/orca/blob/main/samples/ui/src/main.c + +Can be run using in the local folder +1. odin.exe build main.odin -file -target:orca_wasm32 -out:module.wasm +2. orca bundle --name orca_output --resource-dir data module.wasm +3. orca_output\bin\orca_output.exe +*/ + +import "base:runtime" +import "core:fmt" +import oc "core:sys/orca" + +frameSize := oc.vec2{1200, 838} + +surface: oc.surface +renderer: oc.canvas_renderer +canvas: oc.canvas_context +fontRegular: oc.font +fontBold: oc.font +ui: oc.ui_context +textArena: oc.arena +logArena: oc.arena +logLines: oc.str8_list + +// NOTE(Skytrias): global instead of statics because of odin +labelFont: ^oc.font + +cmd :: enum { + NONE, + SET_DARK_THEME, + SET_LIGHT_THEME, +} + +command := cmd.NONE + +main :: proc() { + context = runtime.default_context() + oc.window_set_title("Orca UI Demo") + oc.window_set_size(frameSize) + + renderer = oc.canvas_renderer_create() + surface = oc.canvas_surface_create(renderer) + canvas = oc.canvas_context_create() + oc.ui_init(&ui) + + fonts := [2]^oc.font{&fontRegular, &fontBold} + fontNames := [2]string{"/OpenSans-Regular.ttf", "/OpenSans-Bold.ttf"} + for i in 0 ..< len(fontNames) { + scratch := oc.scratch_begin() + + file := oc.file_open(fontNames[i], {.READ}, {}) + if oc.file_last_error(file) != .OK { + oc.log_errorf("Couldn't open file %s\n", fontNames[i]) + } + size := oc.file_size(file) + buffer := cast([^]byte)oc.arena_push(scratch.arena, size) + oc.file_read(file, size, buffer) + oc.file_close(file) + ranges := [?]oc.unicode_range { + oc.UNICODE_BASIC_LATIN, + oc.UNICODE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, + oc.UNICODE_LATIN_EXTENDED_A, + oc.UNICODE_LATIN_EXTENDED_B, + oc.UNICODE_SPECIALS, + } + + fonts[i]^ = oc.font_create_from_memory(oc.str8_from_buffer(size, buffer), 5, &ranges[0]) + oc.scratch_end(scratch) + } + labelFont = &fontRegular + + oc.arena_init(&textArena) + oc.arena_init(&logArena) + oc.list_init(&logLines.list) +} + +@(export) +oc_on_raw_event :: proc "c" (event: ^oc.event) { + oc.ui_process_event(event) +} + +@(export) +oc_on_resize :: proc "c" (width, height: u32) { + frameSize.x = f32(width) + frameSize.y = f32(height) +} + +log_push :: proc(line: string) { + oc.str8_list_push(&logArena, &logLines, line) +} + +log_pushf :: proc(format: string, args: ..any) { + str := fmt.tprintf(format, ..args) + oc.str8_list_push(&logArena, &logLines, str) +} + +column_begin :: proc(header: string, widthFraction: f32) { + style := oc.ui_style { + size = {{.PARENT, widthFraction, 1, 0}, {.PARENT, 1, 0, 0}}, + layout = {axis = .Y, margin = {0, 8}, spacing = 24}, + bgColor = ui.theme.bg1, + borderColor = ui.theme.border, + borderSize = 1, + roundness = ui.theme.roundnessSmall, + } + + style_mask := + oc.SIZE + + { + .LAYOUT_AXIS, + .LAYOUT_MARGIN_Y, + .LAYOUT_SPACING, + .BG_COLOR, + .BORDER_COLOR, + .BORDER_SIZE, + .ROUNDNESS, + } + + oc.ui_style_next(style, style_mask) + oc.ui_box_begin_str8(header, {.DRAW_BACKGROUND, .DRAW_BORDER}) + + { + style = oc.ui_style { + size = {{.PARENT, 1, 0, 0}, {}}, + layout = {align = {.CENTER, .START}}, + } + oc.ui_style_next(style, {.SIZE_WIDTH, .LAYOUT_ALIGN_X}) + oc.ui_container("header", {}) + + oc.ui_style_next({fontSize = 18}, {.FONT_SIZE}) + oc.ui_label_str8(header) + } + + style = { + size = {{.PARENT, 1, 0, 0}, {.PARENT, 1, 1, 0}}, + layout = {align = {.START, .START}, margin = {16, 0}, spacing = 24}, + } + oc.ui_style_next(style, oc.SIZE + {.LAYOUT_ALIGN_X, .LAYOUT_MARGIN_X, .LAYOUT_SPACING}) + oc.ui_box_begin_str8("contents", {}) +} + +column_end :: proc() { + oc.ui_box_end() // contents + oc.ui_box_end() // column +} + +@(deferred_none = column_end) +column :: proc(header: string, widthFraction: f32) { + column_begin(header, widthFraction) +} + +labeled_slider :: proc(label: string, value: ^f32) { + oc.ui_style_next({layout = {axis = .X, spacing = 8}}, {.LAYOUT_AXIS, .LAYOUT_SPACING}) + oc.ui_container(label, {}) + + oc.ui_style_match_after( + oc.ui_pattern_owner(), + {size = {{.PIXELS, 100, 0, 0}, {}}}, + {.SIZE_WIDTH}, + ) + oc.ui_label_str8(label) + + oc.ui_style_next({size = {{.PIXELS, 100, 0, 0}, {}}}, {.SIZE_WIDTH}) + oc.ui_slider("slider", value) +} + +// reset_next_radio_group_to_dark_theme :: (oc.arena* arena) + +@(export) +oc_on_frame_refresh :: proc "c" () { + context = runtime.default_context() + scratch := oc.scratch_begin() + + #partial switch command { + case .SET_DARK_THEME: + oc.ui_set_theme(&oc.UI_DARK_THEME) + case .SET_LIGHT_THEME: + oc.ui_set_theme(&oc.UI_LIGHT_THEME) + } + command = .NONE + + defaultStyle := oc.ui_style { + font = fontRegular, + } + { + oc.ui_frame(frameSize, defaultStyle, {.FONT}) + //-------------------------------------------------------------------------------------------- + // Menu bar + //-------------------------------------------------------------------------------------------- + { + oc.ui_menu_bar("menu_bar") + + { + oc.ui_menu("File") + if oc.ui_menu_button("Quit").pressed { + oc.request_quit() + } + } + + { + oc.ui_menu("Theme") + if oc.ui_menu_button("Dark theme").pressed { + command = .SET_DARK_THEME + } + if oc.ui_menu_button("Light theme").pressed { + command = .SET_LIGHT_THEME + } + } + } + + { + oc.ui_panel("main panel", {}) + oc.ui_style_next( + { + size = {{.PARENT, 1, 0, 0}, {.PARENT, 1, 1, 0}}, + layout = {axis = .X, margin = 16, spacing = 16}, + }, + oc.SIZE + oc.LAYOUT_MARGINS + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + + { + oc.ui_container("background", {.DRAW_BACKGROUND}) + { + column("Widgets", f32(1.0) / 3) + + { + oc.ui_style_next( + {size = {{.PARENT, 1, 0, 0}, {}}, layout = {axis = .X, spacing = 32}}, + {.SIZE_WIDTH, .LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("top", {}) + + { + oc.ui_style_next( + {layout = {axis = .Y, spacing = 24}}, + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("top_left", {}) + + //----------------------------------------------------------------------------- + // Label + //----------------------------------------------------------------------------- + oc.ui_label("Label") + + //----------------------------------------------------------------------------- + // Button + //----------------------------------------------------------------------------- + if oc.ui_button("Button").clicked { + log_push("Button clicked") + } + + oc.ui_style_next( + {layout = {axis = .X, align = {{}, .CENTER}, spacing = 8}}, + {.LAYOUT_AXIS, .LAYOUT_ALIGN_Y, .LAYOUT_SPACING}, + ) + + { + oc.ui_container("checkbox", {}) + + //------------------------------------------------------------------------- + // Checkbox + //------------------------------------------------------------------------- + @(static) + checked := false + if oc.ui_checkbox("checkbox", &checked).clicked { + if checked { + log_push("Checkbox checked") + } else { + log_push("Checkbox unchecked") + } + } + + oc.ui_label("Checkbox") + } + } + + //--------------------------------------------------------------------------------- + // Vertical slider + //--------------------------------------------------------------------------------- + @(static) + vSliderValue := f32(0) + @(static) + vSliderLoggedValue := f32(0) + @(static) + vSliderLogTime := f64(0) + oc.ui_style_next({size = {{}, {.PIXELS, 130, 0, 0}}}, {.SIZE_HEIGHT}) + oc.ui_slider("v_slider", &vSliderValue) + now := oc.clock_time(.MONOTONIC) + if (now - vSliderLogTime) >= 0.2 && vSliderValue != vSliderLoggedValue { + log_pushf("Vertical slider moved to %f", vSliderValue) + vSliderLoggedValue = vSliderValue + vSliderLogTime = now + } + + { + oc.ui_style_next( + {layout = {axis = .Y, spacing = 24}}, + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("top_right", {}) + + //----------------------------------------------------------------------------- + // Tooltip + //----------------------------------------------------------------------------- + if oc.ui_label("Tooltip").hovering { + oc.ui_tooltip("Hi") + } + + //----------------------------------------------------------------------------- + // Radio group + //----------------------------------------------------------------------------- + @(static) + radioSelected: i32 = 0 + options := [?]oc.str8{"Radio 1", "Radio 2"} + radioGroupInfo := oc.ui_radio_group_info { + selectedIndex = radioSelected, + optionCount = 2, + options = &options[0], + } + result := oc.ui_radio_group("radio_group", &radioGroupInfo) + radioSelected = result.selectedIndex + if result.changed { + log_pushf("Selected Radio %i", result.selectedIndex + 1) + } + + //----------------------------------------------------------------------------- + // Horizontal slider + //----------------------------------------------------------------------------- + @(static) + hSliderValue := f32(0) + @(static) + hSliderLoggedValue := f32(0) + @(static) + hSliderLogTime := f64(0) + oc.ui_style_next({size = {{.PIXELS, 130, 0, 0}, {}}}, {.SIZE_WIDTH}) + oc.ui_slider("h_slider", &hSliderValue) + now = oc.clock_time(.MONOTONIC) + if (now - hSliderLogTime) >= 0.2 && + hSliderValue != hSliderLoggedValue { + log_pushf("Slider moved to %f", hSliderValue) + hSliderLoggedValue = hSliderValue + hSliderLogTime = now + } + } + } + + //------------------------------------------------------------------------------------- + // Text box + //------------------------------------------------------------------------------------- + oc.ui_style_next({size = {{.PIXELS, 305, 0, 0}, {.TEXT, 0, 0, 0}}}, oc.SIZE) + @(static) + text: oc.str8 + res := oc.ui_text_box("text", scratch.arena, text) + if res.changed { + oc.arena_clear(&textArena) + text = oc.str8_push_copy(&textArena, res.text) + } + if res.accepted { + log_pushf("Entered text \"%s\"", text) + } + + //------------------------------------------------------------------------------------- + // Select + //------------------------------------------------------------------------------------- + @(static) + selected: i32 = -1 + options := [?]oc.str8{"Option 1", "Option 2"} + info := oc.ui_select_popup_info { + selectedIndex = selected, + optionCount = 2, + options = &options[0], + placeholder = "Select", + } + result := oc.ui_select_popup("select", &info) + if result.selectedIndex != selected { + log_pushf("Selected %s", options[result.selectedIndex]) + } + selected = result.selectedIndex + + //------------------------------------------------------------------------------------- + // Scrollable panel + //------------------------------------------------------------------------------------- + oc.ui_style_next( + { + size = {{.PARENT, 1, 0, 0}, {.PARENT, 1, 1, 0}}, + bgColor = ui.theme.bg2, + borderColor = ui.theme.border, + borderSize = 1, + roundness = ui.theme.roundnessSmall, + }, + oc.SIZE + {.BG_COLOR, .BORDER_COLOR, .BORDER_SIZE, .ROUNDNESS}, + ) + + { + oc.ui_panel("log", {.DRAW_BACKGROUND, .DRAW_BORDER}) + + { + oc.ui_style_next( + {layout = {margin = 16}}, + oc.LAYOUT_MARGINS + {.LAYOUT_SPACING}, + ) + oc.ui_container("contents", {}) + + if oc.list_empty(logLines.list) { + oc.ui_style_next({_color = ui.theme.text2}, {.COLOR}) + oc.ui_label("Log") + } + + iter: ^oc.list_elt + i: int + for logLine in oc.list_for( + &logLines.list, + &iter, + oc.str8_elt, + "listElt", + ) { + id := fmt.tprintf("%d", i) + + oc.ui_container(id, {}) + oc.ui_label_str8(logLine.string) + + i += 1 + } + } + } + } + + //----------------------------------------------------------------------------------------- + // Styling + //----------------------------------------------------------------------------------------- + // Initial values here are hardcoded from the dark theme and everything is overridden all + // the time. In a real program you'd only override what you need and supply the values from + // ui.theme or ui.theme.palette. + // + // Rule-based styling is described at + // https://www.forkingpaths.dev/posts/23-03-10/rule_based_styling_imgui.html + { + column("Styling", f32(2.0) / 3) + @(static) + unselectedWidth := f32(16) + @(static) + unselectedHeight := f32(16) + @(static) + unselectedRoundness := f32(8) + @(static) + unselectedBgColor := oc.color{{0.086, 0.086, 0.102, 1}, .RGB} + @(static) + unselectedBorderColor := oc.color{{0.976, 0.976, 0.976, 0.35}, .RGB} + @(static) + unselectedBorderSize := f32(1) + @(static) + unselectedWhenStatus: oc.ui_status + + @(static) + selectedWidth := f32(16) + @(static) + selectedHeight := f32(16) + @(static) + selectedRoundness := f32(8) + @(static) + selectedCenterColor := oc.color{{1, 1, 1, 1}, .RGB} + @(static) + selectedBgColor := oc.color{{0.33, 0.66, 1, 1}, .RGB} + @(static) + selectedWhenStatus: oc.ui_status + + @(static) + labelFontColor := oc.color{{0.976, 0.976, 0.976, 1}, .RGB} + @(static) + labelFontSize := f32(14) + + oc.ui_style_next( + { + size = {{.PARENT, 1, 0, 0}, {.PIXELS, 152, 0, 0}}, + layout = {margin = {320, 16}}, + bgColor = oc.UI_DARK_THEME.bg0, + roundness = oc.UI_DARK_THEME.roundnessSmall, + }, + oc.SIZE + oc.LAYOUT_MARGINS + {.BG_COLOR, .ROUNDNESS}, + ) + + { + oc.ui_container("styled_radios", {.DRAW_BACKGROUND, .DRAW_BORDER}) + reset_next_radio_group_to_dark_theme(scratch.arena) + + unselectedPattern := oc.ui_pattern{} + oc.ui_pattern_push( + scratch.arena, + &unselectedPattern, + {kind = .TAG, tag = oc.ui_tag_make_str8("radio")}, + ) + + if unselectedWhenStatus != {} { + oc.ui_pattern_push( + scratch.arena, + &unselectedPattern, + {op = .AND, kind = .STATUS, status = unselectedWhenStatus}, + ) + } + oc.ui_style_match_after( + unselectedPattern, + { + size = { + {.PIXELS, unselectedWidth, 0, 0}, + {.PIXELS, unselectedHeight, 0, 0}, + }, + bgColor = unselectedBgColor, + borderColor = unselectedBorderColor, + borderSize = unselectedBorderSize, + roundness = unselectedRoundness, + }, + oc.SIZE + {.BG_COLOR, .BORDER_COLOR, .BORDER_SIZE, .ROUNDNESS}, + ) + + selectedPattern := oc.ui_pattern{} + oc.ui_pattern_push( + scratch.arena, + &selectedPattern, + {kind = .TAG, tag = oc.ui_tag_make_str8("radio_selected")}, + ) + + if selectedWhenStatus != {} { + oc.ui_pattern_push( + scratch.arena, + &selectedPattern, + {op = .AND, kind = .STATUS, status = selectedWhenStatus}, + ) + } + oc.ui_style_match_after( + selectedPattern, + { + size = { + {.PIXELS, selectedWidth, 0, 0}, + {.PIXELS, selectedHeight, 0, 0}, + }, + _color = selectedCenterColor, + bgColor = selectedBgColor, + roundness = selectedRoundness, + }, + oc.SIZE + {.COLOR, .BG_COLOR, .ROUNDNESS}, + ) + + labelPattern := oc.ui_pattern{} + labelTag := oc.ui_tag_make_str8("label") + oc.ui_pattern_push( + scratch.arena, + &labelPattern, + {kind = .TAG, tag = labelTag}, + ) + oc.ui_style_match_after( + labelPattern, + {_color = labelFontColor, font = labelFont^, fontSize = labelFontSize}, + {.COLOR, .FONT, .FONT_SIZE}, + ) + + @(static) + selectedIndex: i32 = 0 + options := [?]oc.str8{"I", "Am", "Stylish"} + radioGroupInfo := oc.ui_radio_group_info { + selectedIndex = selectedIndex, + optionCount = len(options), + options = &options[0], + } + result := oc.ui_radio_group("radio_group", &radioGroupInfo) + selectedIndex = result.selectedIndex + } + + { + oc.ui_style_next( + {layout = {axis = .X, spacing = 32}}, + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("controls", {}) + + { + oc.ui_style_next( + {layout = {axis = .Y, spacing = 16}}, + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("unselected", {}) + + oc.ui_style_next({fontSize = 16}, {.FONT_SIZE}) + oc.ui_label("Radio style") + + { + oc.ui_style_next({layout = {spacing = 4}}, {.LAYOUT_SPACING}) + oc.ui_container("size", {}) + + widthSlider := f32(unselectedWidth - 8) / 16 + labeled_slider("Width", &widthSlider) + unselectedWidth = 8 + widthSlider * 16 + + heightSlider := f32(unselectedHeight - 8) / 16 + labeled_slider("Height", &heightSlider) + unselectedHeight = 8 + heightSlider * 16 + + roundnessSlider := f32(unselectedRoundness - 4) / 8 + labeled_slider("Roundness", &roundnessSlider) + unselectedRoundness = 4 + roundnessSlider * 8 + } + + { + oc.ui_style_next({layout = {spacing = 4}}, {.LAYOUT_SPACING}) + oc.ui_container("background", {}) + + labeled_slider("Background R", &unselectedBgColor.r) + labeled_slider("Background G", &unselectedBgColor.g) + labeled_slider("Background B", &unselectedBgColor.b) + labeled_slider("Background A", &unselectedBgColor.a) + } + + { + oc.ui_style_next({layout = {spacing = 4}}, {.LAYOUT_SPACING}) + oc.ui_container("border", {}) + + labeled_slider("Border R", &unselectedBorderColor.r) + labeled_slider("Border G", &unselectedBorderColor.g) + labeled_slider("Border B", &unselectedBorderColor.b) + labeled_slider("Border A", &unselectedBorderColor.a) + } + + borderSizeSlider := f32(unselectedBorderSize) / 5 + labeled_slider("Border size", &borderSizeSlider) + unselectedBorderSize = borderSizeSlider * 5 + + { + oc.ui_style_next({layout = {spacing = 10}}, {.LAYOUT_SPACING}) + oc.ui_container("status_override", {}) + + oc.ui_label("Override") + + @(static) + statusIndex: i32 = 0 + statusOptions := [?]oc.str8 { + "Always", + "When hovering", + "When active", + } + statusInfo := oc.ui_radio_group_info { + selectedIndex = statusIndex, + optionCount = len(statusOptions), + options = &statusOptions[0], + } + result := oc.ui_radio_group("status", &statusInfo) + statusIndex = result.selectedIndex + switch statusIndex { + case 0: + unselectedWhenStatus = {} + case 1: + unselectedWhenStatus = {.HOVER} + case 2: + unselectedWhenStatus = {.ACTIVE} + } + } + } + + { + oc.ui_style_next( + {layout = {axis = .Y, spacing = 16}}, + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("selected", {}) + + oc.ui_style_next({fontSize = 16}, {.FONT_SIZE}) + oc.ui_label("Radio selected style") + + { + oc.ui_style_next({layout = {spacing = 4}}, {.LAYOUT_SPACING}) + oc.ui_container("size", {}) + + widthSlider := f32(selectedWidth - 8) / 16 + labeled_slider("Width", &widthSlider) + selectedWidth = 8 + widthSlider * 16 + + heightSlider := f32(selectedHeight - 8) / 16 + labeled_slider("Height", &heightSlider) + selectedHeight = 8 + heightSlider * 16 + + roundnessSlider := f32(selectedRoundness - 4) / 8 + labeled_slider("Roundness", &roundnessSlider) + selectedRoundness = 4 + roundnessSlider * 8 + } + + { + oc.ui_style_next({layout = {spacing = 4}}, {.LAYOUT_SPACING}) + oc.ui_container("color", {}) + + labeled_slider("Center R", &selectedCenterColor.r) + labeled_slider("Center G", &selectedCenterColor.g) + labeled_slider("Center B", &selectedCenterColor.b) + labeled_slider("Center A", &selectedCenterColor.a) + } + + { + oc.ui_style_next({layout = {spacing = 4}}, {.LAYOUT_SPACING}) + oc.ui_container("background", {}) + + labeled_slider("Background R", &selectedBgColor.r) + labeled_slider("Background G", &selectedBgColor.g) + labeled_slider("Background B", &selectedBgColor.b) + labeled_slider("Background A", &selectedBgColor.a) + } + + { + oc.ui_style_next({layout = {spacing = 10}}, {.LAYOUT_SPACING}) + oc.ui_container("status_override", {}) + + oc.ui_style_next( + {size = {{}, {.PIXELS, 30, 0, 0}}}, + {.SIZE_HEIGHT}, + ) + oc.ui_box_make_str8("spacer", {}) + oc.ui_label("Override") + + @(static) + statusIndex: i32 = 0 + statusOptions := [?]oc.str8 { + "Always", + "When hovering", + "When active", + } + statusInfo := oc.ui_radio_group_info { + selectedIndex = statusIndex, + optionCount = len(statusOptions), + options = &statusOptions[0], + } + result := oc.ui_radio_group("status", &statusInfo) + statusIndex = result.selectedIndex + switch statusIndex { + case 0: + selectedWhenStatus = {} + case 1: + selectedWhenStatus = {.HOVER} + case 2: + selectedWhenStatus = {.ACTIVE} + } + } + } + + { + oc.ui_style_next( + {layout = {axis = .Y, spacing = 16}}, + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("label", {}) + + oc.ui_style_next({fontSize = 16}, {.FONT_SIZE}) + oc.ui_label("Label style") + + { + oc.ui_style_next( + {layout = {axis = .X, spacing = 8}}, + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("font_color", {}) + + oc.ui_style_match_after( + oc.ui_pattern_owner(), + {size = {{.PIXELS, 100, 0, 0}, {}}}, + {.SIZE_WIDTH}, + ) + oc.ui_label("Font color") + + @(static) + colorSelected: i32 = 0 + colorNames := [?]oc.str8 { + "Default", + "Red", + "Orange", + "Amber", + "Yellow", + "Lime", + "Light Green", + "Green", + } + colors := [?]oc.color { + oc.UI_DARK_THEME.text0, + oc.UI_DARK_THEME.palette.red5, + oc.UI_DARK_THEME.palette.orange5, + oc.UI_DARK_THEME.palette.amber5, + oc.UI_DARK_THEME.palette.yellow5, + oc.UI_DARK_THEME.palette.lime5, + oc.UI_DARK_THEME.palette.lightGreen5, + oc.UI_DARK_THEME.palette.green5, + } + colorInfo := oc.ui_select_popup_info { + selectedIndex = colorSelected, + optionCount = len(colorNames), + options = &colorNames[0], + } + colorResult := oc.ui_select_popup("color", &colorInfo) + colorSelected = colorResult.selectedIndex + labelFontColor = colors[colorSelected] + } + + { + oc.ui_style_next( + {layout = {axis = .X, spacing = 8}}, + {.LAYOUT_AXIS, .LAYOUT_SPACING}, + ) + oc.ui_container("font", {}) + + oc.ui_style_match_after( + oc.ui_pattern_owner(), + {size = {{.PIXELS, 100, 0, 0}, {}}}, + {.SIZE_WIDTH}, + ) + oc.ui_label("Font") + + @(static) + fontSelected: i32 = 0 + fontNames := [?]oc.str8{"Regular", "Bold"} + fonts := [?]^oc.font{&fontRegular, &fontBold} + fontInfo := oc.ui_select_popup_info { + selectedIndex = fontSelected, + optionCount = len(fontNames), + options = &fontNames[0], + } + fontResult := oc.ui_select_popup("font_style", &fontInfo) + fontSelected = fontResult.selectedIndex + labelFont = fonts[fontSelected] + } + + fontSizeSlider := f32(labelFontSize - 8) / 16 + labeled_slider("Font size", &fontSizeSlider) + labelFontSize = 8 + fontSizeSlider * 16 + } + } + } + } + } + } + + oc.canvas_context_select(canvas) + + oc.set_color(ui.theme.bg0) + oc.clear() + + oc.ui_draw() + oc.canvas_render(renderer, canvas, surface) + oc.canvas_present(renderer, surface) + + oc.scratch_end(scratch) +} + +// This makes sure the light theme doesn't break the styling overrides +// You won't need it in a real program as long as your colors come from ui.theme or ui.theme.palette +reset_next_radio_group_to_dark_theme :: proc "c" (arena: ^oc.arena) { + defaultTag := oc.ui_tag_make_str8("radio") + defaultPattern := oc.ui_pattern{} + oc.ui_pattern_push(arena, &defaultPattern, {kind = .TAG, tag = defaultTag}) + defaultStyle := oc.ui_style { + borderColor = oc.UI_DARK_THEME.text3, + borderSize = 1, + } + defaultMask: oc.ui_style_mask = {.BORDER_COLOR, .BORDER_SIZE} + oc.ui_style_match_after(defaultPattern, defaultStyle, defaultMask) + + hoverPattern := oc.ui_pattern{} + oc.ui_pattern_push(arena, &hoverPattern, {kind = .TAG, tag = defaultTag}) + oc.ui_pattern_push(arena, &hoverPattern, {op = .AND, kind = .STATUS, status = {.HOVER}}) + hoverStyle := oc.ui_style { + bgColor = oc.UI_DARK_THEME.fill0, + borderColor = oc.UI_DARK_THEME.primary, + } + hoverMask: oc.ui_style_mask = {.BG_COLOR, .BORDER_COLOR} + oc.ui_style_match_after(hoverPattern, hoverStyle, hoverMask) + + activePattern := oc.ui_pattern{} + oc.ui_pattern_push(arena, &activePattern, {kind = .TAG, tag = defaultTag}) + oc.ui_pattern_push(arena, &activePattern, {op = .AND, kind = .STATUS, status = {.ACTIVE}}) + activeStyle := oc.ui_style { + bgColor = oc.UI_DARK_THEME.fill1, + borderColor = oc.UI_DARK_THEME.primary, + } + activeMask: oc.ui_style_mask = {.BG_COLOR, .BORDER_COLOR} + oc.ui_style_match_after(activePattern, activeStyle, activeMask) + + selectedTag := oc.ui_tag_make_str8("radio_selected") + selectedPattern := oc.ui_pattern{} + oc.ui_pattern_push(arena, &selectedPattern, {kind = .TAG, tag = selectedTag}) + selectedStyle := oc.ui_style { + _color = oc.UI_DARK_THEME.palette.white, + bgColor = oc.UI_DARK_THEME.primary, + } + selectedMask: oc.ui_style_mask = {.COLOR, .BG_COLOR} + oc.ui_style_match_after(selectedPattern, selectedStyle, selectedMask) + + selectedHoverPattern := oc.ui_pattern{} + oc.ui_pattern_push(arena, &selectedHoverPattern, {kind = .TAG, tag = selectedTag}) + oc.ui_pattern_push( + arena, + &selectedHoverPattern, + {op = .AND, kind = .STATUS, status = {.HOVER}}, + ) + selectedHoverStyle := oc.ui_style { + bgColor = oc.UI_DARK_THEME.primaryHover, + } + oc.ui_style_match_after(selectedHoverPattern, selectedHoverStyle, {.BG_COLOR}) + + selectedActivePattern := oc.ui_pattern{} + oc.ui_pattern_push(arena, &selectedActivePattern, {kind = .TAG, tag = selectedTag}) + oc.ui_pattern_push( + arena, + &selectedActivePattern, + {op = .AND, kind = .STATUS, status = {.ACTIVE}}, + ) + selectedActiveStyle := oc.ui_style { + bgColor = oc.UI_DARK_THEME.primaryActive, + } + oc.ui_style_match_after(selectedActivePattern, selectedActiveStyle, {.BG_COLOR}) +}