iris: add an android-view backend beside winit (I2, part 1)
Relocates the platform-neutral halves of the winit `default` backend (WidgetState, CursorState/sense, Tasks, Selector/Selectable's focus handling) into shared crate-root modules so both backends can use them without duplication, generalizes Tasks' redraw nudge behind a RequestRedraw trait instead of a concrete winit::window::Window, and adds iris/src/android/: a second backend on android-view's ViewPeer -- wgpu on the view's surface, touch as a mouse-like cursor, an InputConnection bridge onto TextEdit (I1's parley editor), and a window-insets side channel since android-view has no hook for it. winit's own Android support pulls in android-activity without a selected backend feature, so `default`/`android` are now target-gated rather than both compiled in; confirmed by cross-compiling before this split (cargo ndk failed inside android-activity) and after (clean). Host build/clippy/fmt/tests and the android (x86_64, API 26) cross-compile of the iris crate are all clean; the winit tabs example still renders via run-headless.sh. Not yet exercised: an actual android-app crate and Gradle shell to run this on the emulator -- next in RUST.md's I2. Co-Authored-By: Claude Sonnet <noreply@anthropic.com>
This commit is contained in:
1 parent
fba572427d
commit
982449293d
20 files changed
+1432
-163
No files matched your search
Generated
+48
-35
@@ -88,6 +88,20 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
|
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android-view"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "git+https://github.com/rust-mobile/android-view.git?rev=bec6c62a96cef8239b0fd7fedeef9b184d02e3a1#bec6c62a96cef8239b0fd7fedeef9b184d02e3a1"
|
||||||
|
dependencies = [
|
||||||
|
"dpi",
|
||||||
|
"jni",
|
||||||
|
"ndk",
|
||||||
|
"num_enum",
|
||||||
|
"send_wrapper",
|
||||||
|
"smallvec",
|
||||||
|
"ui-events",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -739,15 +753,6 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "font-types"
|
|
||||||
version = "0.10.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "font-types"
|
name = "font-types"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
@@ -771,7 +776,7 @@ dependencies = [
|
|||||||
"objc2-core-text",
|
"objc2-core-text",
|
||||||
"objc2-foundation 0.3.2",
|
"objc2-foundation 0.3.2",
|
||||||
"parlance",
|
"parlance",
|
||||||
"read-fonts 0.41.0",
|
"read-fonts",
|
||||||
"roxmltree",
|
"roxmltree",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"windows",
|
"windows",
|
||||||
@@ -933,7 +938,7 @@ checksum = "c03d949a14aa089bbb282f7dd76a498a7f684428e4257202efc119ec010376f9"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"read-fonts 0.41.0",
|
"read-fonts",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1171,12 +1176,14 @@ dependencies = [
|
|||||||
name = "iris"
|
name = "iris"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"android-view",
|
||||||
"arboard",
|
"arboard",
|
||||||
"image",
|
"image",
|
||||||
"iris-core",
|
"iris-core",
|
||||||
"iris-macro",
|
"iris-macro",
|
||||||
"parley",
|
"parley",
|
||||||
"pollster",
|
"pollster",
|
||||||
|
"send_wrapper",
|
||||||
"swash",
|
"swash",
|
||||||
"tokio",
|
"tokio",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
@@ -1255,6 +1262,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keyboard-types"
|
||||||
|
version = "0.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "khronos-egl"
|
name = "khronos-egl"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
@@ -1971,7 +1988,7 @@ dependencies = [
|
|||||||
"linebender_resource_handle",
|
"linebender_resource_handle",
|
||||||
"parlance",
|
"parlance",
|
||||||
"parley_data",
|
"parley_data",
|
||||||
"skrifa 0.44.0",
|
"skrifa",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2314,16 +2331,6 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "read-fonts"
|
|
||||||
version = "0.36.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5eaa2941a4c05443ee3a7b26ab076a553c343ad5995230cc2b1d3e993bdc6345"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"font-types 0.10.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "read-fonts"
|
name = "read-fonts"
|
||||||
version = "0.41.0"
|
version = "0.41.0"
|
||||||
@@ -2331,7 +2338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
|
checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"font-types 0.12.4",
|
"font-types",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2455,6 +2462,12 @@ dependencies = [
|
|||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "send_wrapper"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@@ -2506,16 +2519,6 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "skrifa"
|
|
||||||
version = "0.39.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c9eb0b904a04d09bd68c65d946617b8ff733009999050f3b851c32fb3cfb60e"
|
|
||||||
dependencies = [
|
|
||||||
"bytemuck",
|
|
||||||
"read-fonts 0.36.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "skrifa"
|
name = "skrifa"
|
||||||
version = "0.44.0"
|
version = "0.44.0"
|
||||||
@@ -2523,7 +2526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
|
checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"read-fonts 0.41.0",
|
"read-fonts",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2614,7 +2617,7 @@ version = "0.2.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
|
checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"skrifa 0.39.0",
|
"skrifa",
|
||||||
"yazi",
|
"yazi",
|
||||||
"zeno",
|
"zeno",
|
||||||
]
|
]
|
||||||
@@ -2823,6 +2826,16 @@ version = "0.25.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ui-events"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c4c2cc34489c685d4e7a1a1f97b7b4416c5aa789892114ae77df6cd2a60f0ec4"
|
||||||
|
dependencies = [
|
||||||
|
"dpi",
|
||||||
|
"keyboard-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|||||||
+25
-2
@@ -10,13 +10,36 @@ iris-core = { workspace = true }
|
|||||||
iris-macro = { workspace = true }
|
iris-macro = { workspace = true }
|
||||||
parley = { workspace = true }
|
parley = { workspace = true }
|
||||||
swash = { workspace = true }
|
swash = { workspace = true }
|
||||||
winit = { workspace = true }
|
|
||||||
arboard = { workspace = true, features = ["wayland-data-control"] }
|
|
||||||
pollster = { workspace = true }
|
pollster = { workspace = true }
|
||||||
wgpu = { workspace = true }
|
wgpu = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread"] }
|
||||||
|
|
||||||
|
# winit everywhere except Android; android-view (below) is what stands in
|
||||||
|
# for it there. Both backends live in this crate (see `src/android/mod.rs`'s
|
||||||
|
# doc comment) but are never compiled together: winit's own Android support
|
||||||
|
# pulls in `android-activity`, which panics at compile time unless one of
|
||||||
|
# its own backend features is picked, and picking one is exactly what
|
||||||
|
# `iris-core` was kept free of (RUST.md's I0b). Confirmed by trying it
|
||||||
|
# 2026-09-05: `cargo ndk -t x86_64 -P 26 build -p iris` failed inside
|
||||||
|
# `android-activity` itself with "Either game-activity or native-activity
|
||||||
|
# must be enabled" before this split existed.
|
||||||
|
[target.'cfg(not(target_os = "android"))'.dependencies]
|
||||||
|
winit = { workspace = true }
|
||||||
|
arboard = { workspace = true, features = ["wayland-data-control"] }
|
||||||
|
|
||||||
|
# Pinned to the exact commit RUST.md's E1 (2026-09-04) measured on this
|
||||||
|
# emulator -- real Vulkan rendering, a working `InputConnection`, and the
|
||||||
|
# accesskit-detach abort, all against this rev specifically. Advancing it
|
||||||
|
# wants re-running E1's checks, the same reason the nightly toolchain pin
|
||||||
|
# is dated rather than floating.
|
||||||
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
|
android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" }
|
||||||
|
# Not re-exported by android-view (only `jni` and `ndk` are), and needed
|
||||||
|
# for `android/insets.rs`'s own id -> state map -- the same reason
|
||||||
|
# android-view's own `PEER_MAP` carries one.
|
||||||
|
send_wrapper = "0.6.0"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
tokio = { workspace = true, features = ["sync", "rt", "rt-multi-thread", "time"] }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
use crate::attr::{FocusHost, recent_click};
|
||||||
|
use crate::prelude::*;
|
||||||
|
|
||||||
|
use super::view::HasAndroidUiState;
|
||||||
|
|
||||||
|
impl<T: HasAndroidUiState> FocusHost for T {
|
||||||
|
fn recent_click(&mut self) -> bool {
|
||||||
|
recent_click(&mut self.android_state_mut().last_click)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_focus(&mut self, id: Option<WeakWidget<TextEdit>>) {
|
||||||
|
self.android_state_mut().focus = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus_gained(&mut self, region: Option<PixelRegion>) {
|
||||||
|
// Showing the keyboard is a JNI call (`InputMethodManager.showSoftInput`),
|
||||||
|
// and this runs deep inside the platform-agnostic sensor dispatch
|
||||||
|
// with no `CallbackCtx` in reach -- `IrisViewPeer::after_input`
|
||||||
|
// (`view.rs`) is what actually makes the call, right after the
|
||||||
|
// sensor pass that got here returns.
|
||||||
|
if region.is_some() {
|
||||||
|
self.android_state_mut().pending_show_keyboard = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,272 @@
|
|||||||
|
//! `InputConnection`, implemented directly against a focused `TextEdit`
|
||||||
|
//! rather than against a stand-in editor the way android-view's own demo
|
||||||
|
//! does over its `parley::PlainEditor` -- I1 already put parley behind
|
||||||
|
//! `TextEdit`, so this is that same bridge, just wired to iris's widget
|
||||||
|
//! instead of a bespoke one. Follows `demo/src/lib.rs`'s
|
||||||
|
//! `impl InputConnection for DemoViewPeer`, which is where RUST.md's E1
|
||||||
|
//! found the shape this needs (`text_before_cursor` is what gets Gboard's
|
||||||
|
//! suggestion strip to read real words out of the buffer).
|
||||||
|
//!
|
||||||
|
//! Two things the demo tracks that this does not, both noted rather than
|
||||||
|
//! silently dropped: a real "composing region" distinct from the
|
||||||
|
//! selection (`set_composing_region` here just moves the caret, since
|
||||||
|
//! `TextEdit` has no third range to hold one), and batch-edit coalescing
|
||||||
|
//! (`begin`/`end_batch_edit` are no-ops -- a redraw mid-batch costs a frame
|
||||||
|
//! it does not need to, not correctness).
|
||||||
|
|
||||||
|
use crate::prelude::*;
|
||||||
|
use android_view::{
|
||||||
|
CAP_MODE_SENTENCES, CallbackCtx, EditorInfo, IME_FLAG_NO_ENTER_ACTION, IME_FLAG_NO_EXTRACT_UI,
|
||||||
|
IME_FLAG_NO_FULLSCREEN, INPUT_TYPE_CLASS_TEXT, INPUT_TYPE_TEXT_FLAG_AUTO_CORRECT,
|
||||||
|
INPUT_TYPE_TEXT_FLAG_CAP_SENTENCES, INPUT_TYPE_TEXT_FLAG_MULTI_LINE, InputConnection,
|
||||||
|
caps_mode,
|
||||||
|
};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use super::view::{AndroidAppState, IrisViewPeer};
|
||||||
|
|
||||||
|
/// Byte offset -> UTF-16 code unit offset, the unit every `InputConnection`
|
||||||
|
/// method speaks in (Java strings are UTF-16). `TextEdit` is byte-indexed
|
||||||
|
/// throughout since I1 moved it to parley -- see `edit.rs`'s doc comment on
|
||||||
|
/// `text()` -- so every crossing of this boundary goes through here rather
|
||||||
|
/// than through ad hoc counting at each call site.
|
||||||
|
fn byte_to_utf16(text: &str, byte_idx: usize) -> usize {
|
||||||
|
text[..byte_idx].encode_utf16().count()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn utf16_to_byte(text: &str, utf16_idx: usize) -> usize {
|
||||||
|
let mut utf16_len = 0;
|
||||||
|
for (byte_idx, ch) in text.char_indices() {
|
||||||
|
if utf16_len >= utf16_idx {
|
||||||
|
return byte_idx;
|
||||||
|
}
|
||||||
|
utf16_len += ch.len_utf16();
|
||||||
|
}
|
||||||
|
text.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||||
|
fn focus(&self) -> Option<WeakWidget<TextEdit>> {
|
||||||
|
self.state.android_state().focus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: AndroidAppState> InputConnection for IrisViewPeer<State> {
|
||||||
|
fn on_create_input_connection<'local>(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx<'local>,
|
||||||
|
out_attrs: &EditorInfo<'local>,
|
||||||
|
) {
|
||||||
|
// Set once per `InputConnection`, not per field -- Android calls
|
||||||
|
// this when the view (not a particular widget) attaches to an
|
||||||
|
// IME. `MULTI_LINE`/`AUTO_CORRECT`/`CAP_SENTENCES` cover both the
|
||||||
|
// tabs example's composer and a plain single-line field well
|
||||||
|
// enough that no per-field variant is worth the extra state yet.
|
||||||
|
out_attrs.set_input_type(
|
||||||
|
&mut ctx.env,
|
||||||
|
INPUT_TYPE_CLASS_TEXT
|
||||||
|
| INPUT_TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||||
|
| INPUT_TYPE_TEXT_FLAG_AUTO_CORRECT
|
||||||
|
| INPUT_TYPE_TEXT_FLAG_MULTI_LINE,
|
||||||
|
);
|
||||||
|
out_attrs.set_ime_options(
|
||||||
|
&mut ctx.env,
|
||||||
|
IME_FLAG_NO_FULLSCREEN | IME_FLAG_NO_EXTRACT_UI | IME_FLAG_NO_ENTER_ACTION,
|
||||||
|
);
|
||||||
|
if let Some(focus) = self.focus() {
|
||||||
|
let text = &self.rsc[focus];
|
||||||
|
let sel = text.selection_range().unwrap_or(0..0);
|
||||||
|
let start = byte_to_utf16(text.text(), sel.start) as i32;
|
||||||
|
let end = byte_to_utf16(text.text(), sel.end) as i32;
|
||||||
|
out_attrs.set_initial_sel_start(&mut ctx.env, start);
|
||||||
|
out_attrs.set_initial_sel_end(&mut ctx.env, end);
|
||||||
|
let caps = caps_mode(
|
||||||
|
&mut ctx.env,
|
||||||
|
text.text(),
|
||||||
|
start as usize,
|
||||||
|
CAP_MODE_SENTENCES,
|
||||||
|
);
|
||||||
|
out_attrs.set_initial_caps_mode(&mut ctx.env, caps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_before_cursor<'slf>(
|
||||||
|
&'slf mut self,
|
||||||
|
_ctx: &mut CallbackCtx,
|
||||||
|
n: i32,
|
||||||
|
) -> Option<Cow<'slf, str>> {
|
||||||
|
if n < 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let focus = self.focus()?;
|
||||||
|
let text = &self.rsc[focus];
|
||||||
|
let sel = text.selection_range()?;
|
||||||
|
let end_16 = byte_to_utf16(text.text(), sel.start);
|
||||||
|
let start_16 = end_16.saturating_sub(n as usize);
|
||||||
|
let start = utf16_to_byte(text.text(), start_16);
|
||||||
|
Some(Cow::Borrowed(&text.text()[start..sel.start]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn text_after_cursor<'slf>(
|
||||||
|
&'slf mut self,
|
||||||
|
_ctx: &mut CallbackCtx,
|
||||||
|
n: i32,
|
||||||
|
) -> Option<Cow<'slf, str>> {
|
||||||
|
if n < 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let focus = self.focus()?;
|
||||||
|
let text = &self.rsc[focus];
|
||||||
|
let sel = text.selection_range()?;
|
||||||
|
let len_16 = byte_to_utf16(text.text(), text.text().len());
|
||||||
|
let start_16 = byte_to_utf16(text.text(), sel.end);
|
||||||
|
let end_16 = (start_16 + n as usize).min(len_16);
|
||||||
|
let end = utf16_to_byte(text.text(), end_16);
|
||||||
|
Some(Cow::Borrowed(&text.text()[sel.end..end]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_text<'slf>(&'slf mut self, _ctx: &mut CallbackCtx) -> Option<Cow<'slf, str>> {
|
||||||
|
let focus = self.focus()?;
|
||||||
|
Some(Cow::Owned(self.rsc[focus].selected_text()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cursor_caps_mode(&mut self, ctx: &mut CallbackCtx, req_modes: u32) -> u32 {
|
||||||
|
let Some(focus) = self.focus() else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let text = &self.rsc[focus];
|
||||||
|
let Some(caret) = text.caret() else {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
let off = byte_to_utf16(text.text(), caret);
|
||||||
|
caps_mode(&mut ctx.env, text.text(), off, req_modes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_surrounding_text(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx,
|
||||||
|
before_length: i32,
|
||||||
|
after_length: i32,
|
||||||
|
) -> bool {
|
||||||
|
let Some(focus) = self.focus() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let text = &self.rsc[focus];
|
||||||
|
let Some(sel) = text.selection_range() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let content = text.text();
|
||||||
|
let start_16 =
|
||||||
|
byte_to_utf16(content, sel.start).saturating_sub(before_length.max(0) as usize);
|
||||||
|
let len_16 = byte_to_utf16(content, content.len());
|
||||||
|
let end_16 = (byte_to_utf16(content, sel.end) + after_length.max(0) as usize).min(len_16);
|
||||||
|
let start = utf16_to_byte(content, start_16);
|
||||||
|
let end = utf16_to_byte(content, end_16);
|
||||||
|
focus.edit(&mut self.rsc).delete_byte_range(start, end);
|
||||||
|
self.after_input(ctx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_surrounding_text_in_code_points(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx,
|
||||||
|
before_length: i32,
|
||||||
|
after_length: i32,
|
||||||
|
) -> bool {
|
||||||
|
// Approximated as UTF-16 units rather than Unicode scalar values --
|
||||||
|
// the two differ only outside the Basic Multilingual Plane, which
|
||||||
|
// this widget tree does not exercise today. Worth revisiting if a
|
||||||
|
// field ever needs to edit emoji or other astral-plane text well.
|
||||||
|
self.delete_surrounding_text(ctx, before_length, after_length)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_composing_text(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx,
|
||||||
|
text: &str,
|
||||||
|
_new_cursor_position: i32,
|
||||||
|
) -> bool {
|
||||||
|
let Some(focus) = self.focus() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
// The IME re-sends its whole composition on every keystroke;
|
||||||
|
// `compose_len` (chars, not bytes -- `TextEditCtx::replace`'s unit)
|
||||||
|
// is what lets `replace` remove exactly what it inserted last time.
|
||||||
|
// The same shape as `default::DefaultApp`'s `Ime::Preedit` handling
|
||||||
|
// for winit.
|
||||||
|
let compose_len = self.state.android_state().compose_len;
|
||||||
|
focus.edit(&mut self.rsc).replace(compose_len, text);
|
||||||
|
self.state.android_state_mut().compose_len = text.chars().count();
|
||||||
|
self.after_input(ctx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_composing_region(&mut self, _ctx: &mut CallbackCtx, _start: i32, _end: i32) -> bool {
|
||||||
|
// `TextEdit` has no separate composing range to move -- see this
|
||||||
|
// module's doc comment. Declining (rather than moving the caret,
|
||||||
|
// which would surprise a caller expecting only a style change)
|
||||||
|
// is the safer approximation.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish_composing_text(&mut self, ctx: &mut CallbackCtx) -> bool {
|
||||||
|
self.state.android_state_mut().compose_len = 0;
|
||||||
|
self.after_input(ctx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selection(&mut self, ctx: &mut CallbackCtx, start: i32, end: i32) -> bool {
|
||||||
|
let Some(focus) = self.focus() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let text = &self.rsc[focus];
|
||||||
|
let content = text.text();
|
||||||
|
// Collapsed to `end`: `TextEditCtx` has no range-selection setter
|
||||||
|
// yet (nothing before I2 needed one), so an IME-driven selection
|
||||||
|
// lands the caret at its focus end rather than spanning both.
|
||||||
|
let byte = utf16_to_byte(content, end.max(0) as usize);
|
||||||
|
focus.edit(&mut self.rsc).set_cursor_byte(byte);
|
||||||
|
let _ = start;
|
||||||
|
self.after_input(ctx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn perform_editor_action(&mut self, _ctx: &mut CallbackCtx, _editor_action: i32) -> bool {
|
||||||
|
// `IME_FLAG_NO_ENTER_ACTION` above asks the IME not to offer one;
|
||||||
|
// nothing here needs handling it yet.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn begin_batch_edit(&mut self, _ctx: &mut CallbackCtx) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_batch_edit(&mut self, _ctx: &mut CallbackCtx) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_key_event<'local>(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx<'local>,
|
||||||
|
event: &android_view::KeyEvent<'local>,
|
||||||
|
) -> bool {
|
||||||
|
let key_code = event.key_code(&mut ctx.env);
|
||||||
|
let handled = super::input::on_key(
|
||||||
|
&mut self.rsc,
|
||||||
|
&mut self.state,
|
||||||
|
&mut ctx.env,
|
||||||
|
key_code,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
if handled {
|
||||||
|
self.after_input(ctx);
|
||||||
|
}
|
||||||
|
handled
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_cursor_updates(&mut self, _ctx: &mut CallbackCtx, _cursor_update_mode: i32) -> bool {
|
||||||
|
// No cursor-anchor UI to feed -- see RUST.md's I2 notes on what
|
||||||
|
// this backend does not do yet.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use android_view::{jni::JNIEnv, ndk::event::Keycode};
|
||||||
|
|
||||||
|
use super::view::{AndroidAppState, AndroidRsc};
|
||||||
|
|
||||||
|
/// Hardware/synthesized key handling for the field that currently has
|
||||||
|
/// focus. Most typing on Android goes through the IME's `InputConnection`
|
||||||
|
/// (`android/ime.rs`) instead -- this only sees what a soft keyboard still
|
||||||
|
/// sends as a real `KeyEvent` in "not fullscreen" mode (Backspace, Enter,
|
||||||
|
/// the arrow keys on a physical keyboard) plus whatever `unicode_char`
|
||||||
|
/// reports for a plain key press. Returns whether anything used the event.
|
||||||
|
pub(super) fn on_key<'local, State: AndroidAppState>(
|
||||||
|
rsc: &mut AndroidRsc<State>,
|
||||||
|
state: &mut State,
|
||||||
|
env: &mut JNIEnv<'local>,
|
||||||
|
key_code: Keycode,
|
||||||
|
event: &android_view::KeyEvent<'local>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(focus) = state.android_state().focus else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut text = focus.edit(rsc);
|
||||||
|
match key_code {
|
||||||
|
Keycode::Del => text.backspace(false),
|
||||||
|
Keycode::ForwardDel => text.delete(false),
|
||||||
|
Keycode::DpadLeft => text.motion(Motion::Left, false),
|
||||||
|
Keycode::DpadRight => text.motion(Motion::Right, false),
|
||||||
|
Keycode::DpadUp => text.motion(Motion::Up, false),
|
||||||
|
Keycode::DpadDown => text.motion(Motion::Down, false),
|
||||||
|
Keycode::MoveHome => text.motion(Motion::LineStart, false),
|
||||||
|
Keycode::MoveEnd => text.motion(Motion::LineEnd, false),
|
||||||
|
Keycode::Enter | Keycode::NumpadEnter => text.newline(),
|
||||||
|
_ => match event.unicode_char(env) {
|
||||||
|
Some(c) if !c.is_control() => text.insert(&c.to_string()),
|
||||||
|
_ => return false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
//! Window insets, fed in from outside `ViewPeer`.
|
||||||
|
//!
|
||||||
|
//! android-view's registered native methods (`view.rs` in that crate) cover
|
||||||
|
//! touch, keys, focus, the surface and the IME -- there is nothing for
|
||||||
|
//! `View.onApplyWindowInsets`, because android-view's own demo does not
|
||||||
|
//! need it. The back gesture needed no new plumbing at all: with no
|
||||||
|
//! `OnBackPressedCallback` registered, Android still delivers it as an
|
||||||
|
//! ordinary `KEYCODE_BACK` `KeyEvent` through the ordinary key path (see
|
||||||
|
//! `view.rs`'s `on_key_down`), which is the legacy behaviour every app gets
|
||||||
|
//! by default and is enough for "the back gesture as an event". Insets have
|
||||||
|
//! no such stand-in, so this module registers one more native method by
|
||||||
|
//! hand, on the app's own `View` subclass rather than on android-view's.
|
||||||
|
//!
|
||||||
|
//! The peer id android-view hands back from `register_view_peer` is opaque
|
||||||
|
//! outside that crate (`with_peer` is `pub(crate)` there), so there is no
|
||||||
|
//! way to reach an existing `IrisViewPeer` from a JNI entry point we define
|
||||||
|
//! ourselves. Instead of forking android-view to add a hook, `new_peer`
|
||||||
|
//! (`view.rs`) inserts the *same* id into this module's own map, pointing
|
||||||
|
//! at a plain `Rc<RefCell<Shared>>` cloned into `AndroidUiState` too --
|
||||||
|
//! so writing here is reading there, with no dependency in either
|
||||||
|
//! direction on the other's internals.
|
||||||
|
|
||||||
|
use android_view::{
|
||||||
|
View,
|
||||||
|
jni::{
|
||||||
|
JNIEnv, NativeMethod,
|
||||||
|
descriptors::Desc,
|
||||||
|
objects::JClass,
|
||||||
|
sys::{jint, jlong},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
collections::HashMap,
|
||||||
|
ffi::c_void,
|
||||||
|
rc::Rc,
|
||||||
|
sync::{Mutex, OnceLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use send_wrapper::SendWrapper;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Insets {
|
||||||
|
pub left: i32,
|
||||||
|
pub top: i32,
|
||||||
|
pub right: i32,
|
||||||
|
pub bottom: i32,
|
||||||
|
/// The keyboard's own inset (`WindowInsetsCompat.Type.ime()`), separate
|
||||||
|
/// from `bottom` (the system bars): a layout wants to know about the
|
||||||
|
/// keyboard specifically, since it usually means "make room" rather
|
||||||
|
/// than "stay clear of a corner".
|
||||||
|
pub ime_bottom: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Shared {
|
||||||
|
pub insets: Insets,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SharedMap = HashMap<jlong, SendWrapper<Rc<RefCell<Shared>>>>;
|
||||||
|
|
||||||
|
fn map() -> &'static Mutex<SharedMap> {
|
||||||
|
static MAP: OnceLock<Mutex<SharedMap>> = OnceLock::new();
|
||||||
|
MAP.get_or_init(Default::default)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called from `view::new_peer` with the same id android-view's
|
||||||
|
/// `register_view_peer` returned, so a later `apply_window_insets` call
|
||||||
|
/// (keyed on that id by Java, which only ever sees the one long) reaches
|
||||||
|
/// the same `Shared` cell `AndroidUiState` reads from.
|
||||||
|
pub(super) fn register(id: jlong, shared: Rc<RefCell<Shared>>) {
|
||||||
|
map().lock().unwrap().insert(id, SendWrapper::new(shared));
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "system" fn unregister_insets<'local>(
|
||||||
|
_env: JNIEnv<'local>,
|
||||||
|
_view: View<'local>,
|
||||||
|
peer: jlong,
|
||||||
|
) {
|
||||||
|
map().lock().unwrap().remove(&peer);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "system" fn apply_window_insets<'local>(
|
||||||
|
mut env: JNIEnv<'local>,
|
||||||
|
view: View<'local>,
|
||||||
|
peer: jlong,
|
||||||
|
left: jint,
|
||||||
|
top: jint,
|
||||||
|
right: jint,
|
||||||
|
bottom: jint,
|
||||||
|
ime_bottom: jint,
|
||||||
|
) {
|
||||||
|
if let Some(shared) = map().lock().unwrap().get(&peer) {
|
||||||
|
shared.borrow_mut().insets = Insets {
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
right,
|
||||||
|
bottom,
|
||||||
|
ime_bottom,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Insets can change (the keyboard opening) with no resize and no
|
||||||
|
// touch, so nothing else here would otherwise ask for a frame.
|
||||||
|
view.post_frame_callback(&mut env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers `applyWindowInsetsNative` on the app's own `View` subclass.
|
||||||
|
/// Called once from `JNI_OnLoad` alongside `android_view::register_view_class`.
|
||||||
|
pub fn register_native_methods<'local, 'other_local>(
|
||||||
|
env: &mut JNIEnv<'local>,
|
||||||
|
class: impl Desc<'local, JClass<'other_local>>,
|
||||||
|
) {
|
||||||
|
env.register_native_methods(
|
||||||
|
class,
|
||||||
|
&[
|
||||||
|
NativeMethod {
|
||||||
|
name: "applyWindowInsetsNative".into(),
|
||||||
|
sig: "(JIIIII)V".into(),
|
||||||
|
fn_ptr: apply_window_insets as *mut c_void,
|
||||||
|
},
|
||||||
|
NativeMethod {
|
||||||
|
name: "unregisterInsetsNative".into(),
|
||||||
|
sig: "(J)V".into(),
|
||||||
|
fn_ptr: unregister_insets as *mut c_void,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
//! iris's second windowing backend: `android-view` (a `SurfaceView` plus a
|
||||||
|
//! JNI `ViewPeer`) instead of winit. See RUST.md's I2 for why this exists
|
||||||
|
//! as a second backend rather than winit's own (unfinished, and blocked on
|
||||||
|
//! `android-activity`'s backend-feature requirement) Android support, and
|
||||||
|
//! for the pass condition this was built against.
|
||||||
|
//!
|
||||||
|
//! Structured to mirror `default/` module for module: `view.rs` is that
|
||||||
|
//! module's `app.rs` + `state.rs` combined (android-view has one harness
|
||||||
|
//! type, `ViewPeer`, where winit splits `ApplicationHandler` from the
|
||||||
|
//! per-window state), `render.rs` is `render.rs`, `input.rs` is `input.rs`,
|
||||||
|
//! `attr.rs` is `attr.rs`. `ime.rs` and `insets.rs` have no winit
|
||||||
|
//! counterpart: winit cannot drive an IME beyond `Ime::Preedit`/`Commit`
|
||||||
|
//! (RUST.md's E1) and has no concept of Android's window insets at all.
|
||||||
|
|
||||||
|
mod attr;
|
||||||
|
mod ime;
|
||||||
|
mod input;
|
||||||
|
mod insets;
|
||||||
|
mod render;
|
||||||
|
mod view;
|
||||||
|
|
||||||
|
pub use insets::Insets;
|
||||||
|
pub use render::AndroidRenderer;
|
||||||
|
pub use view::{
|
||||||
|
AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState, IrisViewPeer, new_peer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Registers the extra native methods this backend needs beyond what
|
||||||
|
/// `android_view::register_view_class` covers (window insets -- see
|
||||||
|
/// `insets.rs`'s doc comment for why that one could not ride along on an
|
||||||
|
/// existing android-view callback the way the back gesture does). Call
|
||||||
|
/// from `JNI_OnLoad` alongside `register_view_class`, on the same `View`
|
||||||
|
/// subclass.
|
||||||
|
pub fn register_native_methods<'local, 'other_local>(
|
||||||
|
env: &mut android_view::jni::JNIEnv<'local>,
|
||||||
|
class: impl android_view::jni::descriptors::Desc<
|
||||||
|
'local,
|
||||||
|
android_view::jni::objects::JClass<'other_local>,
|
||||||
|
>,
|
||||||
|
) {
|
||||||
|
insets::register_native_methods(env, class);
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
use crate::task::RequestRedraw;
|
||||||
|
use android_view::{
|
||||||
|
View,
|
||||||
|
jni::{JavaVM, objects::GlobalRef},
|
||||||
|
ndk::native_window::NativeWindow,
|
||||||
|
};
|
||||||
|
use iris_core::{UiData, UiRenderNode, UiRenderState};
|
||||||
|
use pollster::FutureExt;
|
||||||
|
use wgpu::{
|
||||||
|
rwh::{DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WindowHandle},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const CLEAR_COLOR: Color = Color::BLACK;
|
||||||
|
|
||||||
|
/// `NativeWindow` (from the surface android-view hands over in
|
||||||
|
/// `surfaceChanged`) has a window handle but not a display one -- there is
|
||||||
|
/// exactly one display on Android and `rwh` has a unit variant for it.
|
||||||
|
/// Mirrors android-view's own demo (`demo/src/lib.rs`'s
|
||||||
|
/// `AndroidWindowHandle`).
|
||||||
|
struct AndroidWindowHandle {
|
||||||
|
window: NativeWindow,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasDisplayHandle for AndroidWindowHandle {
|
||||||
|
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
|
||||||
|
Ok(DisplayHandle::android())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasWindowHandle for AndroidWindowHandle {
|
||||||
|
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
|
||||||
|
self.window.window_handle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The android-view surface, unlike winit's window, does not outlive a
|
||||||
|
/// backgrounding of the activity: `surfaceDestroyed`/`surfaceCreated` (via
|
||||||
|
/// `SurfaceHolder.Callback`) recreate it, so this holds everything that
|
||||||
|
/// depends on that surface rather than being built once at startup --
|
||||||
|
/// `AndroidUiState` holds it as `Option<AndroidRenderer>`, `None` exactly
|
||||||
|
/// when there is no surface to draw into.
|
||||||
|
pub struct AndroidRenderer {
|
||||||
|
surface: Surface<'static>,
|
||||||
|
device: Device,
|
||||||
|
queue: Queue,
|
||||||
|
config: SurfaceConfiguration,
|
||||||
|
encoder: CommandEncoder,
|
||||||
|
pub ui: UiRenderNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AndroidRenderer {
|
||||||
|
pub fn new(window: NativeWindow, width: u32, height: u32) -> Self {
|
||||||
|
let instance = Instance::new(&InstanceDescriptor {
|
||||||
|
backends: Backends::PRIMARY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// SAFETY: the `NativeWindow` outlives the surface built from it --
|
||||||
|
// android-view drops the old renderer (and this surface with it)
|
||||||
|
// before handing over a new window, in `surface_changed` below.
|
||||||
|
let surface = instance
|
||||||
|
.create_surface(SurfaceTarget::from(AndroidWindowHandle { window }))
|
||||||
|
.expect("Could not create android surface!");
|
||||||
|
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&RequestAdapterOptions {
|
||||||
|
power_preference: PowerPreference::default(),
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
})
|
||||||
|
.block_on()
|
||||||
|
.expect("Could not get adapter!");
|
||||||
|
|
||||||
|
// Same request as the winit backend's `UiRenderer::new` -- no
|
||||||
|
// binding-array features, see TEXTURES.md's "Recommended shape".
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.request_device(&DeviceDescriptor {
|
||||||
|
required_limits: Limits {
|
||||||
|
max_buffer_size: 1 << 30,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.block_on()
|
||||||
|
.expect("Could not get device!");
|
||||||
|
|
||||||
|
let surface_caps = surface.get_capabilities(&adapter);
|
||||||
|
let surface_format = surface_caps
|
||||||
|
.formats
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|f| f.is_srgb())
|
||||||
|
.unwrap_or(surface_caps.formats[0]);
|
||||||
|
|
||||||
|
let config = SurfaceConfiguration {
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format: surface_format,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
present_mode: PresentMode::AutoVsync,
|
||||||
|
alpha_mode: surface_caps.alpha_modes[0],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
view_formats: vec![],
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
let encoder = Self::create_encoder(&device);
|
||||||
|
let ui = UiRenderNode::new(&device, &queue, &config);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
encoder,
|
||||||
|
ui,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_encoder(device: &Device) -> CommandEncoder {
|
||||||
|
device.create_command_encoder(&CommandEncoderDescriptor {
|
||||||
|
label: Some("Render Encoder"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, ui: &mut UiData, render: &mut UiRenderState) {
|
||||||
|
self.ui.update(&self.device, &self.queue, ui, render);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self) {
|
||||||
|
let output = self.surface.get_current_texture().unwrap();
|
||||||
|
let view = output
|
||||||
|
.texture
|
||||||
|
.create_view(&TextureViewDescriptor::default());
|
||||||
|
|
||||||
|
let mut encoder = std::mem::replace(&mut self.encoder, Self::create_encoder(&self.device));
|
||||||
|
{
|
||||||
|
let render_pass = &mut encoder.begin_render_pass(&RenderPassDescriptor {
|
||||||
|
color_attachments: &[Some(RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: Operations {
|
||||||
|
load: LoadOp::Clear(CLEAR_COLOR),
|
||||||
|
store: StoreOp::Store,
|
||||||
|
},
|
||||||
|
depth_slice: None,
|
||||||
|
})],
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
self.ui.draw(render_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.submit(std::iter::once(encoder.finish()));
|
||||||
|
output.present();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> iris_core::util::Vec2 {
|
||||||
|
(self.config.width, self.config.height).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(&mut self, width: u32, height: u32) {
|
||||||
|
self.config.width = width;
|
||||||
|
self.config.height = height;
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
self.ui.resize((width, height), &self.queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Tasks`' redraw handle on Android: a background task finishes on the
|
||||||
|
/// tokio thread `Tasks::init` spawned, which is not attached to the JVM, so
|
||||||
|
/// asking for a frame means attaching first. `post_frame_callback` needs a
|
||||||
|
/// live `View` reference; the global ref is what survives past the JNI call
|
||||||
|
/// that handed it to us.
|
||||||
|
pub struct AndroidRedrawHandle {
|
||||||
|
vm: JavaVM,
|
||||||
|
view: GlobalRef,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AndroidRedrawHandle {
|
||||||
|
pub fn new(vm: JavaVM, view: GlobalRef) -> Self {
|
||||||
|
Self { vm, view }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RequestRedraw for AndroidRedrawHandle {
|
||||||
|
fn request_redraw(&self) {
|
||||||
|
let Ok(mut env) = self.vm.attach_current_thread() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let local = env.new_local_ref(&self.view).unwrap();
|
||||||
|
View(local).post_frame_callback(&mut env);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,404 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use crate::task::RequestRedraw;
|
||||||
|
use android_view::{
|
||||||
|
CallbackCtx, Context, InputConnection, KeyEvent, MotionEvent, Rect, View, ViewPeer,
|
||||||
|
jni::JNIEnv,
|
||||||
|
ndk::event::{Keycode, MotionAction},
|
||||||
|
};
|
||||||
|
// `marker::Sized` explicitly: `crate::prelude::*` below also brings in the
|
||||||
|
// `Sized` *widget* (`widget::position::sized::Sized`), and an unqualified
|
||||||
|
// glob import shadows the language prelude -- `default/mod.rs` has the same
|
||||||
|
// explicit import for the same reason.
|
||||||
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
|
marker::{PhantomData, Sized},
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
insets::{Insets, Shared},
|
||||||
|
render::{AndroidRedrawHandle, AndroidRenderer},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The android-view analogue of `default::DefaultUiState`. `renderer` is an
|
||||||
|
/// `Option` because a `SurfaceView`'s surface does not outlive backgrounding
|
||||||
|
/// the way a winit `Window` does -- `surfaceDestroyed`/`surfaceCreated` can
|
||||||
|
/// happen any number of times over the life of one `IrisViewPeer`.
|
||||||
|
pub struct AndroidUiState {
|
||||||
|
pub root: Option<StrongWidget>,
|
||||||
|
pub renderer: Option<AndroidRenderer>,
|
||||||
|
pub focus: Option<WeakWidget<TextEdit>>,
|
||||||
|
pub cursor: CursorState,
|
||||||
|
pub last_click: Instant,
|
||||||
|
/// The IME preedit's previous length, in `char`s -- the same
|
||||||
|
/// re-send-the-whole-composition bookkeeping `default::DefaultUiState`
|
||||||
|
/// keeps for winit's `Ime::Preedit`, since android-view's
|
||||||
|
/// `setComposingText` has the identical shape (see `android/ime.rs`).
|
||||||
|
pub compose_len: usize,
|
||||||
|
/// Set by `attr::FocusHost::focus_gained` when a `TextEdit` is focused;
|
||||||
|
/// consumed by the touch handler after the sensor pass finishes, since
|
||||||
|
/// showing the keyboard is a JNI call and `focus_gained` runs deep
|
||||||
|
/// inside the platform-agnostic sensor dispatch with no `CallbackCtx`
|
||||||
|
/// in reach.
|
||||||
|
pub pending_show_keyboard: bool,
|
||||||
|
/// Window insets, filled in from outside the normal `ViewPeer` callback
|
||||||
|
/// path -- see `android/insets.rs` for why they need a registry of
|
||||||
|
/// their own.
|
||||||
|
shared: Rc<RefCell<Shared>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AndroidUiState {
|
||||||
|
fn new(shared: Rc<RefCell<Shared>>) -> Self {
|
||||||
|
Self {
|
||||||
|
root: None,
|
||||||
|
renderer: None,
|
||||||
|
focus: None,
|
||||||
|
cursor: Default::default(),
|
||||||
|
last_click: Instant::now(),
|
||||||
|
compose_len: 0,
|
||||||
|
pending_show_keyboard: false,
|
||||||
|
shared,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insets(&self) -> Insets {
|
||||||
|
self.shared.borrow().insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasRoot for AndroidUiState {
|
||||||
|
fn set_root(&mut self, root: StrongWidget) {
|
||||||
|
self.root = Some(root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HasAndroidUiState: Sized + 'static {
|
||||||
|
fn android_state(&self) -> &AndroidUiState;
|
||||||
|
fn android_state_mut(&mut self) -> &mut AndroidUiState;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AndroidAppState: HasAndroidUiState {
|
||||||
|
fn new(ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self;
|
||||||
|
/// The system back gesture/button. `true` means handled -- nothing
|
||||||
|
/// further happens; `false` lets the activity finish as it would with
|
||||||
|
/// no view at all. The default declines, since most screens have
|
||||||
|
/// nothing to intercept it for.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
fn back_pressed(&mut self, rsc: &mut AndroidRsc<Self>, render: &mut UiRenderState) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The android-view analogue of `default::DefaultRsc` -- identical in
|
||||||
|
/// substance, since none of `UiRsc`/`HasEvents`/`HasTasks`/`HasWidgetState`
|
||||||
|
/// mention winit. Kept as a separate type rather than shared code because
|
||||||
|
/// the two backends' `ViewPeer`/`ApplicationHandler` entry points hold
|
||||||
|
/// their harness state differently (see RUST.md's I2).
|
||||||
|
pub struct AndroidRsc<State: 'static> {
|
||||||
|
pub ui: UiData,
|
||||||
|
pub events: EventManager<Self>,
|
||||||
|
pub tasks: Tasks<Self>,
|
||||||
|
pub state: WidgetState,
|
||||||
|
_state: PhantomData<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State> AndroidRsc<State> {
|
||||||
|
pub fn create_state<T: 'static>(&mut self, id: impl IdLike, data: T) -> WeakState<T> {
|
||||||
|
self.state.add(id.id(), data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State> UiRsc for AndroidRsc<State> {
|
||||||
|
fn ui(&self) -> &UiData {
|
||||||
|
&self.ui
|
||||||
|
}
|
||||||
|
fn ui_mut(&mut self) -> &mut UiData {
|
||||||
|
&mut self.ui
|
||||||
|
}
|
||||||
|
fn on_draw(&mut self, active: &ActiveData) {
|
||||||
|
self.events.draw(active);
|
||||||
|
}
|
||||||
|
fn on_undraw(&mut self, active: &ActiveData) {
|
||||||
|
self.events.undraw(active);
|
||||||
|
}
|
||||||
|
fn on_remove(&mut self, id: WidgetId) {
|
||||||
|
self.events.remove(id);
|
||||||
|
self.state.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: 'static> HasState for AndroidRsc<State> {
|
||||||
|
type State = State;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: 'static> HasEvents for AndroidRsc<State> {
|
||||||
|
fn events(&self) -> &EventManager<Self> {
|
||||||
|
&self.events
|
||||||
|
}
|
||||||
|
fn events_mut(&mut self) -> &mut EventManager<Self> {
|
||||||
|
&mut self.events
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: 'static> HasTasks for AndroidRsc<State> {
|
||||||
|
fn tasks_mut(&mut self) -> &mut Tasks<Self> {
|
||||||
|
&mut self.tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: 'static> HasWidgetState for AndroidRsc<State> {
|
||||||
|
fn widget_state(&self) -> &WidgetState {
|
||||||
|
&self.state
|
||||||
|
}
|
||||||
|
fn widget_state_mut(&mut self) -> &mut WidgetState {
|
||||||
|
&mut self.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `ViewPeer` android-view dispatches every callback to. One per
|
||||||
|
/// `RustView` instance; `new_peer` (below) builds it and hands the id to
|
||||||
|
/// Java the same way android-view's own demo does.
|
||||||
|
pub struct IrisViewPeer<State: AndroidAppState> {
|
||||||
|
pub(super) rsc: AndroidRsc<State>,
|
||||||
|
pub(super) render: UiRenderState,
|
||||||
|
pub(super) state: State,
|
||||||
|
task_recv: TaskMsgReceiver<AndroidRsc<State>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: 'static, I: RscIdx<AndroidRsc<State>>> std::ops::Index<I> for AndroidRsc<State> {
|
||||||
|
type Output = I::Output;
|
||||||
|
|
||||||
|
fn index(&self, index: I) -> &Self::Output {
|
||||||
|
index.get(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: 'static, I: RscIdx<AndroidRsc<State>>> std::ops::IndexMut<I> for AndroidRsc<State> {
|
||||||
|
fn index_mut(&mut self, index: I) -> &mut Self::Output {
|
||||||
|
index.get_mut(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: AndroidAppState> IrisViewPeer<State> {
|
||||||
|
fn drain_tasks(&mut self) {
|
||||||
|
while let Ok(update) = self.task_recv.try_recv() {
|
||||||
|
update(&mut self.state, &mut self.rsc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common tail for every callback that might have changed the cursor,
|
||||||
|
/// the text focus, or the widget tree: run the sensors that touch
|
||||||
|
/// input feeds, then ask for a frame if the result needs drawing.
|
||||||
|
/// Mirrors `default::DefaultApp::window_event`'s tail, split across
|
||||||
|
/// android-view's several entry points instead of winit's one.
|
||||||
|
pub(super) fn after_input(&mut self, ctx: &mut CallbackCtx) {
|
||||||
|
let window_size = self.window_size();
|
||||||
|
let ui_state = self.state.android_state_mut();
|
||||||
|
let cursor = ui_state.cursor.clone();
|
||||||
|
let old_focus = ui_state.focus;
|
||||||
|
self.render
|
||||||
|
.run_sensors(&mut self.rsc, &mut self.state, cursor, window_size);
|
||||||
|
|
||||||
|
let ui_state = self.state.android_state_mut();
|
||||||
|
if old_focus != ui_state.focus
|
||||||
|
&& let Some(old) = old_focus
|
||||||
|
{
|
||||||
|
old.edit(&mut self.rsc).deselect();
|
||||||
|
}
|
||||||
|
if std::mem::take(&mut ui_state.pending_show_keyboard) {
|
||||||
|
show_soft_input(&mut ctx.env, &ctx.view);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ui_state = self.state.android_state_mut();
|
||||||
|
ui_state.cursor.end_frame();
|
||||||
|
if self.render.needs_redraw(&ui_state.root, self.rsc.widgets()) {
|
||||||
|
ctx.view.post_frame_callback(&mut ctx.env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_size(&self) -> Vec2 {
|
||||||
|
let ui_state = self.state.android_state();
|
||||||
|
match &ui_state.renderer {
|
||||||
|
Some(r) => r.size(),
|
||||||
|
None => Vec2::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self) {
|
||||||
|
let ui_state = self.state.android_state();
|
||||||
|
if ui_state.renderer.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ui_state = self.state.android_state_mut();
|
||||||
|
self.render.update(&ui_state.root, &mut self.rsc);
|
||||||
|
let ui_state = self.state.android_state_mut();
|
||||||
|
let Some(renderer) = &mut ui_state.renderer else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
renderer.update(&mut self.rsc.ui, &mut self.render);
|
||||||
|
renderer.draw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_soft_input<'local>(env: &mut JNIEnv<'local>, view: &View<'local>) {
|
||||||
|
let imm = view.input_method_manager(env);
|
||||||
|
imm.show_soft_input(env, view, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<State: AndroidAppState> ViewPeer for IrisViewPeer<State> {
|
||||||
|
fn on_key_down<'local>(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx<'local>,
|
||||||
|
key_code: Keycode,
|
||||||
|
event: &KeyEvent<'local>,
|
||||||
|
) -> bool {
|
||||||
|
self.drain_tasks();
|
||||||
|
// With no `OnBackPressedCallback` registered on the Java side, the
|
||||||
|
// system still delivers the back gesture as a synthetic
|
||||||
|
// `KEYCODE_BACK` through this same path -- the legacy behaviour
|
||||||
|
// every view-based app gets by default, and enough for "the back
|
||||||
|
// gesture as an event" without a second JNI registry. See
|
||||||
|
// `android/insets.rs`'s doc comment for why insets could not take
|
||||||
|
// the same shortcut.
|
||||||
|
if key_code == Keycode::Back {
|
||||||
|
let handled = self.state.back_pressed(&mut self.rsc, &mut self.render);
|
||||||
|
if handled {
|
||||||
|
self.after_input(ctx);
|
||||||
|
}
|
||||||
|
return handled;
|
||||||
|
}
|
||||||
|
let handled = super::input::on_key(
|
||||||
|
&mut self.rsc,
|
||||||
|
&mut self.state,
|
||||||
|
&mut ctx.env,
|
||||||
|
key_code,
|
||||||
|
event,
|
||||||
|
);
|
||||||
|
if handled {
|
||||||
|
self.after_input(ctx);
|
||||||
|
}
|
||||||
|
handled
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_touch_event<'local>(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx<'local>,
|
||||||
|
event: &MotionEvent<'local>,
|
||||||
|
) -> bool {
|
||||||
|
self.drain_tasks();
|
||||||
|
let action = event.action_masked(&mut ctx.env);
|
||||||
|
let x = event.x(&mut ctx.env);
|
||||||
|
let y = event.y(&mut ctx.env);
|
||||||
|
let ui_state = self.state.android_state_mut();
|
||||||
|
match action {
|
||||||
|
MotionAction::Down => {
|
||||||
|
ui_state.cursor.pos = vec2(x, y);
|
||||||
|
ui_state.cursor.exists = true;
|
||||||
|
ui_state.cursor.buttons.left.update(true);
|
||||||
|
}
|
||||||
|
MotionAction::Move => {
|
||||||
|
ui_state.cursor.pos = vec2(x, y);
|
||||||
|
}
|
||||||
|
MotionAction::Up | MotionAction::Cancel => {
|
||||||
|
ui_state.cursor.pos = vec2(x, y);
|
||||||
|
ui_state.cursor.buttons.left.update(false);
|
||||||
|
}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
self.after_input(ctx);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_focus_changed<'local>(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx<'local>,
|
||||||
|
gain_focus: bool,
|
||||||
|
_direction: i32,
|
||||||
|
_previously_focused_rect: Option<&Rect<'local>>,
|
||||||
|
) {
|
||||||
|
self.drain_tasks();
|
||||||
|
if !gain_focus {
|
||||||
|
let ui_state = self.state.android_state_mut();
|
||||||
|
if let Some(focus) = ui_state.focus.take() {
|
||||||
|
focus.edit(&mut self.rsc).deselect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.after_input(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_attached_to_window(&mut self, _ctx: &mut CallbackCtx) {
|
||||||
|
self.drain_tasks();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface_changed<'local>(
|
||||||
|
&mut self,
|
||||||
|
ctx: &mut CallbackCtx<'local>,
|
||||||
|
holder: &android_view::SurfaceHolder<'local>,
|
||||||
|
_format: i32,
|
||||||
|
width: i32,
|
||||||
|
height: i32,
|
||||||
|
) {
|
||||||
|
self.drain_tasks();
|
||||||
|
let window = holder.surface(&mut ctx.env).to_native_window(&mut ctx.env);
|
||||||
|
// Drop the old renderer (and the surface it owns) before building
|
||||||
|
// one from the new window -- see `AndroidRenderer`'s doc comment.
|
||||||
|
let ui_state = self.state.android_state_mut();
|
||||||
|
ui_state.renderer = None;
|
||||||
|
ui_state.renderer = Some(AndroidRenderer::new(window, width as u32, height as u32));
|
||||||
|
self.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface_destroyed<'local>(
|
||||||
|
&mut self,
|
||||||
|
_ctx: &mut CallbackCtx<'local>,
|
||||||
|
_holder: &android_view::SurfaceHolder<'local>,
|
||||||
|
) {
|
||||||
|
self.state.android_state_mut().renderer = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_frame(&mut self, _ctx: &mut CallbackCtx, _frame_time_nanos: i64) {
|
||||||
|
self.drain_tasks();
|
||||||
|
self.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_input_connection(&mut self) -> Option<&mut dyn InputConnection> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Registers `IrisViewPeer<State>`'s native methods and builds one on every
|
||||||
|
/// `newViewPeer` call from Java. `State`'s app crate wraps this in a
|
||||||
|
/// concrete `extern "system" fn` (a generic function cannot be handed to
|
||||||
|
/// `register_view_class`, which wants a plain function pointer) -- see
|
||||||
|
/// `iris/android-app/src/lib.rs`.
|
||||||
|
pub fn new_peer<'local, State: AndroidAppState>(
|
||||||
|
env: JNIEnv<'local>,
|
||||||
|
view: View<'local>,
|
||||||
|
_context: Context<'local>,
|
||||||
|
) -> android_view::jni::sys::jlong {
|
||||||
|
let vm = env.get_java_vm().unwrap();
|
||||||
|
let global_view = env.new_global_ref(&view.0).unwrap();
|
||||||
|
let redraw: Arc<dyn RequestRedraw> = Arc::new(AndroidRedrawHandle::new(vm, global_view));
|
||||||
|
let (tasks, task_recv) = Tasks::init(redraw);
|
||||||
|
let mut rsc = AndroidRsc {
|
||||||
|
ui: Default::default(),
|
||||||
|
events: Default::default(),
|
||||||
|
tasks,
|
||||||
|
state: Default::default(),
|
||||||
|
_state: PhantomData,
|
||||||
|
};
|
||||||
|
let shared = Rc::new(RefCell::new(Shared::default()));
|
||||||
|
let ui_state = AndroidUiState::new(shared.clone());
|
||||||
|
let state = State::new(ui_state, &mut rsc);
|
||||||
|
let peer = IrisViewPeer {
|
||||||
|
rsc,
|
||||||
|
render: UiRenderState::new(),
|
||||||
|
state,
|
||||||
|
task_recv,
|
||||||
|
};
|
||||||
|
let id = android_view::register_view_peer(peer);
|
||||||
|
super::insets::register(id, shared);
|
||||||
|
id
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// What focusing a text field takes from whichever backend is running --
|
||||||
|
/// tracked here rather than duplicated per backend, since `Selector` and
|
||||||
|
/// `Selectable` (below) are the *only* thing that decides which `TextEdit`
|
||||||
|
/// is the IME's target, and both platforms need the same double-click
|
||||||
|
/// timing and the same "remember which one" bookkeeping. What differs is
|
||||||
|
/// what happens *after* the focus record is set: winit tells the
|
||||||
|
/// compositor an IME area (`focus_gained`, in `default/attr.rs`); on
|
||||||
|
/// android-view a keyboard has to be asked for explicitly, and only from a
|
||||||
|
/// JNI call this crate cannot make outside a view callback -- so
|
||||||
|
/// `focus_gained` there (`android/attr.rs`) just raises a flag the next
|
||||||
|
/// touch callback consumes. See RUST.md's I2.
|
||||||
|
pub trait FocusHost {
|
||||||
|
/// True on a click close enough in time to the previous one to grow a
|
||||||
|
/// selection instead of starting a new one, updating the clock as a
|
||||||
|
/// side effect the way a real double-click timer does.
|
||||||
|
fn recent_click(&mut self) -> bool;
|
||||||
|
fn set_focus(&mut self, id: Option<WeakWidget<TextEdit>>);
|
||||||
|
/// Called after a `TextEdit` becomes the focus target, with the region
|
||||||
|
/// it was hit in (`None` when the widget could not be located, which
|
||||||
|
/// happens for one it was just deselected from).
|
||||||
|
fn focus_gained(&mut self, region: Option<PixelRegion>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper shared by every `FocusHost` impl, so the double-click window is
|
||||||
|
/// one constant rather than one per backend.
|
||||||
|
pub fn recent_click(last_click: &mut Instant) -> bool {
|
||||||
|
let now = Instant::now();
|
||||||
|
let recent = (now - *last_click) < Duration::from_millis(300);
|
||||||
|
*last_click = now;
|
||||||
|
recent
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Selector;
|
||||||
|
|
||||||
|
impl<Rsc: HasEvents, W: Widget + 'static> WidgetAttr<Rsc, W> for Selector
|
||||||
|
where
|
||||||
|
Rsc::State: FocusHost,
|
||||||
|
{
|
||||||
|
type Input = WeakWidget<TextEdit>;
|
||||||
|
|
||||||
|
fn run(rsc: &mut Rsc, container: WeakWidget<W>, id: Self::Input) {
|
||||||
|
rsc.register_event(container, CursorSense::click_or_drag(), move |ctx, rsc| {
|
||||||
|
let region = ctx.data.render.window_region(&id, &*rsc).unwrap();
|
||||||
|
let id_pos = region.top_left;
|
||||||
|
let container_pos = ctx
|
||||||
|
.data
|
||||||
|
.render
|
||||||
|
.window_region(&container, &*rsc)
|
||||||
|
.unwrap()
|
||||||
|
.top_left;
|
||||||
|
let pos = ctx.data.pos + container_pos - id_pos;
|
||||||
|
let size = region.size();
|
||||||
|
select(
|
||||||
|
rsc,
|
||||||
|
ctx.data.render,
|
||||||
|
ctx.state,
|
||||||
|
id,
|
||||||
|
pos,
|
||||||
|
size,
|
||||||
|
ctx.data.sense.is_dragging(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Selectable;
|
||||||
|
|
||||||
|
impl<Rsc: HasEvents> WidgetAttr<Rsc, TextEdit> for Selectable
|
||||||
|
where
|
||||||
|
Rsc::State: FocusHost,
|
||||||
|
{
|
||||||
|
type Input = ();
|
||||||
|
|
||||||
|
fn run(rsc: &mut Rsc, id: WeakWidget<TextEdit>, _: Self::Input) {
|
||||||
|
rsc.register_event(id, CursorSense::click_or_drag(), move |ctx, rsc| {
|
||||||
|
select(
|
||||||
|
rsc,
|
||||||
|
ctx.data.render,
|
||||||
|
ctx.state,
|
||||||
|
id,
|
||||||
|
ctx.data.pos,
|
||||||
|
ctx.data.size,
|
||||||
|
ctx.data.sense.is_dragging(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select(
|
||||||
|
rsc: &mut impl UiRsc,
|
||||||
|
render: &UiRenderState,
|
||||||
|
state: &mut impl FocusHost,
|
||||||
|
id: WeakWidget<TextEdit>,
|
||||||
|
pos: Vec2,
|
||||||
|
size: Vec2,
|
||||||
|
dragging: bool,
|
||||||
|
) {
|
||||||
|
let recent = state.recent_click();
|
||||||
|
id.edit(rsc).select(pos, size, dragging, recent);
|
||||||
|
state.set_focus(Some(id));
|
||||||
|
state.focus_gained(render.window_region(&id, &*rsc));
|
||||||
|
}
|
||||||
@@ -1,83 +1,22 @@
|
|||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use std::time::{Duration, Instant};
|
|
||||||
use winit::dpi::{LogicalPosition, LogicalSize};
|
use winit::dpi::{LogicalPosition, LogicalSize};
|
||||||
|
|
||||||
pub struct Selector;
|
impl<T: HasDefaultUiState> FocusHost for T {
|
||||||
|
fn recent_click(&mut self) -> bool {
|
||||||
impl<Rsc: HasEvents, W: Widget + 'static> WidgetAttr<Rsc, W> for Selector
|
crate::attr::recent_click(&mut self.default_state_mut().last_click)
|
||||||
where
|
|
||||||
Rsc::State: HasDefaultUiState,
|
|
||||||
{
|
|
||||||
type Input = WeakWidget<TextEdit>;
|
|
||||||
|
|
||||||
fn run(rsc: &mut Rsc, container: WeakWidget<W>, id: Self::Input) {
|
|
||||||
rsc.register_event(container, CursorSense::click_or_drag(), move |ctx, rsc| {
|
|
||||||
let region = ctx.data.render.window_region(&id, &*rsc).unwrap();
|
|
||||||
let id_pos = region.top_left;
|
|
||||||
let container_pos = ctx
|
|
||||||
.data
|
|
||||||
.render
|
|
||||||
.window_region(&container, &*rsc)
|
|
||||||
.unwrap()
|
|
||||||
.top_left;
|
|
||||||
let pos = ctx.data.pos + container_pos - id_pos;
|
|
||||||
let size = region.size();
|
|
||||||
select(
|
|
||||||
rsc,
|
|
||||||
ctx.data.render,
|
|
||||||
ctx.state,
|
|
||||||
id,
|
|
||||||
pos,
|
|
||||||
size,
|
|
||||||
ctx.data.sense.is_dragging(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Selectable;
|
fn set_focus(&mut self, id: Option<WeakWidget<TextEdit>>) {
|
||||||
|
self.default_state_mut().focus = id;
|
||||||
impl<Rsc: HasEvents> WidgetAttr<Rsc, TextEdit> for Selectable
|
|
||||||
where
|
|
||||||
Rsc::State: HasDefaultUiState,
|
|
||||||
{
|
|
||||||
type Input = ();
|
|
||||||
|
|
||||||
fn run(rsc: &mut Rsc, id: WeakWidget<TextEdit>, _: Self::Input) {
|
|
||||||
rsc.register_event(id, CursorSense::click_or_drag(), move |ctx, rsc| {
|
|
||||||
select(
|
|
||||||
rsc,
|
|
||||||
ctx.data.render,
|
|
||||||
ctx.state,
|
|
||||||
id,
|
|
||||||
ctx.data.pos,
|
|
||||||
ctx.data.size,
|
|
||||||
ctx.data.sense.is_dragging(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select(
|
fn focus_gained(&mut self, region: Option<PixelRegion>) {
|
||||||
rsc: &mut impl UiRsc,
|
let state = self.default_state_mut();
|
||||||
render: &UiRenderState,
|
let Some(region) = region else { return };
|
||||||
state: &mut impl HasDefaultUiState,
|
|
||||||
id: WeakWidget<TextEdit>,
|
|
||||||
pos: Vec2,
|
|
||||||
size: Vec2,
|
|
||||||
dragging: bool,
|
|
||||||
) {
|
|
||||||
let state = state.default_state_mut();
|
|
||||||
let now = Instant::now();
|
|
||||||
let recent = (now - state.last_click) < Duration::from_millis(300);
|
|
||||||
state.last_click = now;
|
|
||||||
id.edit(rsc).select(pos, size, dragging, recent);
|
|
||||||
if let Some(region) = render.window_region(&id, &*rsc) {
|
|
||||||
state.window.set_ime_allowed(true);
|
state.window.set_ime_allowed(true);
|
||||||
state.window.set_ime_cursor_area(
|
state.window.set_ime_cursor_area(
|
||||||
LogicalPosition::<f32>::from(region.top_left.tuple()),
|
LogicalPosition::<f32>::from(region.top_left.tuple()),
|
||||||
LogicalSize::<f32>::from(region.size().tuple()),
|
LogicalSize::<f32>::from(region.size().tuple()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
state.focus = Some(id);
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
use iris_core::Event;
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
|
||||||
pub struct Submit;
|
|
||||||
impl Event for Submit {}
|
|
||||||
|
|
||||||
#[derive(Eq, PartialEq, Hash, Clone)]
|
|
||||||
pub struct Edited;
|
|
||||||
impl Event for Edited {}
|
|
||||||
@@ -13,21 +13,12 @@ use winit::{
|
|||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod attr;
|
mod attr;
|
||||||
mod event;
|
|
||||||
mod input;
|
mod input;
|
||||||
mod render;
|
mod render;
|
||||||
mod sense;
|
|
||||||
mod state;
|
|
||||||
mod task;
|
|
||||||
|
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
pub use attr::*;
|
|
||||||
pub use event::*;
|
|
||||||
pub use input::*;
|
pub use input::*;
|
||||||
pub use render::*;
|
pub use render::*;
|
||||||
pub use sense::*;
|
|
||||||
pub use state::*;
|
|
||||||
pub use task::*;
|
|
||||||
|
|
||||||
pub type Proxy<Event> = EventLoopProxy<Event>;
|
pub type Proxy<Event> = EventLoopProxy<Event>;
|
||||||
|
|
||||||
@@ -309,12 +300,6 @@ impl<State: DefaultAppState> AppState for DefaultApp<State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RscIdx<Rsc> {
|
|
||||||
type Output;
|
|
||||||
fn get(self, rsc: &Rsc) -> &Self::Output;
|
|
||||||
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> {
|
impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::Index<I> for DefaultRsc<State> {
|
||||||
type Output = I::Output;
|
type Output = I::Output;
|
||||||
|
|
||||||
@@ -328,27 +313,3 @@ impl<State: 'static, I: RscIdx<DefaultRsc<State>>> std::ops::IndexMut<I> for Def
|
|||||||
index.get_mut(self)
|
index.get_mut(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W: Widget, Rsc: UiRsc> RscIdx<Rsc> for WeakWidget<W> {
|
|
||||||
type Output = W;
|
|
||||||
|
|
||||||
fn get(self, rsc: &Rsc) -> &Self::Output {
|
|
||||||
&rsc.ui().widgets[self]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
|
|
||||||
&mut rsc.ui_mut().widgets[self]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: 'static, Rsc: HasWidgetState> RscIdx<Rsc> for WeakState<T> {
|
|
||||||
type Output = T;
|
|
||||||
|
|
||||||
fn get(self, rsc: &Rsc) -> &Self::Output {
|
|
||||||
rsc.widget_state().get(self)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
|
|
||||||
rsc.widget_state_mut().get_mut(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::task::RequestRedraw;
|
||||||
use iris_core::{UiData, UiRenderNode, UiRenderState};
|
use iris_core::{UiData, UiRenderNode, UiRenderState};
|
||||||
use pollster::FutureExt;
|
use pollster::FutureExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -6,6 +7,12 @@ use winit::{dpi::PhysicalSize, window::Window};
|
|||||||
|
|
||||||
pub const CLEAR_COLOR: Color = Color::BLACK;
|
pub const CLEAR_COLOR: Color = Color::BLACK;
|
||||||
|
|
||||||
|
impl RequestRedraw for Window {
|
||||||
|
fn request_redraw(&self) {
|
||||||
|
Window::request_redraw(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct UiRenderer {
|
pub struct UiRenderer {
|
||||||
window: Arc<Window>,
|
window: Arc<Window>,
|
||||||
surface: Surface<'static>,
|
surface: Surface<'static>,
|
||||||
|
|||||||
+15
-1
@@ -2,7 +2,21 @@ use iris_core::*;
|
|||||||
use iris_macro::*;
|
use iris_macro::*;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::default::{TaskCtx, TaskUpdate, Tasks};
|
use crate::task::{TaskCtx, TaskUpdate, Tasks};
|
||||||
|
|
||||||
|
/// A field's Enter key (without a shift, in a multi-line field). Backend
|
||||||
|
/// input handling raises it directly rather than through `on`, since a
|
||||||
|
/// field does not know ahead of time whether anything is listening.
|
||||||
|
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||||
|
pub struct Submit;
|
||||||
|
impl Event for Submit {}
|
||||||
|
|
||||||
|
/// A field's content changed as a result of input the backend applied
|
||||||
|
/// directly to it (a keystroke, an IME commit) rather than through a
|
||||||
|
/// widget event handler.
|
||||||
|
#[derive(Eq, PartialEq, Hash, Clone)]
|
||||||
|
pub struct Edited;
|
||||||
|
impl Event for Edited {}
|
||||||
|
|
||||||
pub trait Eventable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
|
pub trait Eventable<Rsc: HasEvents, Tag>: WidgetLike<Rsc, Tag> {
|
||||||
fn on<E: EventLike>(
|
fn on<E: EventLike>(
|
||||||
|
|||||||
+26
-1
@@ -1,12 +1,29 @@
|
|||||||
#![feature(unboxed_closures)]
|
#![feature(unboxed_closures)]
|
||||||
#![feature(fn_traits)]
|
#![feature(fn_traits)]
|
||||||
#![feature(associated_type_defaults)]
|
// Only `default::DefaultAppState::Event`'s default uses this; unused (and
|
||||||
|
// warned about) on the android target, which has no such default.
|
||||||
|
#![cfg_attr(not(target_os = "android"), feature(associated_type_defaults))]
|
||||||
#![feature(unsize)]
|
#![feature(unsize)]
|
||||||
#![feature(option_into_flat_iter)]
|
#![feature(option_into_flat_iter)]
|
||||||
#![feature(async_fn_traits)]
|
#![feature(async_fn_traits)]
|
||||||
|
|
||||||
|
// Two windowing backends live side by side, chosen by target rather than by
|
||||||
|
// feature flag: winit everywhere but Android, android-view on it. They are
|
||||||
|
// mutually exclusive rather than both-compiled-in because winit's own
|
||||||
|
// Android support pulls in `android-activity`, which needs one of its
|
||||||
|
// `game-activity`/`native-activity` features selected -- exactly what
|
||||||
|
// `iris-core` was kept free of, and android-view is the framework's own
|
||||||
|
// answer to the same surface on that platform. See RUST.md's I2.
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub mod android;
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub mod default;
|
pub mod default;
|
||||||
|
|
||||||
|
pub mod attr;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
|
pub mod sense;
|
||||||
|
pub mod state;
|
||||||
|
pub mod task;
|
||||||
pub mod widget;
|
pub mod widget;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -19,10 +36,18 @@ pub use iris_macro as macros;
|
|||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
pub use android::*;
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub use default::*;
|
pub use default::*;
|
||||||
|
|
||||||
|
pub use attr::*;
|
||||||
pub use event::*;
|
pub use event::*;
|
||||||
pub use iris_core::*;
|
pub use iris_core::*;
|
||||||
pub use iris_macro::*;
|
pub use iris_macro::*;
|
||||||
|
pub use sense::*;
|
||||||
|
pub use state::*;
|
||||||
|
pub use task::*;
|
||||||
pub use widget::*;
|
pub use widget::*;
|
||||||
|
|
||||||
pub use iris_core::util::Vec2;
|
pub use iris_core::util::Vec2;
|
||||||
|
|||||||
File renamed without changes.
@@ -1,7 +1,8 @@
|
|||||||
use iris_core::{
|
use iris_core::{
|
||||||
WidgetId,
|
UiRsc, WidgetId,
|
||||||
util::{HashMap, HashSet},
|
util::{HashMap, HashSet},
|
||||||
};
|
};
|
||||||
|
use iris_core::{WeakWidget, Widget};
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
@@ -73,3 +74,39 @@ impl<'a, T: 'static> FnOnce<(&'a mut WidgetState,)> for WeakState<T> {
|
|||||||
state.get_mut(self)
|
state.get_mut(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What `Rsc[weak_handle]` indexes through -- one impl per kind of handle
|
||||||
|
/// (a widget, a piece of per-widget state), shared by both backends' `Rsc`
|
||||||
|
/// types since indexing a widget tree has nothing to do with windowing.
|
||||||
|
/// Each backend still needs its own `Index`/`IndexMut for ItsRsc<State>`
|
||||||
|
/// (`default/mod.rs`, `android/view.rs`), because a blanket impl over every
|
||||||
|
/// `I: RscIdx<Rsc>` for every possible `Rsc` would conflict between crates.
|
||||||
|
pub trait RscIdx<Rsc> {
|
||||||
|
type Output;
|
||||||
|
fn get(self, rsc: &Rsc) -> &Self::Output;
|
||||||
|
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Widget, Rsc: UiRsc> RscIdx<Rsc> for WeakWidget<W> {
|
||||||
|
type Output = W;
|
||||||
|
|
||||||
|
fn get(self, rsc: &Rsc) -> &Self::Output {
|
||||||
|
&rsc.ui().widgets[self]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
|
||||||
|
&mut rsc.ui_mut().widgets[self]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: 'static, Rsc: HasWidgetState> RscIdx<Rsc> for WeakState<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn get(self, rsc: &Rsc) -> &Self::Output {
|
||||||
|
rsc.widget_state().get(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mut(self, rsc: &mut Rsc) -> &mut Self::Output {
|
||||||
|
rsc.widget_state_mut().get_mut(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,17 @@ use tokio::{
|
|||||||
unbounded_channel as async_channel,
|
unbounded_channel as async_channel,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use winit::window::Window;
|
|
||||||
|
/// What a completed task nudges when it wants its result drawn. Shared
|
||||||
|
/// between backends rather than typed as `winit::window::Window` directly:
|
||||||
|
/// android-view has no `Window` at all, and the redraw request there is a
|
||||||
|
/// JNI call (`View::post_frame_callback`) rather than a method call on a
|
||||||
|
/// value this crate owns. Each backend supplies its own implementation --
|
||||||
|
/// `default/render.rs` for winit, `android/render.rs` for android-view --
|
||||||
|
/// and this module never needs to know which one it is holding.
|
||||||
|
pub trait RequestRedraw: Send + Sync + 'static {
|
||||||
|
fn request_redraw(&self);
|
||||||
|
}
|
||||||
|
|
||||||
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
pub type TaskMsgSender<Rsc> = SyncSender<Box<dyn TaskUpdate<Rsc>>>;
|
||||||
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
|
pub type TaskMsgReceiver<Rsc> = SyncReceiver<Box<dyn TaskUpdate<Rsc>>>;
|
||||||
@@ -23,7 +33,7 @@ impl<F: FnOnce(&mut Rsc::State, &mut Rsc) + Send, Rsc: HasState> TaskUpdate<Rsc>
|
|||||||
|
|
||||||
pub struct Tasks<Rsc: HasState> {
|
pub struct Tasks<Rsc: HasState> {
|
||||||
start: AsyncSender<BoxTask>,
|
start: AsyncSender<BoxTask>,
|
||||||
window: Arc<Window>,
|
redraw: Arc<dyn RequestRedraw>,
|
||||||
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
msg_send: SyncSender<Box<dyn TaskUpdate<Rsc>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +55,7 @@ impl<Rsc: HasState + 'static> TaskCtx<Rsc> {
|
|||||||
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
type BoxTask = Pin<Box<dyn Future<Output = ()> + Send>>;
|
||||||
|
|
||||||
impl<Rsc: HasState> Tasks<Rsc> {
|
impl<Rsc: HasState> Tasks<Rsc> {
|
||||||
pub fn init(window: Arc<Window>) -> (Self, TaskMsgReceiver<Rsc>) {
|
pub fn init(redraw: Arc<dyn RequestRedraw>) -> (Self, TaskMsgReceiver<Rsc>) {
|
||||||
let (start, start_recv) = async_channel();
|
let (start, start_recv) = async_channel();
|
||||||
let (msgs, msgs_recv) = sync_channel();
|
let (msgs, msgs_recv) = sync_channel();
|
||||||
std::thread::spawn(|| {
|
std::thread::spawn(|| {
|
||||||
@@ -56,7 +66,7 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
|||||||
Self {
|
Self {
|
||||||
start,
|
start,
|
||||||
msg_send: msgs,
|
msg_send: msgs,
|
||||||
window,
|
redraw,
|
||||||
},
|
},
|
||||||
msgs_recv,
|
msgs_recv,
|
||||||
)
|
)
|
||||||
@@ -67,10 +77,10 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
|||||||
F::CallOnceFuture: Send,
|
F::CallOnceFuture: Send,
|
||||||
{
|
{
|
||||||
let send = self.msg_send.clone();
|
let send = self.msg_send.clone();
|
||||||
let window = self.window.clone();
|
let redraw = self.redraw.clone();
|
||||||
let _ = self.start.send(Box::pin(async move {
|
let _ = self.start.send(Box::pin(async move {
|
||||||
task(TaskCtx::new(send)).await;
|
task(TaskCtx::new(send)).await;
|
||||||
window.request_redraw();
|
redraw.request_redraw();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ use crate::prelude::*;
|
|||||||
use iris_core::{TextData, UiColor};
|
use iris_core::{TextData, UiColor};
|
||||||
use parley::{Affinity, Layout, Selection};
|
use parley::{Affinity, Layout, Selection};
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
use winit::{
|
use winit::{
|
||||||
event::KeyEvent,
|
event::KeyEvent,
|
||||||
keyboard::{Key, NamedKey},
|
keyboard::{Key, NamedKey},
|
||||||
@@ -28,6 +29,7 @@ pub struct TextEdit {
|
|||||||
/// way to say, since it always denotes some position in the text. A
|
/// way to say, since it always denotes some position in the text. A
|
||||||
/// collapsed selection is a caret; an uncollapsed one is a span.
|
/// collapsed selection is a caret; an uncollapsed one is a span.
|
||||||
selection: Option<Selection>,
|
selection: Option<Selection>,
|
||||||
|
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||||
history: Vec<(String, Option<Selection>)>,
|
history: Vec<(String, Option<Selection>)>,
|
||||||
double_hit: Option<usize>,
|
double_hit: Option<usize>,
|
||||||
pub mode: EditMode,
|
pub mode: EditMode,
|
||||||
@@ -57,6 +59,26 @@ impl TextEdit {
|
|||||||
}
|
}
|
||||||
Some(self.buf.text()[sel.text_range()].to_string())
|
Some(self.buf.text()[sel.text_range()].to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The field's content. Byte-indexed, like everything else here since
|
||||||
|
/// I1 moved to parley -- an IME bridge (`android/ime.rs`) converts to
|
||||||
|
/// and from UTF-16 code units at its own edge rather than this type
|
||||||
|
/// knowing about that encoding.
|
||||||
|
pub fn text(&self) -> &str {
|
||||||
|
self.view.buf.text()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The selection as a byte range, collapsed to `caret..caret` when
|
||||||
|
/// there is no span. `None` when the field is not focused.
|
||||||
|
pub fn selection_range(&self) -> Option<std::ops::Range<usize>> {
|
||||||
|
Some(self.selection?.text_range())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The caret's byte offset -- the focus end of the selection, which is
|
||||||
|
/// where typing lands regardless of which end of a span it is.
|
||||||
|
pub fn caret(&self) -> Option<usize> {
|
||||||
|
Some(self.selection?.focus().index())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Widget for TextEdit {
|
impl Widget for TextEdit {
|
||||||
@@ -115,6 +137,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Keep the selection valid after the text underneath it changed.
|
/// Keep the selection valid after the text underneath it changed.
|
||||||
|
#[cfg_attr(target_os = "android", allow(dead_code))]
|
||||||
fn refresh(&mut self) {
|
fn refresh(&mut self) {
|
||||||
if let Some(sel) = self.text.selection {
|
if let Some(sel) = self.text.selection {
|
||||||
let layout = self.layout();
|
let layout = self.layout();
|
||||||
@@ -279,6 +302,20 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
self.set_caret(start);
|
self.set_caret(start);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same range delete, exposed for callers that already have byte
|
||||||
|
/// offsets in hand rather than a `Motion` -- the IME's
|
||||||
|
/// `deleteSurroundingText`, which android-view hands over in UTF-16
|
||||||
|
/// code units that `android/ime.rs` converts before calling this.
|
||||||
|
pub fn delete_byte_range(&mut self, start: usize, end: usize) {
|
||||||
|
self.delete_range(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move the caret to a byte offset, collapsing any selection -- the
|
||||||
|
/// IME's `setSelection`.
|
||||||
|
pub fn set_cursor_byte(&mut self, index: usize) {
|
||||||
|
self.set_caret(index);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn select_all(&mut self) {
|
pub fn select_all(&mut self) {
|
||||||
let len = self.text.view.buf.text().len();
|
let len = self.text.view.buf.text().len();
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
@@ -336,6 +373,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
self.text.double_hit = None;
|
self.text.double_hit = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
|
||||||
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
let old = (self.text.view.buf.text().to_string(), self.text.selection);
|
||||||
let mut undo = false;
|
let mut undo = false;
|
||||||
@@ -352,6 +390,7 @@ impl<'a> TextEditCtx<'a> {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "android"))]
|
||||||
fn apply_event_inner(
|
fn apply_event_inner(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: &KeyEvent,
|
event: &KeyEvent,
|
||||||
|
|||||||
Reference in new issue
Block a user