From b23a657c44bbe301b76ad1633716bac7e1c4ea1b Mon Sep 17 00:00:00 2001 From: David Faure Date: Sun, 14 Sep 2025 01:29:41 +0200 Subject: [PATCH] Hide Android selection handles when scrolled out of view In a LineEdit with a long text, when dragging the selection to the point where the anchor scrolls out of view in the other direection, it used to keep being visible, outside the LineEdit. This commit fixes that by passing the clip rect to the java code that positions the selection handles. --- .../java/SlintAndroidJavaHelper.java | 27 ++++++++++++++----- .../backends/android-activity/javahelper.rs | 13 ++++++--- internal/core/items/text.rs | 14 +++++++--- internal/core/window.rs | 2 ++ 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/internal/backends/android-activity/java/SlintAndroidJavaHelper.java b/internal/backends/android-activity/java/SlintAndroidJavaHelper.java index a32210aca..0f7a9da02 100644 --- a/internal/backends/android-activity/java/SlintAndroidJavaHelper.java +++ b/internal/backends/android-activity/java/SlintAndroidJavaHelper.java @@ -243,18 +243,31 @@ class SlintInputView extends View { mCursorHandle.setPosition(left_x, left_y); handleHeight = mCursorHandle.getHeight(); } else if (num_handles == 2) { - if (mLeftHandle == null) { - mLeftHandle = new InputHandle(this, android.R.attr.textSelectHandleLeft); + if (left_x != -1) { + if (mLeftHandle == null) { + mLeftHandle = new InputHandle(this, android.R.attr.textSelectHandleLeft); + } + mLeftHandle.setPosition(left_x, left_y); + handleHeight = mLeftHandle.getHeight(); + } else { + if (mLeftHandle != null) { + mLeftHandle.hide(); + } } - if (mRightHandle == null) { - mRightHandle = new InputHandle(this, android.R.attr.textSelectHandleRight); + if (right_x != -1) { + if (mRightHandle == null) { + mRightHandle = new InputHandle(this, android.R.attr.textSelectHandleRight); + } + mRightHandle.setPosition(right_x, right_y); + handleHeight = mRightHandle.getHeight(); + } else { + if (mRightHandle != null) { + mRightHandle.hide(); + } } if (mCursorHandle != null) { mCursorHandle.hide(); } - mLeftHandle.setPosition(left_x, left_y); - mRightHandle.setPosition(right_x, right_y); - handleHeight = mLeftHandle.getHeight(); showActionMenu(); } else { if (mCursorHandle != null) { diff --git a/internal/backends/android-activity/javahelper.rs b/internal/backends/android-activity/javahelper.rs index 12733f4cc..c6b2eebd6 100644 --- a/internal/backends/android-activity/javahelper.rs +++ b/internal/backends/android-activity/javahelper.rs @@ -194,15 +194,22 @@ impl JavaHelper { }; env.delete_local_ref(class_it)?; - let cur_origin = data.cursor_rect_origin.to_physical(scale_factor); + 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); + let cur_visible = data.clip_rect.map_or(true, |r| { + r.contains(i_slint_core::lengths::logical_point_from_api(data.cursor_rect_origin)) + }); + let anchor_visible = data.clip_rect.map_or(true, |r| { + r.contains(i_slint_core::lengths::logical_point_from_api(data.anchor_point)) + }); + // 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 = cur_origin.x + cur_size.width as i32 / 2; + 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 = anchor_origin.x; + let anchor_x = if anchor_visible { anchor_origin.x } else { -1 }; let anchor_y = anchor_origin.y + 2 * cur_size.width as i32; env.call_method( diff --git a/internal/core/items/text.rs b/internal/core/items/text.rs index c2043deef..479eba924 100644 --- a/internal/core/items/text.rs +++ b/internal/core/items/text.rs @@ -1505,15 +1505,22 @@ impl TextInput { let cursor_relative = self.cursor_rect_for_byte_offset(cursor_position, window_adapter, self_rc); let geometry = self_rc.geometry(); - let origin = self_rc.map_to_window(geometry.origin).to_vector(); + let origin = self_rc.map_to_window(geometry.origin); + let origin_vector = origin.to_vector(); let cursor_rect_origin = - crate::api::LogicalPosition::from_euclid(cursor_relative.origin + origin); + crate::api::LogicalPosition::from_euclid(cursor_relative.origin + origin_vector); let cursor_rect_size = crate::api::LogicalSize::from_euclid(cursor_relative.size); let anchor_point = crate::api::LogicalPosition::from_euclid( self.cursor_rect_for_byte_offset(anchor_position, window_adapter, self_rc).origin - + origin + + origin_vector + cursor_relative.size, ); + let maybe_parent = + self_rc.parent_item(crate::item_tree::ParentItemTraversalMode::StopAtPopups); + let clip_rect = maybe_parent.map(|parent| { + let geom = parent.geometry(); + LogicalRect::new(parent.map_to_window(geom.origin), geom.size) + }); InputMethodProperties { text, @@ -1525,6 +1532,7 @@ impl TextInput { cursor_rect_size, anchor_point, input_type: self.input_type(), + clip_rect, } } diff --git a/internal/core/window.rs b/internal/core/window.rs index c297a81df..67c6c1f3e 100644 --- a/internal/core/window.rs +++ b/internal/core/window.rs @@ -293,6 +293,8 @@ pub struct InputMethodProperties { pub anchor_point: LogicalPosition, /// The type of input for the text edit. pub input_type: InputType, + /// The clip rect in window coordinates + pub clip_rect: Option, } /// This struct describes layout constraints of a resizable element, such as a window.