Text input in SDL3 on Windows 11 and terminal interference

I am just learning Odin and I successfully got the Hellope example working in SDL3. I tried to add Lua scripting by adding T as a hotkey for a Lua input string, but the Windows terminal steals focus. I tried to build it with subsystem:windows and run it from Explorer without the terminal. Hitting T has the “Lua>” prompt appear on the SDL3 window, but my typing is not echoed, and when typed blindly, it doesn’t execute a simple Lua string after hitting Enter. Should I use sokol or raylib?

Here is the code a hybrid of orignal Hellope, me, and Grok:

package sdl3_example

import sdl "vendor:sdl3"
import "core:fmt"
import "core:math"
import "core:strings"
import lua "vendor:lua/5.4"
import "base:runtime"

// Available render drivers
@require_results
get_driver_names :: proc() -> (drivers: []cstring, count: i32) {
    count = sdl.GetNumRenderDrivers()
    drivers = make([]cstring, count)
    for d in 0 ..< count {
        drivers[d] = sdl.GetRenderDriver(d)
    }
    return
}

// Return first driver found in priority list or empty cstring
set_driver_by_priority :: proc(priority_list: []cstring) -> (driver: cstring) {
    driver_list, _ := get_driver_names()
    defer delete(driver_list)
    for priority in priority_list {
        for d in driver_list {
            if d == priority {
                return priority
            }
        }
    }
    return
}

Triangle :: struct {v0, v1, v2: sdl.FPoint}

sierpinksi :: proc(r: ^sdl.Renderer, t: Triangle, depth: int) {
    if depth == 0 {
        t_raw := [4]sdl.FPoint{t.v0, t.v1, t.v2, t.v0}
        sdl.RenderLines(r, raw_data(t_raw[:]), 4)
    }
    if depth > 0 {
        sierpinksi(r, {t.v0, (t.v0 + t.v1) / 2, (t.v0 + t.v2) / 2}, depth - 1)
        sierpinksi(r, {t.v1, (t.v1 + t.v2) / 2, (t.v0 + t.v1) / 2}, depth - 1)
        sierpinksi(r, {t.v2, (t.v0 + t.v2) / 2, (t.v1 + t.v2) / 2}, depth - 1)
    }
}

// Package-level variables
s_depth: int = 5
color_paused: bool = false
vsync_enabled: bool = true
fps_cap_enabled: bool = true

lua_set_depth :: proc "c" (L: ^lua.State) -> i32 {
    context = runtime.default_context()
    n := lua.L_checkinteger(L, 1)
    if n >= 0 && n <= 9 {
        s_depth = int(n)
        fmt.printf("Lua: set_depth(%d)\n", s_depth)
    } else {
        fmt.println("Lua: depth must be 0-9")
    }
    return 0
}

main :: proc() {
    meta_ok := sdl.SetAppMetadata("Example Renderer", "1.0", "https://forum.odin-lang.org")
    sdl_ok := sdl.Init({.VIDEO})
    defer sdl.Quit()
    if !meta_ok || !sdl_ok {
        fmt.eprintln("Failed to initialize")
        return
    }

    driver: cstring
    when ODIN_OS == .Windows {
        driver = set_driver_by_priority({"direct3d12", "direct3d11", "direct3d", "gpu", "opengl", "software"})
    } else {
        driver = set_driver_by_priority({"gpu", "opengl", "software"})
    }

    window := sdl.CreateWindow("Example Renderer", 640, 480, {.RESIZABLE})
    renderer := sdl.CreateRenderer(window, driver)
    sdl.SetRenderLogicalPresentation(renderer, 640, 480, .LETTERBOX)
    defer sdl.DestroyWindow(window)
    defer sdl.DestroyRenderer(renderer)

    sdl.SetRenderVSync(renderer, 1)

    display_id := sdl.GetDisplayForWindow(window)
    display_mode := sdl.GetCurrentDisplayMode(display_id)
    refresh_rate := display_mode.refresh_rate
    fps_target := 60
    fps: f64
    color: sdl.FColor

    // Lua
    L := lua.L_newstate()
    lua.L_openlibs(L)
    defer lua.close(L)
    lua.register(L, "set_depth", lua_set_depth)
    lua.L_dostring(L, `function help() print("set_depth(n) -- n=0..9") end`)

    drivers, _ := get_driver_names()
    defer delete(drivers)

    controls := [][]cstring {
        {"Quit", "Q", "ESC"},
        {"Pause Color", "P", "LMB"},
        {"Toggle Vsync", "V", ""},
        {"Toggle FPS Cap", "F", ""},
        {"Triangle Depth", "0-9", ""},
        {"Lua Input", "T", ""},
    }

    input_active := false
    input_buffer: [256]byte
    input_len := 0

    main_loop: for {
        frame_start := sdl.GetTicksNS()

        for e: sdl.Event; sdl.PollEvent(&e); {
            #partial switch e.type {
            case .QUIT, .WINDOW_CLOSE_REQUESTED:
                break main_loop

            case .KEY_UP:
                switch e.key.key {
                case sdl.K_0..=sdl.K_9:
                    s_depth = int(e.key.key - sdl.K_0)
                case sdl.K_T:
                    input_active = !input_active
                    if input_active {
                        input_len = 0
                    }
                case sdl.K_P:
                    color_paused = !color_paused
                case sdl.K_ESCAPE, sdl.K_Q:
                    if input_active {
                        input_active = false
                    } else {
                        break main_loop
                    }
                case sdl.K_V:
                    vsync_enabled = !vsync_enabled
                    sdl.SetRenderVSync(renderer, vsync_enabled ? 1 : sdl.RENDERER_VSYNC_DISABLED)
                case sdl.K_F:
                    fps_cap_enabled = !fps_cap_enabled
                }
            case .TEXT_INPUT:
                if input_active {
                    txt := e.text.text
                    if txt != nil {
                        added := strings.clone_from_cstring(txt, context.temp_allocator)
                        for c in added {
                            if input_len >= len(input_buffer)-1 { break }
                            input_buffer[input_len] = u8(c)
                            input_len += 1
                        }
                    }
                }
            case .MOUSE_BUTTON_UP:
                if e.button.button == sdl.BUTTON_LEFT {
                    color_paused = !color_paused
                }
            }
        }

        if !color_paused {
            now := f64(frame_start) / 1000000000.0
            color.r = f32(0.5 + 0.5 * sdl.sin(now))
            color.g = f32(0.5 + 0.5 * sdl.sin(now + math.PI * 2 / 3))
            color.b = f32(0.5 + 0.5 * sdl.sin(now + math.PI * 4 / 3))
            color.a = sdl.ALPHA_OPAQUE_FLOAT
        }

        sdl.SetRenderDrawColorFloat(renderer, color.r, color.g, color.b, color.a)
        sdl.RenderClear(renderer)

        sdl.SetRenderDrawColorFloat(renderer, 1 - color.r, 1 - color.g, 1 - color.b, 255)
        t := Triangle{{366, 20}, {112, 460}, {620, 460}}
        sierpinksi(renderer, t, s_depth)

        r: f32
        row :: proc(row: ^f32, height: f32) -> f32 { row^ += height; return row^ }

        sdl.SetRenderDrawColor(renderer, 0, 0, 0, 255)
        sdl.RenderDebugText(renderer, 10, row(&r, 10), "hellope world!")
        sdl.RenderDebugText(renderer, 10, row(&r, 20), fmt.ctprintf("%-16s%v", "Triangle Depth:", s_depth))
        sdl.RenderDebugText(renderer, 10, row(&r, 10), fmt.ctprintf("%-16s%v", "Color Paused:", color_paused))
        sdl.RenderDebugText(renderer, 10, row(&r, 10), fmt.ctprintf("%-16s%v", "VSync Enabled:", vsync_enabled))
        sdl.RenderDebugText(renderer, 10, row(&r, 10), fmt.ctprintf("%-16s%v", "Refresh Rate:", refresh_rate))
        sdl.RenderDebugText(renderer, 10, row(&r, 10), fmt.ctprintf("%-16s%v", "FPS Capped:", fps_cap_enabled))
        sdl.RenderDebugText(renderer, 10, row(&r, 10), fmt.ctprintf("%-16s%i", "FPS Target:", fps_target))
        sdl.RenderDebugText(renderer, 10, row(&r, 10), fmt.ctprintf("%-16s%.2f", "FPS Current:", fps))

        if input_active {
            input_str := fmt.ctprintf("Lua> %s", cstring(&input_buffer[0]))
            sdl.RenderDebugText(renderer, 10, 380, input_str)
        }

        free_all(context.temp_allocator)
        sdl.RenderPresent(renderer)

        frame_end := sdl.GetTicksNS()
        npf_target := u64(1000000000 / fps_target)
        if fps_cap_enabled && (frame_end - frame_start) < npf_target {
            sdl.DelayPrecise(npf_target - (frame_end - frame_start))
            frame_end = sdl.GetTicksNS()
        }
        fps = 1000000000.0 / f64(frame_end - frame_start)
    }
}

In SDL3, unlike in previous versions, you don’t get text input events automatically. You need to call ‘sdl.StartTextInput(window)’ and ‘sdl.StopTextInput(window)’ when you’re done with it.

1 Like

Thank you. I will try this tonight, and get back to you. Do you think Raylib is easier with Odin?