pub(crate) mod keys;
pub(crate) mod selection;
use std::{
collections::VecDeque,
ffi::c_void,
sync::{Arc, Condvar, Mutex, OnceLock, PoisonError},
time::Duration,
};
use gpui::{Pixels, Point, point, px};
use jni::{
JNIEnv, JavaVM, NativeMethod,
objects::{GlobalRef, JClass, JObject, JString, JValue},
sys::{jboolean, jfloat, jint},
};
use ndk::native_window::NativeWindow;
use crate::ffi::{self, lock};
use crate::platform::dispatcher::LoopWaker;
const SURFACE_RELEASE_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) enum ViewEvent {
SurfaceChanged {
window: NativeWindow,
width: u32,
height: u32,
density: f32,
},
SurfaceDestroyed(Arc<Released>),
Touch {
phase: i32,
id: i32,
x: f32,
y: f32,
},
Key {
down: bool,
key_code: i32,
unicode: i32,
meta: i32,
repeat: bool,
},
CommitText(String),
ComposingText(String),
FinishComposing,
DeleteSurrounding {
before: usize,
after: usize,
},
Insets {
left: i32,
top: i32,
right: i32,
bottom: i32,
ime_bottom: i32,
},
Back,
Lifecycle {
active: bool,
},
Appearance {
dark: bool,
},
LongPress {
x: f32,
y: f32,
},
EditAction(keys::EditAction),
Autofill(String),
HandleDrag {
handle: selection::Handle,
phase: selection::DragPhase,
position: Point<Pixels>,
},
}
#[derive(Default)]
pub(crate) struct Released {
done: Mutex<bool>,
condvar: Condvar,
}
impl Released {
pub(crate) fn signal(&self) {
*lock(&self.done) = true;
self.condvar.notify_all();
}
fn wait(&self) {
let done = lock(&self.done);
let (_done, timeout) = self
.condvar
.wait_timeout_while(done, SURFACE_RELEASE_TIMEOUT, |done| !*done)
.unwrap_or_else(PoisonError::into_inner);
if timeout.timed_out() {
log::warn!(
"tauri-plugin-gpui: timed out waiting for the renderer to release the surface"
);
}
}
}
#[derive(Default)]
struct Queue {
events: VecDeque<ViewEvent>,
waker: Option<Arc<LoopWaker>>,
}
static QUEUE: Mutex<Queue> = Mutex::new(Queue {
events: VecDeque::new(),
waker: None,
});
static VM: OnceLock<JavaVM> = OnceLock::new();
static VIEW: Mutex<Option<GlobalRef>> = Mutex::new(None);
static DENSITY: Mutex<f32> = Mutex::new(1.0);
static DARK: Mutex<bool> = Mutex::new(false);
fn push(event: ViewEvent) -> bool {
let mut queue = lock(&QUEUE);
queue.events.push_back(event);
match &queue.waker {
Some(waker) => {
waker.wake();
true
}
None => false,
}
}
pub(crate) fn set_waker(waker: Arc<LoopWaker>) {
let mut queue = lock(&QUEUE);
if !queue.events.is_empty() {
waker.wake();
}
queue.waker = Some(waker);
}
pub(crate) fn take_events() -> VecDeque<ViewEvent> {
std::mem::take(&mut lock(&QUEUE).events)
}
pub(crate) fn density() -> f32 {
*lock(&DENSITY)
}
pub(crate) fn is_dark() -> bool {
*lock(&DARK)
}
fn with_view(name: &str, f: impl FnOnce(&mut JNIEnv, &JObject) -> jni::errors::Result<()>) {
let Some(vm) = VM.get() else { return };
let Some(view) = lock(&VIEW).clone() else {
return;
};
let mut env = match vm.attach_current_thread_as_daemon() {
Ok(env) => env,
Err(error) => {
log::warn!("tauri-plugin-gpui: cannot attach to the JVM for {name}: {error}");
return;
}
};
if let Err(error) = f(&mut env, view.as_obj()) {
log::warn!("tauri-plugin-gpui: GpuiView.{name} failed: {error}");
let _ = env.exception_describe();
let _ = env.exception_clear();
}
}
pub(crate) fn show_keyboard() {
log::debug!("tauri-plugin-gpui: showing the soft keyboard");
with_view("showKeyboard", |env, view| {
env.call_method(view, "showKeyboard", "()V", &[]).map(drop)
});
}
pub(crate) fn hide_keyboard() {
log::debug!("tauri-plugin-gpui: hiding the soft keyboard");
with_view("hideKeyboard", |env, view| {
env.call_method(view, "hideKeyboard", "()V", &[]).map(drop)
});
}
pub(crate) fn set_back_enabled(enabled: bool) {
with_view("setBackEnabled", |env, view| {
env.call_method(
view,
"setBackEnabled",
"(Z)V",
&[JValue::Bool(enabled.into())],
)
.map(drop)
});
}
pub(crate) fn clipboard_text() -> Option<String> {
let mut text = None;
with_view("clipboardText", |env, view| {
let value = env
.call_method(view, "clipboardText", "()Ljava/lang/String;", &[])?
.l()?;
if !value.is_null() {
text = Some(env.get_string(&JString::from(value))?.into());
}
Ok(())
});
text
}
pub(crate) fn set_clipboard_text(text: &str) {
with_view("setClipboardText", |env, view| {
let text = env.new_string(text)?;
env.call_method(
view,
"setClipboardText",
"(Ljava/lang/String;)V",
&[JValue::Object(&text)],
)
.map(drop)
});
}
pub(crate) fn load_plugins() {
with_view("loadPlugins", |env, view| {
env.call_method(view, "loadPlugins", "()V", &[]).map(drop)
});
}
pub(crate) fn show_selection_toolbar(rect: selection::ViewRect, has_selection: bool) {
with_view("showSelectionToolbar", |env, view| {
env.call_method(
view,
"showSelectionToolbar",
"(IIIIZ)V",
&[
JValue::Int(rect.left),
JValue::Int(rect.top),
JValue::Int(rect.right),
JValue::Int(rect.bottom),
JValue::Bool(has_selection.into()),
],
)
.map(drop)
});
}
pub(crate) fn hide_selection_toolbar() {
with_view("hideSelectionToolbar", |env, view| {
env.call_method(view, "hideSelectionToolbar", "()V", &[])
.map(drop)
});
}
pub(crate) fn show_selection_handles(anchors: selection::HandleAnchors, collapsed: bool) {
with_view("showSelectionHandles", |env, view| {
env.call_method(
view,
"showSelectionHandles",
"(FFFFFZ)V",
&[
JValue::Float(anchors.start_x),
JValue::Float(anchors.start_y),
JValue::Float(anchors.end_x),
JValue::Float(anchors.end_y),
JValue::Float(anchors.line_height),
JValue::Bool(collapsed.into()),
],
)
.map(drop)
});
}
pub(crate) fn hide_selection_handles() {
with_view("hideSelectionHandles", |env, view| {
env.call_method(view, "hideSelectionHandles", "()V", &[])
.map(drop)
});
}
pub(crate) fn set_autofill_input(text: &str, rect: selection::ViewRect) {
with_view("setAutofillInput", |env, view| {
let text = env.new_string(text)?;
env.call_method(
view,
"setAutofillInput",
"(Ljava/lang/String;IIII)V",
&[
JValue::Object(&text),
JValue::Int(rect.left),
JValue::Int(rect.top),
JValue::Int(rect.right),
JValue::Int(rect.bottom),
],
)
.map(drop)
});
}
pub(crate) fn clear_autofill_input() {
with_view("clearAutofillInput", |env, view| {
env.call_method(view, "clearAutofillInput", "()V", &[])
.map(drop)
});
}
pub(crate) fn register_natives(env: &mut JNIEnv, activity: &JObject) -> jni::errors::Result<()> {
let name = env.new_string("app.tauri.gpui.GpuiView")?;
let class = env
.call_method(
activity,
"getAppClass",
"(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&name)],
)?
.l()?;
let class = JClass::from(class);
let method = |name: &str, sig: &str, fn_ptr: *mut c_void| NativeMethod {
name: name.into(),
sig: sig.into(),
fn_ptr,
};
env.register_native_methods(
&class,
&[
method(
"nativeAttach",
"(Lapp/tauri/gpui/GpuiView;F)V",
native_attach as *mut c_void,
),
method(
"nativeSurfaceChanged",
"(Landroid/view/Surface;IIF)V",
native_surface_changed as *mut c_void,
),
method(
"nativeSurfaceDestroyed",
"()V",
native_surface_destroyed as *mut c_void,
),
method("nativeTouch", "(IIFF)V", native_touch as *mut c_void),
method("nativeKey", "(ZIIII)V", native_key as *mut c_void),
method(
"nativeCommitText",
"(Ljava/lang/String;)V",
native_commit_text as *mut c_void,
),
method(
"nativeSetComposingText",
"(Ljava/lang/String;)V",
native_set_composing_text as *mut c_void,
),
method(
"nativeFinishComposingText",
"()V",
native_finish_composing_text as *mut c_void,
),
method(
"nativeDeleteSurroundingText",
"(II)V",
native_delete_surrounding_text as *mut c_void,
),
method("nativeInsets", "(IIIII)V", native_insets as *mut c_void),
method("nativeBack", "()V", native_back as *mut c_void),
method("nativeLifecycle", "(I)V", native_lifecycle as *mut c_void),
method("nativeAppearance", "(Z)V", native_appearance as *mut c_void),
method("nativeLongPress", "(FF)V", native_long_press as *mut c_void),
method(
"nativeEditAction",
"(I)V",
native_edit_action as *mut c_void,
),
method(
"nativeAutofill",
"(Ljava/lang/String;)V",
native_autofill as *mut c_void,
),
method(
"nativeHandleDrag",
"(IIFF)V",
native_handle_drag as *mut c_void,
),
],
)
}
extern "system" fn native_attach(env: JNIEnv, _: JClass, view: JObject, density: jfloat) {
ffi::callback("native_attach", (), || {
if let Ok(vm) = env.get_java_vm() {
let _ = VM.set(vm);
}
match env.new_global_ref(view) {
Ok(view) => *lock(&VIEW) = Some(view),
Err(error) => {
log::error!("tauri-plugin-gpui: cannot keep a reference to GpuiView: {error}")
}
}
*lock(&DENSITY) = density;
})
}
extern "system" fn native_surface_changed(
env: JNIEnv,
_: JClass,
surface: JObject,
width: jint,
height: jint,
density: jfloat,
) {
ffi::callback("native_surface_changed", (), || {
let window = unsafe { NativeWindow::from_surface(env.get_raw(), surface.as_raw()) };
let Some(window) = window else {
log::error!("tauri-plugin-gpui: ANativeWindow_fromSurface returned null");
return;
};
*lock(&DENSITY) = density;
push(ViewEvent::SurfaceChanged {
window,
width: width.max(1) as u32,
height: height.max(1) as u32,
density,
});
})
}
extern "system" fn native_surface_destroyed(_: JNIEnv, _: JClass) {
ffi::callback("native_surface_destroyed", (), || {
let released = Arc::new(Released::default());
if push(ViewEvent::SurfaceDestroyed(released.clone())) {
released.wait();
}
})
}
extern "system" fn native_touch(_: JNIEnv, _: JClass, phase: jint, id: jint, x: jfloat, y: jfloat) {
ffi::callback("native_touch", (), || {
push(ViewEvent::Touch { phase, id, x, y });
})
}
extern "system" fn native_key(
_: JNIEnv,
_: JClass,
down: jboolean,
key_code: jint,
unicode: jint,
meta: jint,
repeat: jint,
) {
ffi::callback("native_key", (), || {
push(ViewEvent::Key {
down: down != 0,
key_code,
unicode,
meta,
repeat: repeat > 0,
});
})
}
fn string(env: &mut JNIEnv, text: &JString) -> Option<String> {
env.get_string(text).ok().map(Into::into)
}
extern "system" fn native_commit_text(mut env: JNIEnv, _: JClass, text: JString) {
ffi::callback("native_commit_text", (), || {
if let Some(text) = string(&mut env, &text) {
push(ViewEvent::CommitText(text));
}
})
}
extern "system" fn native_set_composing_text(mut env: JNIEnv, _: JClass, text: JString) {
ffi::callback("native_set_composing_text", (), || {
if let Some(text) = string(&mut env, &text) {
push(ViewEvent::ComposingText(text));
}
})
}
extern "system" fn native_finish_composing_text(_: JNIEnv, _: JClass) {
ffi::callback("native_finish_composing_text", (), || {
push(ViewEvent::FinishComposing);
})
}
extern "system" fn native_delete_surrounding_text(_: JNIEnv, _: JClass, before: jint, after: jint) {
ffi::callback("native_delete_surrounding_text", (), || {
push(ViewEvent::DeleteSurrounding {
before: before.max(0) as usize,
after: after.max(0) as usize,
});
})
}
extern "system" fn native_insets(
_: JNIEnv,
_: JClass,
left: jint,
top: jint,
right: jint,
bottom: jint,
ime_bottom: jint,
) {
ffi::callback("native_insets", (), || {
push(ViewEvent::Insets {
left,
top,
right,
bottom,
ime_bottom,
});
})
}
extern "system" fn native_back(_: JNIEnv, _: JClass) {
ffi::callback("native_back", (), || {
push(ViewEvent::Back);
})
}
extern "system" fn native_lifecycle(_: JNIEnv, _: JClass, phase: jint) {
ffi::callback("native_lifecycle", (), || {
push(ViewEvent::Lifecycle { active: phase == 0 });
})
}
extern "system" fn native_appearance(_: JNIEnv, _: JClass, dark: jboolean) {
ffi::callback("native_appearance", (), || {
*lock(&DARK) = dark != 0;
push(ViewEvent::Appearance { dark: dark != 0 });
})
}
extern "system" fn native_long_press(_: JNIEnv, _: JClass, x: jfloat, y: jfloat) {
ffi::callback("native_long_press", (), || {
push(ViewEvent::LongPress { x, y });
})
}
extern "system" fn native_edit_action(_: JNIEnv, _: JClass, action: jint) {
ffi::callback("native_edit_action", (), || {
if let Some(action) = keys::EditAction::from_code(action) {
push(ViewEvent::EditAction(action));
}
})
}
extern "system" fn native_autofill(mut env: JNIEnv, _: JClass, text: JString) {
ffi::callback("native_autofill", (), || {
if let Some(text) = string(&mut env, &text) {
push(ViewEvent::Autofill(text));
}
})
}
extern "system" fn native_handle_drag(
_: JNIEnv,
_: JClass,
handle: jint,
phase: jint,
x: jfloat,
y: jfloat,
) {
ffi::callback("native_handle_drag", (), || {
let (Some(handle), Some(phase)) = (
selection::Handle::from_code(handle),
selection::DragPhase::from_code(phase),
) else {
return;
};
let scale = density().max(0.1);
push(ViewEvent::HandleDrag {
handle,
phase,
position: point(px(x / scale), px(y / scale)),
});
})
}