use std::sync::{Once, OnceLock};
use jni::objects::{JObject, JString};
use jni::refs::Global;
use jni::{jni_sig, jni_str};
use crate::NativeWidgetError;
use crate::android::ctx::NativeCtx;
use crate::controls::typeface::{
RegistrationAction, Typeface, cache_key, degrade_on_failure, plan_registration,
};
const TYPEFACE_CLASS: &str = "android.graphics.Typeface";
struct GlyphBytes {
mono: &'static [u8],
plex: &'static [u8],
}
static BYTES: OnceLock<GlyphBytes> = OnceLock::new();
pub(crate) fn set_glyph_bytes(mono: &'static [u8], plex: &'static [u8]) {
let _ = BYTES.set(GlyphBytes { mono, plex });
}
static MONO: OnceLock<Global<JObject<'static>>> = OnceLock::new();
static PLEX: OnceLock<Global<JObject<'static>>> = OnceLock::new();
static WARNED: Once = Once::new();
pub(crate) fn typeface_for(
ctx: &mut NativeCtx<'_, '_>,
typeface: Typeface,
) -> Option<&'static JObject<'static>> {
match typeface {
Typeface::System => None,
Typeface::GlyphMono => resolve(ctx, &MONO, "glyphMono", |bytes| bytes.mono),
Typeface::GlyphPlex => resolve(ctx, &PLEX, "glyphPlex", |bytes| bytes.plex),
}
}
fn resolve(
ctx: &mut NativeCtx<'_, '_>,
cache: &'static OnceLock<Global<JObject<'static>>>,
face_id: &str,
pick: impl FnOnce(&GlyphBytes) -> &'static [u8],
) -> Option<&'static JObject<'static>> {
if let Some(cached) = cache.get() {
return Some(cached.as_obj());
}
let bytes = pick(BYTES.get()?);
let global = degrade_on_failure(register(ctx, face_id, bytes), |e| warn_once(face_id, e))?;
Some(cache.get_or_init(|| global).as_obj())
}
fn warn_once(face_id: &str, error: &NativeWidgetError) {
WARNED.call_once(|| {
log::warn!(
"frust-native-widgets: could not register the Glyph {face_id} typeface ({error}) — \
degrading to the platform's system typeface (see docs/ARCHITECTURE.md's \
`frust-native-widgets` row for the theme ladder)"
);
});
}
fn register(
ctx: &mut NativeCtx<'_, '_>,
face_id: &str,
bytes: &'static [u8],
) -> Result<Global<JObject<'static>>, NativeWidgetError> {
let path = cache_file_path(ctx, face_id, bytes)?;
match plan_registration(std::path::Path::new(&path).exists()) {
RegistrationAction::ReuseExisting => {}
RegistrationAction::WriteThenCreate => write_atomic(&path, bytes)?,
}
create_from_file(ctx, &path)
}
fn cache_file_path(
ctx: &mut NativeCtx<'_, '_>,
face_id: &str,
bytes: &[u8],
) -> Result<String, NativeWidgetError> {
let dir = cache_dir_path(ctx)?;
Ok(format!("{dir}/{}", cache_key(face_id, bytes)))
}
fn cache_dir_path(ctx: &mut NativeCtx<'_, '_>) -> Result<String, NativeWidgetError> {
let context = ctx.context()?;
let dir = ctx.run_jni("Context.getCacheDir", |env| {
env.call_method(
context,
jni_str!("getCacheDir"),
jni_sig!("()Ljava/io/File;"),
&[],
)?
.l()
})?;
ctx.run_jni("File.getAbsolutePath", |env| {
let path = env
.call_method(
&dir,
jni_str!("getAbsolutePath"),
jni_sig!("()Ljava/lang/String;"),
&[],
)?
.l()?;
let path = env.cast_local::<JString>(path)?;
path.try_to_string(env)
})
}
fn write_atomic(path: &str, bytes: &[u8]) -> Result<(), NativeWidgetError> {
let tmp = format!("{path}.tmp-{}", std::process::id());
std::fs::write(&tmp, bytes)
.map_err(|e| NativeWidgetError::Platform(format!("writing {tmp}: {e}")))?;
std::fs::rename(&tmp, path)
.map_err(|e| NativeWidgetError::Platform(format!("renaming {tmp} to {path}: {e}")))
}
fn create_from_file(
ctx: &mut NativeCtx<'_, '_>,
path: &str,
) -> Result<Global<JObject<'static>>, NativeWidgetError> {
let class = ctx.class(TYPEFACE_CLASS)?;
let jpath = ctx.run_jni("NewStringUTF(typeface path)", |env| env.new_string(path))?;
let typeface = ctx.run_jni("Typeface.createFromFile", |env| {
env.call_static_method(
&class,
jni_str!("createFromFile"),
jni_sig!("(Ljava/lang/String;)Landroid/graphics/Typeface;"),
&[jni::objects::JValue::Object(&jpath)],
)?
.l()
})?;
if typeface.is_null() {
return Err(NativeWidgetError::Platform(format!(
"Typeface.createFromFile({path}) returned null"
)));
}
ctx.retain(&typeface)
}