Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jetbrains.skia.BackendRenderTarget
Expand Down Expand Up @@ -126,6 +128,15 @@ internal class TaoComposeSceneHostLinux(
if (fullyTransparent) 0 else 0xFFFFFFFF.toInt(),
)

/**
* IME preedit / commit routing (#558).
*
* No typed-key fallback: that argument exists for the macOS PressAndHold
* accent picker, which has no GTK counterpart — an input method only ever
* delivers text while a text-input session is up.
*/
private val imeSession = TaoImeSession()

/** App-level pre-dispatch hook. See [TaoComposeSceneHost.previewKeyHandler]. */
var previewKeyHandler: ((KeyEvent) -> Boolean)? = null

Expand Down Expand Up @@ -455,6 +466,8 @@ internal class TaoComposeSceneHostLinux(
getRootNode = { scene!!.rootDragAndDropNode },
outboundLauncher = ::launchLinuxOutboundDrag,
)
window.imePreedit = imeSession::preedit
window.imeCommit = imeSession::commit
val platformContext =
LinuxTaoPlatformContext(
windowHandle = window.handle,
Expand All @@ -479,6 +492,7 @@ internal class TaoComposeSceneHostLinux(
semanticsOwnerListener = semanticsOwnerListener,
dragAndDropManager = dndManager,
textToolbar = textToolbar,
onInputSession = { imeSession.onInputSession(it) },
isWindowTransparent = fullyTransparent,
)
sceneBundle =
Expand Down Expand Up @@ -2260,6 +2274,9 @@ internal class TaoComposeSceneHostLinux(
}

fun detach() {
window.imePreedit = null
window.imeCommit = null
imeSession.onInputSession(null)
shutdownA11yScheduler()
textToolbar.hide()
if (dev.nucleusframework.window.tao.ffi.NativeTaoLinuxDndBridge.isLoaded &&
Expand Down Expand Up @@ -2534,6 +2551,8 @@ private class LinuxTaoPlatformContext(
override val semanticsOwnerListener: androidx.compose.ui.platform.PlatformContext.SemanticsOwnerListener?,
override val dragAndDropManager: androidx.compose.ui.platform.PlatformDragAndDropManager,
override val textToolbar: androidx.compose.ui.platform.TextToolbar,
/** Publishes the active text-input session to the host's [TaoImeSession] (#558). */
private val onInputSession: (androidx.compose.ui.platform.PlatformTextInputMethodRequest?) -> Unit = {},
// #559: forwarded to Compose so `CanvasLayersComposeScene` picks the
// alpha-aware dialog-scrim blend mode (`BlendMode.SrcAtop`) on windows
// created with `transparent = true` — same as Compose Desktop's
Expand All @@ -2548,6 +2567,45 @@ private class LinuxTaoPlatformContext(
override val captionBar: androidx.compose.ui.platform.PlatformInsets get() = systemBars
}

/**
* Keeps the GTK input context anchored to the caret for as long as a field
* owns the input (#558).
*
* The macOS twin also has to activate the view's `NSTextInputContext`
* first; GTK needs no such step, because the context is created with — and
* follows the focus of — the window itself. So this only mirrors the caret
* rect, through the same `nativeSetImeRect` contract Windows uses.
*/
@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class)
override suspend fun startInputMethod(
request: androidx.compose.ui.platform.PlatformTextInputMethodRequest,
): Nothing {
onInputSession(request)
try {
coroutineScope {
launch {
androidx.compose.runtime
.snapshotFlow {
request.focusedRectInRoot()
}.collect { rect ->
if (rect != null) {
NativeTaoBridge.nativeSetImeRect(
windowHandle,
rect.left.toInt(),
rect.top.toInt(),
rect.width.toInt().coerceAtLeast(1),
rect.height.toInt().coerceAtLeast(1),
)
}
}
}
awaitCancellation()
}
} finally {
onInputSession(null)
}
}

override fun setPointerIcon(pointerIcon: androidx.compose.ui.input.pointer.PointerIcon) {
// The Rust side maps the code to a freedesktop cursor name and goes
// through `gdk_window_set_device_cursor` for every master pointer of
Expand Down
52 changes: 52 additions & 0 deletions decorated-window-tao/src/main/native/src/platform/linux/ime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Caret-rect plumbing for the Linux IME (#558).
//
// The three backends split by how the platform asks for the caret. macOS is
// pull-based, so `platform/macos/ime.rs` swizzles
// `firstRectForCharacterRange:` because AppKit asks the view where the caret
// is. Windows and Linux are push-based — the app tells the input context — and
// tao already owns that call.
//
// Where Linux differs from Windows is the shape of the answer. IMM32 takes a
// *point* and hangs the candidate list off it, so `platform/windows/ime.rs`
// sends the caret's bottom edge and is done. GTK takes the *area* the cursor
// covers and the input method keeps its own windows off that area, so the full
// rect goes through: pass a bare point and the "Tab to select" hint sits on top
// of the composition it is describing. Hence `set_ime_cursor_area` rather than
// `set_ime_position` here, and no bottom-edge adjustment — GTK derives the
// placement from the rect itself.

use jni::objects::JClass;
use jni::sys::{jint, jlong};
use jni::JNIEnv;

use tao::dpi::{PhysicalPosition, PhysicalSize};
use tao::platform::unix::WindowExtUnix;

use crate::state::WINDOWS;

/// Reports the caret rectangle to the input method, in *window-local physical
/// pixels* with a top-left origin — the same contract as the macOS and Windows
/// implementations, which is why the JVM side passes the same four numbers to
/// all three.
#[no_mangle]
pub extern "system" fn Java_dev_nucleusframework_window_tao_ffi_NativeTaoBridge_nativeSetImeRect(
_env: JNIEnv,
_class: JClass,
handle: jlong,
x_px: jint,
y_px: jint,
w_px: jint,
h_px: jint,
) {
let guard = match WINDOWS.lock() {
Ok(g) => g,
Err(_) => return,
};
let Some(map) = guard.as_ref() else { return };
if let Some(window) = map.get(&(handle as u64)) {
window.set_ime_cursor_area(
PhysicalPosition::new(x_px, y_px),
PhysicalSize::new(w_px.max(0), h_px.max(0)),
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub(crate) mod a11y;
pub(crate) mod decoration;
pub(crate) mod dnd;
pub(crate) mod handles;
pub(crate) mod ime;
pub(crate) mod monitor;
pub(crate) mod scroll;
pub(crate) mod touch;
4 changes: 4 additions & 0 deletions decorated-window-tao/src/main/native/vendor/tao/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ pub enum WindowEvent<'a> {
/// - **macOS**: `setMarkedText:` / `unmarkText` (nucleusframework#595).
/// - **Windows**: `WM_IME_COMPOSITION` with `GCS_COMPSTR`
/// (nucleusframework#558).
/// - **Linux**: the GTK input context's `preedit-changed`, and
/// `preedit-end` as an empty string (nucleusframework#558).
/// - Not emitted on other platforms.
ImePreedit(String),

Expand All @@ -359,6 +361,8 @@ pub enum WindowEvent<'a> {
/// (nucleusframework#595).
/// - **Windows**: `WM_IME_COMPOSITION` with `GCS_RESULTSTR`
/// (nucleusframework#558).
/// - **Linux**: the GTK input context's `commit`, when it arrives while a
/// composition is in flight (nucleusframework#558).
/// - Not emitted on other platforms.
ImeCommit(String),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub use crate::platform_impl::x11;
use crate::platform_impl::x11::xdisplay::XError;
pub use crate::platform_impl::EventLoop as UnixEventLoop;
use crate::{
dpi::{Position, Size},
error::{ExternalError, OsError},
event_loop::{EventLoopBuilder, EventLoopWindowTarget},
monitor::MonitorHandle,
Expand Down Expand Up @@ -86,6 +87,17 @@ pub trait WindowExtUnix {
fn set_skip_taskbar(&self, skip: bool) -> Result<(), ExternalError>;

fn set_badge_count(&self, count: Option<i64>, desktop_filename: Option<String>);

/// Tells the input method the rectangle the text caret occupies, in window
/// coordinates, so it keeps its preedit and candidate windows clear of the
/// text being typed (nucleusframework#558).
///
/// The cross-platform [`Window::set_ime_position`] carries only a point,
/// which is all IMM32 and AppKit need. GTK is area-based instead: the input
/// method is told the region the cursor covers and stays off it, so a bare
/// point leaves the candidate window free to sit on top of the composition.
/// Callers that know the caret's size should use this.
fn set_ime_cursor_area<P: Into<Position>, S: Into<Size>>(&self, position: P, size: S);
}

impl WindowExtUnix for Window {
Expand All @@ -112,6 +124,10 @@ impl WindowExtUnix for Window {
fn set_badge_count(&self, count: Option<i64>, desktop_filename: Option<String>) {
self.window.set_badge_count(count, desktop_filename);
}

fn set_ime_cursor_area<P: Into<Position>, S: Into<Size>>(&self, position: P, size: S) {
self.window.set_ime_cursor_area(position, size);
}
}

pub trait WindowBuilderExtUnix {
Expand Down
Loading
Loading