From 119362a269b1a4a04b96531d28f8e88b37c9b478 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 13:03:05 +0100 Subject: [PATCH 01/12] Added `scroll_inset` field to `EditableText`. `scroll_editable_text` scrolling calculations changed to support the inset. --- crates/bevy_text/src/editing.rs | 4 + .../bevy_ui/src/widget/text_input_layout.rs | 107 +++++++++++++++++- 2 files changed, 108 insertions(+), 3 deletions(-) diff --git a/crates/bevy_text/src/editing.rs b/crates/bevy_text/src/editing.rs index d5cc2b905773a..8927b10cf3b57 100644 --- a/crates/bevy_text/src/editing.rs +++ b/crates/bevy_text/src/editing.rs @@ -80,6 +80,7 @@ use alloc::sync::Arc; use bevy_clipboard::ClipboardRead; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; +use bevy_math::Vec2; use core::time::Duration; use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; @@ -144,6 +145,8 @@ pub struct EditableText { pub visible_width: Option, /// Allow new lines pub allow_newlines: bool, + /// Fraction of the visible input size to keep between the cursor and the view edge when scrolling. + pub scroll_inset: Vec2, } impl Default for EditableText { @@ -159,6 +162,7 @@ impl Default for EditableText { visible_lines: Some(1.), visible_width: None, allow_newlines: false, + scroll_inset: Vec2::new(0.1, 0.25), } } } diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index da53e23a45e96..a3c73cdba7733 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -540,17 +540,29 @@ pub fn scroll_editable_text( info.size.x } - view_size.x) .max(0.); + let max_scroll_y = (info.size.y - view_size.y).max(0.); scroll.set_if_neq(TextScroll(Vec2 { - x: scroll_axis( + x: scroll_axis_with_inset( + editable_text.scroll_inset.x.clamp(0., 0.49) * view_size.x, + 0., + max_scroll_x, scroll.0.x, scroll.0.x + view_size.x, cursor.min.x, cursor.max.x, ) - .clamp(0., max_scroll_x) .floor(), - y: scroll_axis(scroll.0.y, scroll.0.y + view_size.y, line_min, line_max).floor(), + y: scroll_axis_with_inset( + editable_text.scroll_inset.y.clamp(0., 0.49) * view_size.x, + 0., + max_scroll_y, + scroll.0.y, + scroll.0.y + view_size.y, + line_min, + line_max, + ) + .floor(), })); } @@ -585,3 +597,92 @@ fn scroll_axis(v_min: f32, v_max: f32, t_min: f32, t_max: f32) -> f32 { v_min } } + +fn scroll_axis_with_inset( + inset: f32, + scroll_min: f32, + scroll_max: f32, + v_min: f32, + v_max: f32, + t_min: f32, + t_max: f32, +) -> f32 { + let v_size = v_max - v_min; + let u_min = v_min + inset; + let u_max = v_max - inset; + let t_size = t_max - t_min; + + let new_v_min = if v_size - 2. * inset < t_size { + scroll_axis(v_min, v_max, t_min, t_max) + } else if t_min < u_min { + t_min - inset + } else if u_max < t_max { + t_max - v_size + inset + } else { + v_min + }; + + new_v_min.clamp(scroll_min, scroll_max) +} + +#[cfg(test)] +mod test { + use super::{scroll_axis, scroll_axis_with_inset}; + + #[test] + fn test_scroll_axis() { + assert_eq!(scroll_axis(0., 100., 0., 10.), 0.); + } + + #[test] + fn test_scroll_axis_with_inset() { + assert_eq!( + scroll_axis_with_inset(0.25, 0., 100., 0., 100., 0., 50.), + 0. + ); + assert_eq!( + scroll_axis_with_inset(0.25, 0., 0., 0., 100., 50., 100.), + 0. + ); + } + + #[test] + fn test_scroll_axis_with_inset_moves_to_inner_min() { + assert_eq!( + scroll_axis_with_inset(0.25, 0., 100., 50., 150., 60., 65.), + 35. + ); + } + + #[test] + fn test_scroll_axis_with_inset_moves_to_inner_max() { + assert_eq!( + scroll_axis_with_inset(0.25, 0., 100., 0., 100., 90., 95.), + 20. + ); + } + + #[test] + fn test_scroll_axis_with_inset_saturates() { + assert_eq!( + scroll_axis_with_inset(0.25, 0., 100., 10., 110., 10., 20.), + 0. + ); + assert_eq!( + scroll_axis_with_inset(0.25, 0., 100., 80., 180., 175., 180.), + 100. + ); + } + + #[test] + fn test_scroll_axis_with_inset_uses_full_view_when_target_larger_than_inner() { + assert_eq!( + scroll_axis_with_inset(0.25, 0., 100., 0., 100., 20., 90.), + 0. + ); + assert_eq!( + scroll_axis_with_inset(0.25, 0., 100., 0., 100., 80., 150.), + 50. + ); + } +} From 454334ec9c26b1b9099fdfa70170631b97422c3b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 13:33:32 +0100 Subject: [PATCH 02/12] `on_pointer_drag` queues `MoveToPoint` as well as `ExtendSelectionToPoint` when dragging. So each time the mouse moves during a drag, the current selection is cleared and a new selection is created. As long as the local drag start position is constant this is seamless, but if you scroll the text input view, the start of the drag is now at a different position relative to the text layout, and the start of the selection range changes. Solution: Don't queue the `MoveToPoint` edit. --- crates/bevy_ui_widgets/src/text_input.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/bevy_ui_widgets/src/text_input.rs b/crates/bevy_ui_widgets/src/text_input.rs index fd7a2e6df181c..b65f7bc511bf1 100644 --- a/crates/bevy_ui_widgets/src/text_input.rs +++ b/crates/bevy_ui_widgets/src/text_input.rs @@ -15,7 +15,7 @@ use bevy_input_focus::{ FocusCause, FocusGained, FocusLost, FocusedInput, InputFocus, InputFocusSystems, }; use bevy_math::Vec2; -use bevy_picking::events::{Click, Drag, Pointer, Press, Release}; +use bevy_picking::events::{Click, Drag, DragStart, Pointer, Press, Release}; use bevy_picking::pointer::PointerButton; use bevy_text::{EditableText, PreeditCursor, TextEdit}; use bevy_ui::widget::{scroll_editable_text, update_editable_text_layout, TextScroll}; @@ -285,23 +285,18 @@ fn on_pointer_drag( return; } - let Some((drag_start_local_pos, current_local_pos)) = transform.try_inverse().map(|inverse| { - let transform_pos = |pointer_pos| { - inverse.transform_point2(pointer_pos * target.scale_factor() / ui_scale.0) - - node.content_box().min - + text_scroll.0 - }; - let current_pos = drag.pointer_location.position; - let drag_start_pos = current_pos - drag.distance; - (transform_pos(drag_start_pos), transform_pos(current_pos)) + let Some(current_local_pos) = transform.try_inverse().map(|inverse| { + inverse + .transform_point2(drag.pointer_location.position * target.scale_factor() / ui_scale.0) + - node.content_box().min + + text_scroll.0 }) else { return; }; - editable_text.pending_edits.extend([ - TextEdit::MoveToPoint(drag_start_local_pos), - TextEdit::ExtendSelectionToPoint(current_local_pos), - ]); + editable_text + .pending_edits + .push(TextEdit::ExtendSelectionToPoint(current_local_pos)); drag.propagate(false); } From 2154aad65ed791699a76c63cff4efd6515796396 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 14:16:11 +0100 Subject: [PATCH 03/12] Removed unused import. --- crates/bevy_ui_widgets/src/text_input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_widgets/src/text_input.rs b/crates/bevy_ui_widgets/src/text_input.rs index b65f7bc511bf1..0174191b29074 100644 --- a/crates/bevy_ui_widgets/src/text_input.rs +++ b/crates/bevy_ui_widgets/src/text_input.rs @@ -15,7 +15,7 @@ use bevy_input_focus::{ FocusCause, FocusGained, FocusLost, FocusedInput, InputFocus, InputFocusSystems, }; use bevy_math::Vec2; -use bevy_picking::events::{Click, Drag, DragStart, Pointer, Press, Release}; +use bevy_picking::events::{Click, Drag, Pointer, Press, Release}; use bevy_picking::pointer::PointerButton; use bevy_text::{EditableText, PreeditCursor, TextEdit}; use bevy_ui::widget::{scroll_editable_text, update_editable_text_layout, TextScroll}; From a633abde6690325ef19f98254d69fc344eb95f8a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 18:09:25 +0100 Subject: [PATCH 04/12] Added `NeedsScroll` component tracking if scrolling needs to be recalculated --- crates/bevy_text/src/editing.rs | 20 +++++++-- crates/bevy_text/src/text_edit.rs | 43 +++++++++++++++++++ .../bevy_ui/src/widget/text_input_layout.rs | 12 ++++-- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/crates/bevy_text/src/editing.rs b/crates/bevy_text/src/editing.rs index 8927b10cf3b57..9979a9e492ed9 100644 --- a/crates/bevy_text/src/editing.rs +++ b/crates/bevy_text/src/editing.rs @@ -102,7 +102,8 @@ use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; TextColor, LineHeight, FontHinting, - EditableTextGeneration + EditableTextGeneration, + NeedsScroll )] pub struct EditableText { /// A [`parley::PlainEditor`], tracking both the text content and cursor position. @@ -215,6 +216,7 @@ impl EditableText { layout_context: &mut LayoutContext, clipboard: &mut bevy_clipboard::Clipboard, char_filter: impl Fn(char) -> bool, + needs_scroll: &mut bool, ) { let Self { editor, @@ -251,7 +253,13 @@ impl EditableText { return; } } - other => other.apply(&mut driver, clipboard, *max_characters, &char_filter), + other => other.apply( + &mut driver, + clipboard, + *max_characters, + &char_filter, + needs_scroll, + ), } } } @@ -301,13 +309,14 @@ pub fn apply_text_edits( &mut EditableText, Option<&EditableTextFilter>, &EditableTextGeneration, + &mut NeedsScroll, )>, mut font_context: ResMut, mut layout_context: ResMut, mut clipboard: ResMut, mut commands: Commands, ) { - for (entity, mut editable_text, filter, generation) in query.iter_mut() { + for (entity, mut editable_text, filter, generation, mut needs_scroll) in query.iter_mut() { // `pending_paste` can hold a cross-frame paste even when no new edits are queued, // so check for either before doing work. if !editable_text.pending_edits.is_empty() || editable_text.pending_paste.is_some() { @@ -319,6 +328,7 @@ pub fn apply_text_edits( Some(EditableTextFilter(Some(filter))) => filter.as_ref(), _ => &|_| true, }, + &mut needs_scroll.0, ); } @@ -335,3 +345,7 @@ pub fn apply_text_edits( pub struct TextEditChange { entity: Entity, } + +/// If true, scrolling needs to be updated. +#[derive(Component, Copy, Clone, Debug, PartialEq, Default)] +pub struct NeedsScroll(pub bool); diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index 9e9f5defa0e7e..a06f47a21c557 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -1,4 +1,7 @@ use bevy_clipboard::ClipboardRead; +use bevy_derive::Deref; +use bevy_derive::DerefMut; +use bevy_ecs::component::Component; use bevy_math::Vec2; use bevy_reflect::Reflect; use parley::PlainEditorDriver; @@ -221,7 +224,10 @@ impl TextEdit { clipboard: &mut bevy_clipboard::Clipboard, max_characters: Option, char_filter: impl Fn(char) -> bool, + needs_scroll: &mut bool, ) { + *needs_scroll = *needs_scroll || self.needs_scroll(); + match self { TextEdit::Copy => { if let Some(text) = driver.editor.selected_text() @@ -315,6 +321,43 @@ impl TextEdit { } } } + + /// True if the text editor view should scroll after the given edit. + pub fn needs_scroll(&self) -> bool { + match self { + TextEdit::Copy => false, + TextEdit::Cut => true, + TextEdit::Paste => true, + TextEdit::Insert(_) => true, + TextEdit::Backspace => true, + TextEdit::BackspaceWord => true, + TextEdit::Delete => true, + TextEdit::DeleteWord => true, + TextEdit::Left(_) => true, + TextEdit::Right(_) => true, + TextEdit::WordLeft(_) => true, + TextEdit::WordRight(_) => true, + TextEdit::Up(_) => true, + TextEdit::Down(_) => true, + TextEdit::TextStart(_) => true, + TextEdit::TextEnd(_) => true, + TextEdit::HardLineStart(_) => true, + TextEdit::HardLineEnd(_) => true, + TextEdit::LineStart(_) => true, + TextEdit::LineEnd(_) => true, + TextEdit::CollapseSelection => true, + TextEdit::SelectAll => false, + TextEdit::SelectAllIfCollapsed => false, + TextEdit::MoveToPoint(_) => true, + TextEdit::SelectWordAtPoint(_) => false, + TextEdit::SelectLineAtPoint(_) => false, + TextEdit::SelectedHardLineAtPoint(_) => false, + TextEdit::ExtendSelectionToPoint(_) => true, + TextEdit::ShiftClickExtension(_) => false, + TextEdit::ImeSetCompose { .. } => true, + TextEdit::ImeCommit { .. } => true, + } + } } /// Reason an [`insert_filtered`] call was rejected. diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index a3c73cdba7733..6e3a6ee44e836 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -18,8 +18,8 @@ use bevy_platform::hash::FixedHasher; use bevy_text::{ add_glyph_to_atlas, get_glyph_atlas_info, resolve_font_source, EditableText, EditableTextGeneration, Font, FontAtlasKey, FontAtlasSet, FontCx, FontHinting, FontSize, - GlyphCacheKey, LayoutCx, LineBreak, LineHeight, PositionedGlyph, RemSize, RunGeometry, ScaleCx, - TextBrush, TextFont, TextLayout, TextLayoutInfo, + GlyphCacheKey, LayoutCx, LineBreak, LineHeight, NeedsScroll, PositionedGlyph, RemSize, + RunGeometry, ScaleCx, TextBrush, TextFont, TextLayout, TextLayoutInfo, }; use bevy_time::{Real, Time}; use parley::{BoundingBox, PositionedLayoutItem, StyleProperty}; @@ -498,6 +498,7 @@ pub fn scroll_editable_text( &mut TextScroll, &ComputedNode, &TextLayoutInfo, + &mut NeedsScroll, )>, ) { let current_focus = input_focus @@ -505,10 +506,13 @@ pub fn scroll_editable_text( .and_then(|input_focus| input_focus.get()); let focus_changed = *previous_focus != current_focus; - for (entity, editable_text, generation, mut scroll, node, info) in query.iter_mut() { + for (entity, editable_text, generation, mut scroll, node, info, mut needs_scroll) in + query.iter_mut() + { if !(editable_text.is_changed() || generation.is_changed() || focus_changed && (Some(entity) == *previous_focus || Some(entity) == current_focus)) + || !needs_scroll.0 { continue; } @@ -564,8 +568,8 @@ pub fn scroll_editable_text( ) .floor(), })); + needs_scroll.0 = false; } - *previous_focus = current_focus; } From 3b9f89389a0ca01104805f542f1323b393450fd4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 18:10:58 +0100 Subject: [PATCH 05/12] Don't scroll on `ExtendSelectionToPoint` --- crates/bevy_text/src/text_edit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index a06f47a21c557..353f9bdc41ff8 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -352,7 +352,7 @@ impl TextEdit { TextEdit::SelectWordAtPoint(_) => false, TextEdit::SelectLineAtPoint(_) => false, TextEdit::SelectedHardLineAtPoint(_) => false, - TextEdit::ExtendSelectionToPoint(_) => true, + TextEdit::ExtendSelectionToPoint(_) => false, TextEdit::ShiftClickExtension(_) => false, TextEdit::ImeSetCompose { .. } => true, TextEdit::ImeCommit { .. } => true, From 32819139dbd6d167099b6e541feb2002fea8670b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 18:22:09 +0100 Subject: [PATCH 06/12] Set needs scroll if pointer dragged outside of editor. --- crates/bevy_text/src/editing.rs | 6 +++--- crates/bevy_ui/src/widget/text_input_layout.rs | 8 ++++---- crates/bevy_ui_widgets/src/text_input.rs | 11 +++++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/crates/bevy_text/src/editing.rs b/crates/bevy_text/src/editing.rs index 9979a9e492ed9..d160e8298fdc0 100644 --- a/crates/bevy_text/src/editing.rs +++ b/crates/bevy_text/src/editing.rs @@ -103,7 +103,7 @@ use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; LineHeight, FontHinting, EditableTextGeneration, - NeedsScroll + EditableTextNeedsScroll )] pub struct EditableText { /// A [`parley::PlainEditor`], tracking both the text content and cursor position. @@ -309,7 +309,7 @@ pub fn apply_text_edits( &mut EditableText, Option<&EditableTextFilter>, &EditableTextGeneration, - &mut NeedsScroll, + &mut EditableTextNeedsScroll, )>, mut font_context: ResMut, mut layout_context: ResMut, @@ -348,4 +348,4 @@ pub struct TextEditChange { /// If true, scrolling needs to be updated. #[derive(Component, Copy, Clone, Debug, PartialEq, Default)] -pub struct NeedsScroll(pub bool); +pub struct EditableTextNeedsScroll(pub bool); diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 6e3a6ee44e836..a058877c1dcfa 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -17,9 +17,9 @@ use bevy_math::{Rect, Vec2}; use bevy_platform::hash::FixedHasher; use bevy_text::{ add_glyph_to_atlas, get_glyph_atlas_info, resolve_font_source, EditableText, - EditableTextGeneration, Font, FontAtlasKey, FontAtlasSet, FontCx, FontHinting, FontSize, - GlyphCacheKey, LayoutCx, LineBreak, LineHeight, NeedsScroll, PositionedGlyph, RemSize, - RunGeometry, ScaleCx, TextBrush, TextFont, TextLayout, TextLayoutInfo, + EditableTextGeneration, EditableTextNeedsScroll, Font, FontAtlasKey, FontAtlasSet, FontCx, + FontHinting, FontSize, GlyphCacheKey, LayoutCx, LineBreak, LineHeight, PositionedGlyph, + RemSize, RunGeometry, ScaleCx, TextBrush, TextFont, TextLayout, TextLayoutInfo, }; use bevy_time::{Real, Time}; use parley::{BoundingBox, PositionedLayoutItem, StyleProperty}; @@ -498,7 +498,7 @@ pub fn scroll_editable_text( &mut TextScroll, &ComputedNode, &TextLayoutInfo, - &mut NeedsScroll, + &mut EditableTextNeedsScroll, )>, ) { let current_focus = input_focus diff --git a/crates/bevy_ui_widgets/src/text_input.rs b/crates/bevy_ui_widgets/src/text_input.rs index 0174191b29074..220736e8d2266 100644 --- a/crates/bevy_ui_widgets/src/text_input.rs +++ b/crates/bevy_ui_widgets/src/text_input.rs @@ -17,7 +17,7 @@ use bevy_input_focus::{ use bevy_math::Vec2; use bevy_picking::events::{Click, Drag, Pointer, Press, Release}; use bevy_picking::pointer::PointerButton; -use bevy_text::{EditableText, PreeditCursor, TextEdit}; +use bevy_text::{EditableText, EditableTextNeedsScroll, PreeditCursor, TextEdit}; use bevy_ui::widget::{scroll_editable_text, update_editable_text_layout, TextScroll}; use bevy_ui::UiSystems; use bevy_ui::{ @@ -267,6 +267,7 @@ fn on_pointer_drag( &ComputedUiRenderTargetInfo, &UiGlobalTransform, &TextScroll, + &mut EditableTextNeedsScroll, )>, ui_scale: Res, ) { @@ -274,7 +275,7 @@ fn on_pointer_drag( return; } - let Ok((mut editable_text, node, target, transform, text_scroll)) = + let Ok((mut editable_text, node, target, transform, text_scroll, mut needs_scroll)) = text_input_query.get_mut(drag.entity) else { return; @@ -285,7 +286,7 @@ fn on_pointer_drag( return; } - let Some(current_local_pos) = transform.try_inverse().map(|inverse| { + let Some(local_pos) = transform.try_inverse().map(|inverse| { inverse .transform_point2(drag.pointer_location.position * target.scale_factor() / ui_scale.0) - node.content_box().min @@ -294,9 +295,11 @@ fn on_pointer_drag( return; }; + needs_scroll.0 = needs_scroll.0 || node.content_box().contains(local_pos); + editable_text .pending_edits - .push(TextEdit::ExtendSelectionToPoint(current_local_pos)); + .push(TextEdit::ExtendSelectionToPoint(local_pos)); drag.propagate(false); } From f4bdad73e4760bc85d86ce0fca448c72dcb3c7cb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 18:41:31 +0100 Subject: [PATCH 07/12] Fixed content box drag check --- crates/bevy_text/src/text_edit.rs | 62 ++++++++++++------------ crates/bevy_ui_widgets/src/text_input.rs | 12 ++--- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index 353f9bdc41ff8..26fe2ea914b98 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -325,37 +325,37 @@ impl TextEdit { /// True if the text editor view should scroll after the given edit. pub fn needs_scroll(&self) -> bool { match self { - TextEdit::Copy => false, - TextEdit::Cut => true, - TextEdit::Paste => true, - TextEdit::Insert(_) => true, - TextEdit::Backspace => true, - TextEdit::BackspaceWord => true, - TextEdit::Delete => true, - TextEdit::DeleteWord => true, - TextEdit::Left(_) => true, - TextEdit::Right(_) => true, - TextEdit::WordLeft(_) => true, - TextEdit::WordRight(_) => true, - TextEdit::Up(_) => true, - TextEdit::Down(_) => true, - TextEdit::TextStart(_) => true, - TextEdit::TextEnd(_) => true, - TextEdit::HardLineStart(_) => true, - TextEdit::HardLineEnd(_) => true, - TextEdit::LineStart(_) => true, - TextEdit::LineEnd(_) => true, - TextEdit::CollapseSelection => true, - TextEdit::SelectAll => false, - TextEdit::SelectAllIfCollapsed => false, - TextEdit::MoveToPoint(_) => true, - TextEdit::SelectWordAtPoint(_) => false, - TextEdit::SelectLineAtPoint(_) => false, - TextEdit::SelectedHardLineAtPoint(_) => false, - TextEdit::ExtendSelectionToPoint(_) => false, - TextEdit::ShiftClickExtension(_) => false, - TextEdit::ImeSetCompose { .. } => true, - TextEdit::ImeCommit { .. } => true, + TextEdit::Copy + | TextEdit::SelectAllIfCollapsed + | TextEdit::SelectAll + | TextEdit::SelectWordAtPoint(_) + | TextEdit::SelectLineAtPoint(_) + | TextEdit::SelectedHardLineAtPoint(_) + | TextEdit::ExtendSelectionToPoint(_) + | TextEdit::ShiftClickExtension(_) => false, + TextEdit::Cut + | TextEdit::Paste + | TextEdit::Insert(_) + | TextEdit::Backspace + | TextEdit::BackspaceWord + | TextEdit::Delete + | TextEdit::DeleteWord + | TextEdit::Left(_) + | TextEdit::Right(_) + | TextEdit::WordLeft(_) + | TextEdit::WordRight(_) + | TextEdit::Up(_) + | TextEdit::Down(_) + | TextEdit::TextStart(_) + | TextEdit::TextEnd(_) + | TextEdit::HardLineStart(_) + | TextEdit::HardLineEnd(_) + | TextEdit::LineStart(_) + | TextEdit::LineEnd(_) + | TextEdit::CollapseSelection + | TextEdit::MoveToPoint(_) + | TextEdit::ImeSetCompose { .. } + | TextEdit::ImeCommit { .. } => true, } } } diff --git a/crates/bevy_ui_widgets/src/text_input.rs b/crates/bevy_ui_widgets/src/text_input.rs index 220736e8d2266..73d56f7b29721 100644 --- a/crates/bevy_ui_widgets/src/text_input.rs +++ b/crates/bevy_ui_widgets/src/text_input.rs @@ -286,20 +286,20 @@ fn on_pointer_drag( return; } - let Some(local_pos) = transform.try_inverse().map(|inverse| { + let Some(pointer_pos) = transform.try_inverse().map(|inverse| { inverse .transform_point2(drag.pointer_location.position * target.scale_factor() / ui_scale.0) - - node.content_box().min - + text_scroll.0 }) else { return; }; - needs_scroll.0 = needs_scroll.0 || node.content_box().contains(local_pos); - + needs_scroll.0 = needs_scroll.0 || !node.content_box().contains(pointer_pos); + println!("drag needs scroll = {}", needs_scroll.0); editable_text .pending_edits - .push(TextEdit::ExtendSelectionToPoint(local_pos)); + .push(TextEdit::ExtendSelectionToPoint( + pointer_pos - node.content_box().min + text_scroll.0, + )); drag.propagate(false); } From bf0ada7c1ccc6850741561a43022a1199b89d96b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 18:47:00 +0100 Subject: [PATCH 08/12] MoveToPoint needs scroll --- crates/bevy_text/src/text_edit.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index 26fe2ea914b98..609140df9a574 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -331,6 +331,7 @@ impl TextEdit { | TextEdit::SelectWordAtPoint(_) | TextEdit::SelectLineAtPoint(_) | TextEdit::SelectedHardLineAtPoint(_) + | TextEdit::MoveToPoint(_) | TextEdit::ExtendSelectionToPoint(_) | TextEdit::ShiftClickExtension(_) => false, TextEdit::Cut @@ -353,7 +354,6 @@ impl TextEdit { | TextEdit::LineStart(_) | TextEdit::LineEnd(_) | TextEdit::CollapseSelection - | TextEdit::MoveToPoint(_) | TextEdit::ImeSetCompose { .. } | TextEdit::ImeCommit { .. } => true, } From 0ae93a77e6dc995110bdeaca7929921cde44f0b1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 19:11:21 +0100 Subject: [PATCH 09/12] clean up --- crates/bevy_text/src/text_edit.rs | 3 --- crates/bevy_ui_widgets/src/text_input.rs | 1 - 2 files changed, 4 deletions(-) diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index 609140df9a574..a91432a0ce553 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -1,7 +1,4 @@ use bevy_clipboard::ClipboardRead; -use bevy_derive::Deref; -use bevy_derive::DerefMut; -use bevy_ecs::component::Component; use bevy_math::Vec2; use bevy_reflect::Reflect; use parley::PlainEditorDriver; diff --git a/crates/bevy_ui_widgets/src/text_input.rs b/crates/bevy_ui_widgets/src/text_input.rs index 73d56f7b29721..a16352e7beb03 100644 --- a/crates/bevy_ui_widgets/src/text_input.rs +++ b/crates/bevy_ui_widgets/src/text_input.rs @@ -294,7 +294,6 @@ fn on_pointer_drag( }; needs_scroll.0 = needs_scroll.0 || !node.content_box().contains(pointer_pos); - println!("drag needs scroll = {}", needs_scroll.0); editable_text .pending_edits .push(TextEdit::ExtendSelectionToPoint( From a01c10540f2b3fef5d0dbbad7508c734d1dcd4b1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 19:29:17 +0100 Subject: [PATCH 10/12] Fixed texts by denormalizing inset values. --- crates/bevy_ui/src/widget/text_input_layout.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index a058877c1dcfa..a2ced3cf46b0d 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -641,11 +641,11 @@ mod test { #[test] fn test_scroll_axis_with_inset() { assert_eq!( - scroll_axis_with_inset(0.25, 0., 100., 0., 100., 0., 50.), + scroll_axis_with_inset(25., 0., 100., 0., 100., 0., 50.), 0. ); assert_eq!( - scroll_axis_with_inset(0.25, 0., 0., 0., 100., 50., 100.), + scroll_axis_with_inset(25., 0., 0., 0., 100., 50., 100.), 0. ); } @@ -653,7 +653,7 @@ mod test { #[test] fn test_scroll_axis_with_inset_moves_to_inner_min() { assert_eq!( - scroll_axis_with_inset(0.25, 0., 100., 50., 150., 60., 65.), + scroll_axis_with_inset(25., 0., 100., 50., 150., 60., 65.), 35. ); } @@ -661,7 +661,7 @@ mod test { #[test] fn test_scroll_axis_with_inset_moves_to_inner_max() { assert_eq!( - scroll_axis_with_inset(0.25, 0., 100., 0., 100., 90., 95.), + scroll_axis_with_inset(25., 0., 100., 0., 100., 90., 95.), 20. ); } @@ -669,11 +669,11 @@ mod test { #[test] fn test_scroll_axis_with_inset_saturates() { assert_eq!( - scroll_axis_with_inset(0.25, 0., 100., 10., 110., 10., 20.), + scroll_axis_with_inset(25., 0., 100., 10., 110., 10., 20.), 0. ); assert_eq!( - scroll_axis_with_inset(0.25, 0., 100., 80., 180., 175., 180.), + scroll_axis_with_inset(25., 0., 100., 80., 180., 175., 180.), 100. ); } @@ -681,11 +681,11 @@ mod test { #[test] fn test_scroll_axis_with_inset_uses_full_view_when_target_larger_than_inner() { assert_eq!( - scroll_axis_with_inset(0.25, 0., 100., 0., 100., 20., 90.), + scroll_axis_with_inset(25., 0., 100., 0., 100., 20., 90.), 0. ); assert_eq!( - scroll_axis_with_inset(0.25, 0., 100., 0., 100., 80., 150.), + scroll_axis_with_inset(25., 0., 100., 0., 100., 80., 150.), 50. ); } From c7e58588946447db1f07e334f78d0d4778c8d55f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 19:33:52 +0100 Subject: [PATCH 11/12] cargo fmt --all --- crates/bevy_ui/src/widget/text_input_layout.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index a2ced3cf46b0d..80d9b9c020fef 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -640,14 +640,8 @@ mod test { #[test] fn test_scroll_axis_with_inset() { - assert_eq!( - scroll_axis_with_inset(25., 0., 100., 0., 100., 0., 50.), - 0. - ); - assert_eq!( - scroll_axis_with_inset(25., 0., 0., 0., 100., 50., 100.), - 0. - ); + assert_eq!(scroll_axis_with_inset(25., 0., 100., 0., 100., 0., 50.), 0.); + assert_eq!(scroll_axis_with_inset(25., 0., 0., 0., 100., 50., 100.), 0.); } #[test] From 104d930ca7ebddab09214c17dcd024c107b2ac9e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 12 May 2026 21:44:38 +0100 Subject: [PATCH 12/12] renamed variables for clarity --- crates/bevy_ui/src/widget/text_input_layout.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/widget/text_input_layout.rs b/crates/bevy_ui/src/widget/text_input_layout.rs index 80d9b9c020fef..cd4a89c7552ca 100644 --- a/crates/bevy_ui/src/widget/text_input_layout.rs +++ b/crates/bevy_ui/src/widget/text_input_layout.rs @@ -612,15 +612,15 @@ fn scroll_axis_with_inset( t_max: f32, ) -> f32 { let v_size = v_max - v_min; - let u_min = v_min + inset; - let u_max = v_max - inset; + let inner_min = v_min + inset; + let inner_max = v_max - inset; let t_size = t_max - t_min; let new_v_min = if v_size - 2. * inset < t_size { scroll_axis(v_min, v_max, t_min, t_max) - } else if t_min < u_min { + } else if t_min < inner_min { t_min - inset - } else if u_max < t_max { + } else if inner_max < t_max { t_max - v_size + inset } else { v_min