package main import "shared:android" import "base:runtime" import "core:time" import engine "shared:zephr" import "jni" device_model: string device_sdk: i32 toast_ctx: Context main_activity: Activity Edge_Insets :: struct { left, top, right, bottom: f32 } insets: Edge_Insets insets_valid: bool main :: proc() { // Android must not start the app from here. This library's DT_INIT points at // Odin's _odin_entry_point, which the dynamic loader runs the moment // NativeActivity dlopens libmain.so -- long before the glue spawns its // thread and calls android_main, and so before context.user_ptr holds the // android_app. Running real_main here reaches backend_init with a nil app // and faults writing app.onAppCmd. // // The proc still has to exist: package main requires it, and it is the // desktop entry point. when ODIN_PLATFORM_SUBTARGET != .Android { real_main() } } real_main :: proc() { engine.init("", "", {480, 1000}, false, "") defer engine.deinit() _, font_ok := engine.ui_text_register_font(#load("./assets/Roboto-VariableFont_wdth_wght.ttf")) ensure(font_ok) // Material Symbols, subset to the glyphs the app bar uses: 1.3KB of a 356KB // font. Its codepoints are Private Use Area, so nothing else claims them and // the shaper's per-codepoint fallback routes them here on its own. ☰ and ⋮ // were rendering as tofu because Roboto has no glyph for either. _, icon_font_ok := engine.ui_text_register_font(#load("./assets/material_icons.ttf")) ensure(icon_font_ok) m3_init_dark() // Only draw when something actually changed. Without this the loop renders // flat out forever, which on a phone is straight battery burn. engine.set_render_on_change(true) // Rolling work-vs-swap timings to logcat, to tell a slow frame apart from a // frame that is merely waiting on vsync. engine.set_frame_stats(true) for !engine.should_quit() { engine.frame_start() defer engine.frame_end() e := engine.iter_events() for e != nil { #partial switch e.type { case .WINDOW_CLOSED: engine.quit() case .WINDOW_RESIZED: insets_valid = false } e = engine.iter_events() } if !insets_valid { insets_valid = refresh_insets() if !insets_valid { // The decor view attaches asynchronously and emits no event when // it does, so the retry has to drive itself. // // Deliberately request_render_at, not request_render: a plain // request made here -- before the build -- is consumed by the // very frame it is running in, so it schedules nothing and the // loop parks with insets still unresolved. A timed wake is the // one that survives into the next frame. It only works today // because the first frames redraw anyway while the tree settles. engine.request_render_at(engine.get_time() + 16 * time.Millisecond) } } window_size := engine.get_window_size() engine.ui_begin_build(window_size) engine.ui_text_set_default_pixel_size(16) // dp engine.ui_push_text_color(0xFFFFFF) draw_ui() engine.ui_pop_text_color() engine.ui_end_build() engine.ui_draw(engine.get_projection()) } } @export android_main :: proc "c" (state: ^android.android_app) { context = runtime.default_context() context.user_ptr = state // Static field reads only -- safe from the engine's thread. Anything // touching the UI toolkit (Toast, dialogs) needs the main looper instead. if jni.init(state) { device_model, _ = build_model() device_sdk, _ = build_sdk_int() toast_ctx = Context(jni.activity(state)) main_activity = Activity(jni.activity(state)) } real_main() } draw_ui :: proc() { m3_demo_screen() } // Toast needs a Java looper on this thread. Preparing one is a one-time cost // and the native ALooper the engine polls is unaffected -- they're separate. show_toast :: proc(msg: string) { // The bindings all fail closed without a VM, so this would be harmless off // Android -- just a pointless chain of JNI calls on every tap. Compile it // out instead of relying on that. when ODIN_PLATFORM_SUBTARGET == .Android { if l, ok := looper_my_looper(); ok && l == nil { looper_prepare() } toast, ok := toast_make_text(toast_ctx, msg, 0) // 0 = Toast.LENGTH_SHORT if !ok { return } toast_show(toast) } } // Walks Activity -> Window -> decor View -> WindowInsets. Returns false while // the decor view is still unattached, which is normal for the first few frames. refresh_insets :: proc() -> bool { // Returning true, not false: zero insets are the correct answer where there // are no system bars, and the caller retries every frame until this succeeds // -- requesting a render each time. Failing here would pin the desktop loop // awake forever, which is exactly what render_on_change is meant to avoid. when ODIN_PLATFORM_SUBTARGET != .Android { return true } // This runs every frame until it succeeds, and each call below returns a // local ref. Without a frame to scope them the ref table fills and aborts. env := jni.env() if !jni.push_frame(env) { return false } defer jni.pop_frame(env) win := activity_get_window(main_activity) or_return decor := window_get_decor_view(win) or_return wi := view_get_root_window_insets(decor) or_return if wi == nil { return false } // API 30+ typed insets. Below that both calls fail and we fall through. if mask, ok := insets_type_system_bars(); ok { if cut, got_cut := insets_type_display_cutout(); got_cut { mask |= cut } if ins, got := window_insets_get(wi, mask); got && ins != nil { l := insets_left(ins) or_return t := insets_top(ins) or_return r := insets_right(ins) or_return b := insets_bottom(ins) or_return insets = to_points({f32(l), f32(t), f32(r), f32(b)}) return true } } l := window_insets_sys_left(wi) or_return t := window_insets_sys_top(wi) or_return r := window_insets_sys_right(wi) or_return b := window_insets_sys_bottom(wi) or_return insets = to_points({f32(l), f32(t), f32(r), f32(b)}) return true } // WindowInsets are reported in physical pixels, but layout is in dp. to_points :: proc(v: Edge_Insets) -> Edge_Insets { s := engine.get_backing_scale() if s <= 0 { s = 1 } return {v.left / s, v.top / s, v.right / s, v.bottom / s} }