2026-09-03 10:44:30 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import "shared:android"
|
|
|
|
|
import "base:runtime"
|
|
|
|
|
import engine "../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
|
|
|
|
|
|
2026-09-04 14:26:42 +02:00
|
|
|
main :: proc() {
|
|
|
|
|
real_main()
|
|
|
|
|
}
|
2026-09-03 10:44:30 +02:00
|
|
|
|
2026-09-04 14:26:42 +02:00
|
|
|
real_main :: proc() {
|
|
|
|
|
engine.init("", "", {480, 1000}, false, "")
|
2026-09-03 10:44:30 +02:00
|
|
|
defer engine.deinit()
|
|
|
|
|
|
2026-09-04 14:26:42 +02:00
|
|
|
_, font_ok := engine.ui_text_register_font(#load("./assets/Roboto-VariableFont_wdth_wght.ttf"))
|
2026-09-03 10:44:30 +02:00
|
|
|
ensure(font_ok)
|
|
|
|
|
|
2026-09-04 14:26:42 +02:00
|
|
|
// 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()
|
|
|
|
|
|
2026-09-03 10:44:30 +02:00
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window_size := engine.get_window_size()
|
|
|
|
|
|
|
|
|
|
engine.ui_begin_build(window_size)
|
2026-09-04 14:26:42 +02:00
|
|
|
engine.ui_text_set_default_pixel_size(16) // dp
|
2026-09-03 10:44:30 +02:00
|
|
|
engine.ui_push_text_color(0xFFFFFF)
|
|
|
|
|
draw_ui()
|
|
|
|
|
engine.ui_pop_text_color()
|
|
|
|
|
engine.ui_end_build()
|
|
|
|
|
|
|
|
|
|
engine.ui_draw(engine.get_projection())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-04 14:26:42 +02:00
|
|
|
@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))
|
2026-09-03 10:44:30 +02:00
|
|
|
}
|
2026-09-04 14:26:42 +02:00
|
|
|
|
|
|
|
|
real_main()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draw_ui :: proc() {
|
|
|
|
|
m3_demo_screen()
|
2026-09-03 10:44:30 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
|
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 {
|
|
|
|
|
// 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
|
|
|
|
|
}
|
2026-09-04 14:26:42 +02:00
|
|
|
if ins, got := window_insets_get(wi, mask); got && ins != nil {
|
2026-09-03 10:44:30 +02:00
|
|
|
l := insets_left(ins) or_return
|
|
|
|
|
t := insets_top(ins) or_return
|
|
|
|
|
r := insets_right(ins) or_return
|
|
|
|
|
b := insets_bottom(ins) or_return
|
2026-09-04 14:26:42 +02:00
|
|
|
insets = to_points({f32(l), f32(t), f32(r), f32(b)})
|
2026-09-03 10:44:30 +02:00
|
|
|
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
|
2026-09-04 14:26:42 +02:00
|
|
|
insets = to_points({f32(l), f32(t), f32(r), f32(b)})
|
2026-09-03 10:44:30 +02:00
|
|
|
return true
|
|
|
|
|
}
|
2026-09-04 14:26:42 +02:00
|
|
|
|
|
|
|
|
// 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}
|
|
|
|
|
}
|