Residual scroll jank at 120 Hz (suspected Slint event-loop bug) needs per-frame + per-stage timing the logcat ring buffer can't hold. New cargo feature `perf` (default OFF — all sites are cfg-gated, zero overhead when off): - FR: per-frame app-side render cost + frame gap, from the rendering notifier (BeforeRendering/AfterRendering). - STG: event-loop stage costs (wake/timers/events/poll/lastrender) from timestamps instrumented into the pinned vendor copy of i-slint-backend-android-activity (stg.rs; Cargo.toml now depends on it directly + optional under `perf`). - DECODE: per-thumbnail-job cost on the workers. - All lines mirrored to /sdcard/Download/.gallery_perf.log (logcat rotates too fast at ~120 lines/s). Format + reading guide: docs/BUGS/scroll-jank-120hz.md.
856 lines
31 KiB
Rust
856 lines
31 KiB
Rust
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
|
|
|
|
// cSpell: ignore dalvik jboolean jfloat jint
|
|
use super::*;
|
|
use i_slint_common::unicode_utils::{
|
|
byte_offset_to_utf16_offset, utf16_offset_to_byte_offset_clamped,
|
|
};
|
|
use i_slint_core::SharedString;
|
|
use i_slint_core::api::{PhysicalPosition, PhysicalSize};
|
|
use i_slint_core::graphics::{Color, euclid};
|
|
use i_slint_core::input::{InternalKeyEvent, KeyEvent, KeyEventType};
|
|
use i_slint_core::item_rendering::HasFont;
|
|
use i_slint_core::items::{CapitalizationMode, ColorScheme, InputType};
|
|
use i_slint_core::lengths::{LogicalLength, PhysicalEdges};
|
|
use i_slint_core::platform::{Key, WindowAdapter, WindowEvent, WindowEventDispatchResult};
|
|
use jni::objects::{JClass, JClassLoader, JString, LoaderContext};
|
|
use jni::sys::{jfloat, jint};
|
|
use jni::{Env, JavaVM, bind_java_type};
|
|
use std::sync::OnceLock;
|
|
use std::time::Duration;
|
|
|
|
/// Maps an Android `Configuration.fontScale` to a Slint default font size.
|
|
/// 14 LP = Material Design body-text size at `fontScale = 1.0`.
|
|
pub(crate) fn font_scale_to_logical_length(font_scale: f32) -> Option<LogicalLength> {
|
|
(font_scale.is_finite() && font_scale > 0.0).then(|| LogicalLength::new(14.0 * font_scale))
|
|
}
|
|
|
|
const DEX_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/classes.dex"));
|
|
|
|
bind_java_type! {
|
|
SlintAndroidJavaHelper => ".SlintAndroidJavaHelper",
|
|
type_map = {
|
|
AndroidActivity => "android.app.Activity",
|
|
AndroidRect => "android.graphics.Rect",
|
|
},
|
|
constructors {
|
|
fn new(activity: AndroidActivity),
|
|
},
|
|
methods {
|
|
fn accent_color {
|
|
name = "accent_color",
|
|
sig = () -> jint,
|
|
},
|
|
fn color_scheme {
|
|
name = "color_scheme",
|
|
sig = () -> jint,
|
|
},
|
|
fn font_scale {
|
|
name = "font_scale",
|
|
sig = () -> jfloat,
|
|
},
|
|
fn get_clipboard {
|
|
name = "get_clipboard",
|
|
sig = () -> JString,
|
|
},
|
|
fn get_safe_area {
|
|
name = "get_safe_area",
|
|
sig = () -> AndroidRect,
|
|
},
|
|
fn get_view_rect {
|
|
name = "get_view_rect",
|
|
sig = () -> AndroidRect,
|
|
},
|
|
fn finish_activity {
|
|
name = "finish_activity",
|
|
sig = (),
|
|
},
|
|
fn hide_keyboard {
|
|
name = "hide_keyboard",
|
|
sig = (),
|
|
},
|
|
fn set_clipboard {
|
|
name = "set_clipboard",
|
|
sig = (text: JString),
|
|
},
|
|
fn set_handle_color {
|
|
name = "set_handle_color",
|
|
sig = (color: jint),
|
|
},
|
|
fn set_imm_data {
|
|
name = "set_imm_data",
|
|
sig = (
|
|
text: JString,
|
|
cursor_position: jint,
|
|
anchor_position: jint,
|
|
preedit_start: jint,
|
|
preedit_end: jint,
|
|
cur_x: jint,
|
|
cur_y: jint,
|
|
anchor_x: jint,
|
|
anchor_y: jint,
|
|
cursor_height: jint,
|
|
input_type: jint,
|
|
show_cursor_handles: jboolean
|
|
),
|
|
},
|
|
fn show_action_menu {
|
|
name = "show_action_menu",
|
|
sig = (),
|
|
},
|
|
fn show_keyboard {
|
|
name = "show_keyboard",
|
|
sig = (),
|
|
},
|
|
},
|
|
native_methods_export = false,
|
|
native_methods {
|
|
pub static fn move_cursor_handle {
|
|
sig = (id: jint, pos_x: jint, pos_y: jint) -> (),
|
|
fn = callback_move_cursor_handle,
|
|
},
|
|
pub static fn on_back_invoked {
|
|
sig = () -> (),
|
|
fn = callback_on_back_invoked,
|
|
},
|
|
pub static fn popup_menu_action {
|
|
sig = (id: jint) -> (),
|
|
fn = callback_popup_menu_action,
|
|
},
|
|
pub static fn set_insets {
|
|
sig = (
|
|
window_top: jint,
|
|
window_left: jint,
|
|
window_bottom: jint,
|
|
window_right: jint,
|
|
safe_area_top: jint,
|
|
safe_area_left: jint,
|
|
safe_area_bottom: jint,
|
|
safe_area_right: jint,
|
|
keyboard_top: jint,
|
|
keyboard_left: jint,
|
|
keyboard_bottom: jint,
|
|
keyboard_right: jint
|
|
) -> (),
|
|
fn = callback_set_insets,
|
|
},
|
|
pub static fn set_night_mode {
|
|
sig = (night_mode: jint) -> (),
|
|
fn = callback_set_night_mode,
|
|
},
|
|
pub static fn set_font_scale {
|
|
sig = (font_scale: jfloat) -> (),
|
|
fn = callback_set_font_scale,
|
|
},
|
|
pub static fn update_text {
|
|
sig = (
|
|
text: JString,
|
|
cursor_position: jint,
|
|
anchor_position: jint,
|
|
preedit_start: jint,
|
|
preedit_offset: jint
|
|
) -> (),
|
|
fn = callback_update_text,
|
|
},
|
|
},
|
|
}
|
|
|
|
bind_java_type! {
|
|
AndroidActivity => "android.app.Activity",
|
|
type_map = {
|
|
AndroidContext => "android.content.Context",
|
|
},
|
|
is_instance_of = {
|
|
AndroidContext,
|
|
}
|
|
}
|
|
|
|
bind_java_type! {
|
|
AndroidContext => "android.content.Context",
|
|
type_map = {
|
|
JFile => "java.io.File",
|
|
},
|
|
methods {
|
|
fn get_files_dir() -> JFile,
|
|
fn get_cache_dir() -> JFile,
|
|
fn get_code_cache_dir() -> JFile, // requires API level >= 21
|
|
fn get_class_loader() -> JClassLoader,
|
|
fn get_package_name() -> JString,
|
|
}
|
|
}
|
|
|
|
bind_java_type! {
|
|
AndroidBuildVersion => "android.os.Build$VERSION",
|
|
fields {
|
|
#[allow(non_snake_case)]
|
|
static SDK_INT {
|
|
sig = jint,
|
|
get = SDK_INT,
|
|
},
|
|
},
|
|
}
|
|
|
|
bind_java_type! {
|
|
AndroidRect => "android.graphics.Rect",
|
|
fields {
|
|
bottom: jint,
|
|
left: jint,
|
|
right: jint,
|
|
top: jint,
|
|
},
|
|
}
|
|
|
|
bind_java_type! {
|
|
AndroidInputType => "android.text.InputType",
|
|
fields {
|
|
#[allow(non_snake_case)]
|
|
static TYPE_CLASS_NUMBER {
|
|
sig = jint,
|
|
get = TYPE_CLASS_NUMBER,
|
|
},
|
|
#[allow(non_snake_case)]
|
|
static TYPE_CLASS_TEXT {
|
|
sig = jint,
|
|
get = TYPE_CLASS_TEXT,
|
|
},
|
|
#[allow(non_snake_case)]
|
|
static TYPE_TEXT_VARIATION_PASSWORD {
|
|
sig = jint,
|
|
get = TYPE_TEXT_VARIATION_PASSWORD,
|
|
},
|
|
#[allow(non_snake_case)]
|
|
static TYPE_NUMBER_FLAG_DECIMAL {
|
|
sig = jint,
|
|
get = TYPE_NUMBER_FLAG_DECIMAL,
|
|
},
|
|
#[allow(non_snake_case)]
|
|
static TYPE_TEXT_FLAG_CAP_SENTENCES {
|
|
sig = jint,
|
|
get = TYPE_TEXT_FLAG_CAP_SENTENCES,
|
|
},
|
|
#[allow(non_snake_case)]
|
|
static TYPE_TEXT_FLAG_CAP_WORDS {
|
|
sig = jint,
|
|
get = TYPE_TEXT_FLAG_CAP_WORDS,
|
|
},
|
|
#[allow(non_snake_case)]
|
|
static TYPE_TEXT_FLAG_CAP_CHARACTERS {
|
|
sig = jint,
|
|
get = TYPE_TEXT_FLAG_CAP_CHARACTERS,
|
|
},
|
|
#[allow(non_snake_case)]
|
|
static TYPE_TEXT_FLAG_AUTO_CORRECT {
|
|
sig = jint,
|
|
get = TYPE_TEXT_FLAG_AUTO_CORRECT,
|
|
},
|
|
#[allow(non_snake_case)]
|
|
static TYPE_TEXT_FLAG_AUTO_COMPLETE {
|
|
sig = jint,
|
|
get = TYPE_TEXT_FLAG_AUTO_COMPLETE,
|
|
},
|
|
}
|
|
}
|
|
|
|
bind_java_type! {
|
|
AndroidViewConfiguration => "android.view.ViewConfiguration",
|
|
methods {
|
|
static fn get_long_press_timeout() -> jint,
|
|
}
|
|
}
|
|
|
|
bind_java_type! {
|
|
JFile => "java.io.File",
|
|
methods {
|
|
fn get_absolute_path() -> JString,
|
|
}
|
|
}
|
|
|
|
bind_java_type! {
|
|
InMemoryDexClassLoader => "dalvik.system.InMemoryDexClassLoader",
|
|
constructors {
|
|
fn new(dex_buffer: JByteBuffer, parent: JClassLoader),
|
|
},
|
|
is_instance_of = {
|
|
JClassLoader,
|
|
}
|
|
}
|
|
|
|
bind_java_type! {
|
|
DexFileClassLoader => "dalvik.system.DexClassLoader",
|
|
constructors {
|
|
fn new(dex_path: JString, optimized_directory: JString, library_search_path: JString, parent: JClassLoader),
|
|
},
|
|
is_instance_of = {
|
|
JClassLoader,
|
|
}
|
|
}
|
|
|
|
// See `AttachmentExceptionPolicy::PreReThrowPostCatch` in `jni` crate.
|
|
#[track_caller]
|
|
pub fn print_jni_error(_app: &AndroidApp, e: jni::errors::Error) -> ! {
|
|
panic!("JNI error: {e:#?}")
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub struct JavaHelper(jni::refs::Global<SlintAndroidJavaHelper<'static>>, AndroidApp);
|
|
|
|
fn get_helper_class_loader(
|
|
env: &mut Env,
|
|
native_activity: &AndroidActivity<'_>,
|
|
) -> Result<&'static JClassLoader<'static>, jni::errors::Error> {
|
|
static DEX_CLASS_LOADER: OnceLock<jni::refs::Global<JClassLoader<'static>>> = OnceLock::new();
|
|
|
|
fn build_dex_class_loader<'local>(
|
|
env: &mut Env<'local>,
|
|
native_activity: &AndroidActivity<'_>,
|
|
) -> Result<JClassLoader<'local>, jni::errors::Error> {
|
|
let native_activity = env.new_local_ref(native_activity)?;
|
|
let app_context = AndroidContext::cast_local(env, native_activity)?;
|
|
let context_class_loader = app_context.get_class_loader(env)?;
|
|
|
|
if AndroidBuildVersion::SDK_INT(env)? >= 26 {
|
|
// Safety: DEX_DATA is 'static and the `InMemoryDexClassLoader`` will not mutate it
|
|
let dex_buffer =
|
|
unsafe { env.new_direct_byte_buffer(DEX_DATA.as_ptr() as *mut _, DEX_DATA.len()) }?;
|
|
let dex_loader = InMemoryDexClassLoader::new(env, &dex_buffer, &context_class_loader)?;
|
|
JClassLoader::cast_local(env, dex_loader)
|
|
} else {
|
|
// The dex data must be written in a file; this determines the output
|
|
// directory path inside the application code cache directory.
|
|
let code_cache_path = app_context
|
|
.get_code_cache_dir(env)?
|
|
.get_absolute_path(env)
|
|
.map(|p| std::path::PathBuf::from(p.to_string()))?;
|
|
|
|
let dex_name = env!("CARGO_CRATE_NAME").to_string() + ".dex";
|
|
let dex_file_path = code_cache_path.join(dex_name);
|
|
std::fs::write(&dex_file_path, DEX_DATA).unwrap(); // Note: this panics on failure
|
|
let dex_file_path = JString::new(env, dex_file_path.to_string_lossy())?;
|
|
|
|
// creates the oats directory
|
|
let oats_dir_path = code_cache_path.join("oats");
|
|
let _ = std::fs::create_dir(&oats_dir_path);
|
|
let oats_dir_path = JString::new(env, oats_dir_path.to_string_lossy())?;
|
|
|
|
// loads the dex file
|
|
let dex_loader = DexFileClassLoader::new(
|
|
env,
|
|
&dex_file_path,
|
|
&oats_dir_path,
|
|
JString::null(),
|
|
&context_class_loader,
|
|
)?;
|
|
JClassLoader::cast_local(env, dex_loader)
|
|
}
|
|
}
|
|
|
|
if DEX_CLASS_LOADER.get().is_none() {
|
|
let loader = build_dex_class_loader(env, native_activity)?;
|
|
let loader = env.new_global_ref(loader)?;
|
|
let _ = DEX_CLASS_LOADER.set(loader);
|
|
}
|
|
Ok(DEX_CLASS_LOADER.get().unwrap())
|
|
}
|
|
|
|
fn load_java_helper(
|
|
app: &AndroidApp,
|
|
) -> Result<jni::refs::Global<SlintAndroidJavaHelper<'static>>, jni::errors::Error> {
|
|
let jvm = JavaVM::singleton().unwrap_or_else(|_| unsafe {
|
|
// Safety: as documented in android-activity to obtain a jni::JavaVM
|
|
JavaVM::from_raw(app.vm_as_ptr() as *mut _) // this initializes the `JavaVM::singleton()`
|
|
});
|
|
jvm.attach_current_thread(|env| {
|
|
let native_activity_ptr = app.activity_as_ptr().cast();
|
|
let native_activity =
|
|
unsafe { env.as_cast_raw::<jni::refs::Global<AndroidActivity>>(&native_activity_ptr)? };
|
|
let loader = LoaderContext::Loader(get_helper_class_loader(env, native_activity.as_ref())?);
|
|
let _ = SlintAndroidJavaHelperAPI::get(env, &loader)?;
|
|
let helper_instance = SlintAndroidJavaHelper::new(env, native_activity)?;
|
|
env.new_global_ref(&helper_instance)
|
|
})
|
|
}
|
|
|
|
impl JavaHelper {
|
|
pub fn new(app: &AndroidApp) -> Result<Self, jni::errors::Error> {
|
|
Ok(Self(load_java_helper(app)?, app.clone()))
|
|
}
|
|
|
|
fn with_jni_env<R>(
|
|
&self,
|
|
f: impl FnOnce(&mut Env, &SlintAndroidJavaHelper<'static>) -> Result<R, jni::errors::Error>,
|
|
) -> Result<R, jni::errors::Error> {
|
|
JavaVM::singleton()?.attach_current_thread(|env| {
|
|
let helper = self.0.as_ref();
|
|
f(env, helper)
|
|
})
|
|
}
|
|
|
|
/// Unfortunately, the way that the android-activity crate uses to show or hide the virtual keyboard doesn't
|
|
/// work with native-activity. So do it manually with JNI
|
|
pub fn show_or_hide_soft_input(&self, show: bool) -> Result<(), jni::errors::Error> {
|
|
self.with_jni_env(
|
|
|env, helper| {
|
|
if show { helper.show_keyboard(env) } else { helper.hide_keyboard(env) }
|
|
},
|
|
)
|
|
}
|
|
|
|
pub fn set_imm_data(
|
|
&self,
|
|
data: &i_slint_core::window::InputMethodProperties,
|
|
scale_factor: f32,
|
|
show_cursor_handles: bool,
|
|
) -> Result<(), jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| {
|
|
let mut text = data.text.to_string();
|
|
let mut cursor_position = data.cursor_position;
|
|
let mut anchor_position = data.anchor_position.unwrap_or(data.cursor_position);
|
|
|
|
if !data.preedit_text.is_empty() {
|
|
text.insert_str(data.preedit_offset, data.preedit_text.as_str());
|
|
if cursor_position >= data.preedit_offset {
|
|
cursor_position += data.preedit_text.len()
|
|
}
|
|
if anchor_position >= data.preedit_offset {
|
|
anchor_position += data.preedit_text.len()
|
|
}
|
|
}
|
|
|
|
let to_utf16 = |x| byte_offset_to_utf16_offset(&text, x as usize);
|
|
let text = JString::new(env, text.as_str())?;
|
|
|
|
let input_type = match data.input_type {
|
|
InputType::Text | InputType::Search => {
|
|
let hints = &data.input_method_hints;
|
|
let capitalization_flag = match hints.capitalization {
|
|
CapitalizationMode::None => 0 as jint,
|
|
CapitalizationMode::Sentences => {
|
|
AndroidInputType::TYPE_TEXT_FLAG_CAP_SENTENCES(env)?
|
|
}
|
|
CapitalizationMode::Words => {
|
|
AndroidInputType::TYPE_TEXT_FLAG_CAP_WORDS(env)?
|
|
}
|
|
CapitalizationMode::Characters => {
|
|
AndroidInputType::TYPE_TEXT_FLAG_CAP_CHARACTERS(env)?
|
|
}
|
|
_ => 0 as jint,
|
|
};
|
|
let auto_correct_flag = if hints.auto_correct {
|
|
AndroidInputType::TYPE_TEXT_FLAG_AUTO_CORRECT(env)?
|
|
} else {
|
|
0 as jint
|
|
};
|
|
let auto_complete_flag = if hints.auto_complete {
|
|
AndroidInputType::TYPE_TEXT_FLAG_AUTO_COMPLETE(env)?
|
|
} else {
|
|
0 as jint
|
|
};
|
|
AndroidInputType::TYPE_CLASS_TEXT(env)?
|
|
| capitalization_flag
|
|
| auto_correct_flag
|
|
| auto_complete_flag
|
|
}
|
|
InputType::Password => {
|
|
AndroidInputType::TYPE_TEXT_VARIATION_PASSWORD(env)?
|
|
| AndroidInputType::TYPE_CLASS_TEXT(env)?
|
|
}
|
|
InputType::Number => AndroidInputType::TYPE_CLASS_NUMBER(env)?,
|
|
InputType::Decimal => {
|
|
AndroidInputType::TYPE_CLASS_NUMBER(env)?
|
|
| AndroidInputType::TYPE_NUMBER_FLAG_DECIMAL(env)?
|
|
}
|
|
_ => 0 as jint,
|
|
};
|
|
|
|
let cur_origin = data.cursor_rect_origin.to_physical(scale_factor); // i32
|
|
let anchor_origin = data.anchor_point.to_physical(scale_factor);
|
|
let cur_size = data.cursor_rect_size.to_physical(scale_factor);
|
|
|
|
// A caret at the last column or on the last line sits exactly on the clip rect's max
|
|
// edge (e.g. every right-aligned field), where the exclusive `Rect::contains` reports
|
|
// it invisible and the handle gets sent off-screen. Inflate the rect by one pixel so
|
|
// the max edge counts as visible.
|
|
let clip_rect = data.clip_rect.map(|r| r.inflate(1., 1.));
|
|
|
|
let cur_visible = clip_rect.map_or(true, |r| {
|
|
r.contains(i_slint_core::lengths::logical_point_from_api(data.cursor_rect_origin))
|
|
});
|
|
let anchor_visible = clip_rect.map_or(true, |r| {
|
|
// anchor_point is `origin + cursor_size` for handle placement; check the anchor
|
|
// cursor's origin instead.
|
|
let anchor_origin =
|
|
i_slint_core::lengths::logical_point_from_api(data.anchor_point)
|
|
- i_slint_core::lengths::logical_size_from_api(data.cursor_rect_size)
|
|
.to_vector();
|
|
r.contains(anchor_origin)
|
|
});
|
|
|
|
// Add 2*cur_size.width to the y position to be a bit under the cursor
|
|
let cursor_height = cur_size.height as i32 + 2 * cur_size.width as i32;
|
|
let cur_x = if cur_visible { cur_origin.x + cur_size.width as i32 / 2 } else { -1 };
|
|
let cur_y = cur_origin.y + cursor_height;
|
|
let anchor_x = if anchor_visible { anchor_origin.x } else { -1 };
|
|
let anchor_y = anchor_origin.y + 2 * cur_size.width as i32;
|
|
|
|
helper.set_imm_data(
|
|
env,
|
|
&text,
|
|
to_utf16(cursor_position) as i32,
|
|
to_utf16(anchor_position) as i32,
|
|
to_utf16(data.preedit_offset) as i32,
|
|
to_utf16(data.preedit_offset + data.preedit_text.len()) as i32,
|
|
cur_x,
|
|
cur_y,
|
|
anchor_x,
|
|
anchor_y,
|
|
cursor_height,
|
|
input_type,
|
|
show_cursor_handles,
|
|
)?;
|
|
|
|
Ok(())
|
|
})
|
|
}
|
|
|
|
pub fn color_scheme(&self) -> Result<i32, jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| helper.color_scheme(env))
|
|
}
|
|
|
|
pub fn font_scale(&self) -> Result<f32, jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| helper.font_scale(env))
|
|
}
|
|
|
|
pub fn accent_color(&self) -> Result<Color, jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| {
|
|
Ok(Color::from_argb_encoded(helper.accent_color(env)? as u32))
|
|
})
|
|
}
|
|
|
|
pub fn get_safe_area(&self) -> Result<PhysicalEdges, jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| {
|
|
let rect = helper.get_safe_area(env)?;
|
|
let left = rect.left(env)?;
|
|
let top = rect.top(env)?;
|
|
let right = rect.right(env)?;
|
|
let bottom = rect.bottom(env)?;
|
|
Ok(PhysicalEdges::new(top, bottom, left, right))
|
|
})
|
|
}
|
|
|
|
pub fn set_handle_color(&self, color: Color) -> Result<(), jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| {
|
|
helper.set_handle_color(env, color.as_argb_encoded() as i32)
|
|
})
|
|
}
|
|
|
|
pub fn long_press_timeout(&self) -> Result<Duration, jni::errors::Error> {
|
|
self.with_jni_env(|env, _helper| {
|
|
let long_press_timeout = AndroidViewConfiguration::get_long_press_timeout(env)?;
|
|
Ok(Duration::from_millis(long_press_timeout as _))
|
|
})
|
|
}
|
|
|
|
pub fn show_action_menu(&self) -> Result<(), jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| helper.show_action_menu(env))
|
|
}
|
|
|
|
pub fn set_clipboard(&self, text: &str) -> Result<(), jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| {
|
|
let text = JString::new(env, text)?;
|
|
helper.set_clipboard(env, &text)
|
|
})
|
|
}
|
|
|
|
pub fn get_clipboard(&self) -> Result<String, jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| Ok(helper.get_clipboard(env)?.to_string()))
|
|
}
|
|
|
|
/// Ask the Activity to finish. Used from `callback_on_back_invoked` when
|
|
/// Slint's key dispatch reports the Back key as unhandled so we preserve
|
|
/// the legacy Back-closes-the-activity behavior even under the
|
|
/// OnBackInvokedCallback flow.
|
|
pub fn finish_activity(&self) -> Result<(), jni::errors::Error> {
|
|
self.with_jni_env(|env, helper| helper.finish_activity(env))
|
|
}
|
|
}
|
|
|
|
fn callback_update_text<'local>(
|
|
_env: &mut Env<'local>,
|
|
_class: JClass<'local>,
|
|
text: JString<'local>,
|
|
cursor_position: jint,
|
|
anchor_position: jint,
|
|
preedit_start: jint,
|
|
preedit_end: jint,
|
|
) -> Result<(), jni::errors::Error> {
|
|
let java_str = text.to_string();
|
|
let decoded: std::borrow::Cow<str> = (&java_str).into();
|
|
let text = SharedString::from(decoded.as_ref());
|
|
|
|
let cursor_position = utf16_offset_to_byte_offset_clamped(&text, cursor_position as usize);
|
|
let anchor_position = utf16_offset_to_byte_offset_clamped(&text, anchor_position as usize);
|
|
let preedit_start = utf16_offset_to_byte_offset_clamped(&text, preedit_start as usize);
|
|
let preedit_end = utf16_offset_to_byte_offset_clamped(&text, preedit_end as usize);
|
|
|
|
i_slint_core::api::invoke_from_event_loop(move || {
|
|
if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
|
|
adaptor.show_cursor_handles.set(false);
|
|
let event = if preedit_start != preedit_end {
|
|
let adjust = |pos| {
|
|
if pos <= preedit_start {
|
|
pos
|
|
} else if pos >= preedit_end {
|
|
preedit_start + (pos - preedit_end)
|
|
} else {
|
|
preedit_start
|
|
}
|
|
} as i32;
|
|
let mut key_event = KeyEvent::default();
|
|
key_event.text =
|
|
i_slint_core::format!("{}{}", &text[..preedit_start], &text[preedit_end..]);
|
|
|
|
InternalKeyEvent {
|
|
event_type: KeyEventType::UpdateComposition,
|
|
preedit_text: text[preedit_start..preedit_end].into(),
|
|
preedit_selection: Some(0..(preedit_end - preedit_start) as i32),
|
|
replacement_range: Some(i32::MIN..i32::MAX),
|
|
cursor_position: Some(adjust(cursor_position)),
|
|
anchor_position: Some(adjust(anchor_position)),
|
|
key_event,
|
|
..Default::default()
|
|
}
|
|
} else {
|
|
let mut key_event = KeyEvent::default();
|
|
key_event.text = text;
|
|
|
|
InternalKeyEvent {
|
|
event_type: KeyEventType::CommitComposition,
|
|
replacement_range: Some(i32::MIN..i32::MAX),
|
|
cursor_position: Some(cursor_position as _),
|
|
anchor_position: Some(anchor_position as _),
|
|
key_event,
|
|
..Default::default()
|
|
}
|
|
};
|
|
adaptor.window.dispatch_event(i_slint_core::platform::WindowEvent::internal(event));
|
|
}
|
|
})
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn callback_set_night_mode<'local>(
|
|
_env: &mut Env<'local>,
|
|
_class: JClass<'local>,
|
|
night_mode: jint,
|
|
) -> Result<(), jni::errors::Error> {
|
|
i_slint_core::api::invoke_from_event_loop(move || {
|
|
if let Some(w) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
|
|
let scheme = match night_mode {
|
|
0x10 => ColorScheme::Light, // UI_MODE_NIGHT_NO(0x10)
|
|
0x20 => ColorScheme::Dark, // UI_MODE_NIGHT_YES(0x20)
|
|
0x0 => ColorScheme::Unknown, // UI_MODE_NIGHT_UNDEFINED
|
|
_ => ColorScheme::Unknown,
|
|
};
|
|
let ctx = i_slint_core::window::WindowInner::from_pub(&w.window).context();
|
|
ctx.set_color_scheme(scheme);
|
|
if let Ok(accent) = w.java_helper.accent_color() {
|
|
ctx.set_accent_color(accent);
|
|
}
|
|
}
|
|
})
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn callback_set_font_scale<'local>(
|
|
_env: &mut Env<'local>,
|
|
_class: JClass<'local>,
|
|
font_scale: jfloat,
|
|
) -> Result<(), jni::errors::Error> {
|
|
// Skip rather than clobber: an OEM/ROM that hands us 0.0 or NaN during a
|
|
// transient configuration change must not zero out the value `bind_context`
|
|
// already set.
|
|
let Some(size) = font_scale_to_logical_length(font_scale) else { return Ok(()) };
|
|
i_slint_core::api::invoke_from_event_loop(move || {
|
|
if let Some(w) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
|
|
let ctx = i_slint_core::window::WindowInner::from_pub(&w.window).context();
|
|
ctx.set_platform_default_font_size(Some(size));
|
|
}
|
|
})
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn callback_move_cursor_handle<'local>(
|
|
_env: &mut Env<'local>,
|
|
_class: JClass<'local>,
|
|
id: jint,
|
|
pos_x: jint,
|
|
pos_y: jint,
|
|
) -> Result<(), jni::errors::Error> {
|
|
i_slint_core::api::invoke_from_event_loop(move || {
|
|
if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
|
|
if let Some(focus_item) = i_slint_core::window::WindowInner::from_pub(&adaptor.window)
|
|
.focus_item
|
|
.borrow()
|
|
.upgrade()
|
|
{
|
|
if let Some(text_input) = focus_item.downcast::<i_slint_core::items::TextInput>() {
|
|
let scale_factor = adaptor.window.scale_factor();
|
|
let adaptor = adaptor.clone() as Rc<dyn WindowAdapter>;
|
|
let size = text_input
|
|
.as_pin_ref()
|
|
.font_request(&focus_item)
|
|
.pixel_size
|
|
.unwrap_or_default()
|
|
.get();
|
|
let pos =
|
|
euclid::point2(
|
|
pos_x as f32 / scale_factor,
|
|
pos_y as f32 / scale_factor - size / 2.,
|
|
) - focus_item.map_to_window(focus_item.geometry().origin).to_vector();
|
|
let (text_pos, affinity) = text_input.as_pin_ref().byte_offset_for_position(
|
|
pos,
|
|
&adaptor,
|
|
&focus_item,
|
|
);
|
|
|
|
let (cur_pos, cur_affinity) = if id == 0 {
|
|
text_input.anchor_position_byte_offset.set(text_pos as i32);
|
|
(text_pos as i32, affinity)
|
|
} else {
|
|
let current_cursor = text_input.as_pin_ref().cursor_position_byte_offset();
|
|
let current_anchor = text_input.as_pin_ref().anchor_position_byte_offset();
|
|
if (id == 1 && current_anchor < current_cursor)
|
|
|| (id == 2 && current_anchor > current_cursor)
|
|
{
|
|
if current_cursor == text_pos as i32 {
|
|
return;
|
|
}
|
|
text_input.anchor_position_byte_offset.set(text_pos as i32);
|
|
(current_cursor, text_input.as_pin_ref().cursor_position_affinity())
|
|
} else {
|
|
if current_anchor == text_pos as i32 {
|
|
return;
|
|
}
|
|
(text_pos as i32, affinity)
|
|
}
|
|
};
|
|
|
|
text_input.as_pin_ref().set_cursor_position_with_affinity(
|
|
cur_pos,
|
|
cur_affinity,
|
|
true,
|
|
i_slint_core::items::TextChangeNotify::TriggerCallbacks,
|
|
&adaptor,
|
|
&focus_item,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn callback_popup_menu_action<'local>(
|
|
_env: &mut Env<'local>,
|
|
_class: JClass<'local>,
|
|
id: jint,
|
|
) -> Result<(), jni::errors::Error> {
|
|
i_slint_core::api::invoke_from_event_loop(move || {
|
|
if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
|
|
if let Some(focus_item) = i_slint_core::window::WindowInner::from_pub(&adaptor.window)
|
|
.focus_item
|
|
.borrow()
|
|
.upgrade()
|
|
{
|
|
if let Some(text_input) = focus_item.downcast::<i_slint_core::items::TextInput>() {
|
|
let text_input = text_input.as_pin_ref();
|
|
let adaptor = adaptor.clone() as Rc<dyn WindowAdapter>;
|
|
match id {
|
|
0 => text_input.cut(&adaptor, &focus_item),
|
|
1 => text_input.copy(&adaptor, &focus_item),
|
|
2 => text_input.paste(&adaptor, &focus_item),
|
|
3 => text_input.select_all(&adaptor, &focus_item),
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.unwrap();
|
|
Ok(())
|
|
}
|
|
|
|
fn callback_on_back_invoked<'local>(
|
|
_env: &mut Env<'local>,
|
|
_class: JClass<'local>,
|
|
) -> Result<(), jni::errors::Error> {
|
|
// Forward Back as a Key.Back KeyPressed + KeyReleased pair; fall back to
|
|
// Activity.finish() if unhandled.
|
|
let _ = i_slint_core::api::invoke_from_event_loop(move || {
|
|
if let Some(adaptor) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
|
|
let text: SharedString = Key::Back.into();
|
|
let pressed = adaptor
|
|
.window
|
|
.dispatch_event_with_result(WindowEvent::KeyPressed { text: text.clone() });
|
|
let released =
|
|
adaptor.window.dispatch_event_with_result(WindowEvent::KeyReleased { text });
|
|
let handled = matches!(pressed, Ok(WindowEventDispatchResult::Accepted))
|
|
|| matches!(released, Ok(WindowEventDispatchResult::Accepted));
|
|
if !handled {
|
|
if let Err(e) = adaptor.java_helper.finish_activity() {
|
|
i_slint_core::debug_log!("finish_activity failed: {e:#?}");
|
|
}
|
|
}
|
|
}
|
|
});
|
|
Ok(())
|
|
}
|
|
|
|
fn callback_set_insets<'local>(
|
|
_env: &mut Env<'local>,
|
|
_class: JClass<'local>,
|
|
window_top: jint,
|
|
window_left: jint,
|
|
window_bottom: jint,
|
|
window_right: jint,
|
|
safe_area_top: jint,
|
|
safe_area_left: jint,
|
|
safe_area_bottom: jint,
|
|
safe_area_right: jint,
|
|
keyboard_top: jint,
|
|
keyboard_left: jint,
|
|
keyboard_bottom: jint,
|
|
keyboard_right: jint,
|
|
) -> Result<(), jni::errors::Error> {
|
|
i_slint_core::api::invoke_from_event_loop(move || {
|
|
if let Some(w) = CURRENT_WINDOW.with_borrow(|x| x.upgrade()) {
|
|
w.update_window_insets(
|
|
PhysicalPosition::new(window_left as _, window_top as _),
|
|
PhysicalSize::new(
|
|
(window_right - window_left) as _,
|
|
(window_bottom - window_top) as _,
|
|
),
|
|
PhysicalEdges::new(
|
|
safe_area_top as _,
|
|
safe_area_bottom as _,
|
|
safe_area_left as _,
|
|
safe_area_right as _,
|
|
),
|
|
PhysicalEdges::new(
|
|
keyboard_top as _,
|
|
keyboard_bottom as _,
|
|
keyboard_left as _,
|
|
keyboard_right as _,
|
|
),
|
|
);
|
|
}
|
|
})
|
|
.unwrap();
|
|
Ok(())
|
|
}
|