Skip to content
Open
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
24 changes: 21 additions & 3 deletions crates/bevy_text/src/editing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand All @@ -101,7 +102,8 @@ use parley::{FontContext, LayoutContext, PlainEditor, SplitString};
TextColor,
LineHeight,
FontHinting,
EditableTextGeneration
EditableTextGeneration,
EditableTextNeedsScroll
)]
pub struct EditableText {
/// A [`parley::PlainEditor`], tracking both the text content and cursor position.
Expand Down Expand Up @@ -144,6 +146,8 @@ pub struct EditableText {
pub visible_width: Option<f32>,
/// 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 {
Expand All @@ -159,6 +163,7 @@ impl Default for EditableText {
visible_lines: Some(1.),
visible_width: None,
allow_newlines: false,
scroll_inset: Vec2::new(0.1, 0.25),
}
}
}
Expand Down Expand Up @@ -211,6 +216,7 @@ impl EditableText {
layout_context: &mut LayoutContext<TextBrush>,
clipboard: &mut bevy_clipboard::Clipboard,
char_filter: impl Fn(char) -> bool,
needs_scroll: &mut bool,
) {
let Self {
editor,
Expand Down Expand Up @@ -247,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,
),
}
}
}
Expand Down Expand Up @@ -297,13 +309,14 @@ pub fn apply_text_edits(
&mut EditableText,
Option<&EditableTextFilter>,
&EditableTextGeneration,
&mut EditableTextNeedsScroll,
)>,
mut font_context: ResMut<FontCx>,
mut layout_context: ResMut<LayoutCx>,
mut clipboard: ResMut<bevy_clipboard::Clipboard>,
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() {
Expand All @@ -315,6 +328,7 @@ pub fn apply_text_edits(
Some(EditableTextFilter(Some(filter))) => filter.as_ref(),
_ => &|_| true,
},
&mut needs_scroll.0,
);
}

Expand All @@ -331,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 EditableTextNeedsScroll(pub bool);
40 changes: 40 additions & 0 deletions crates/bevy_text/src/text_edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,10 @@ impl TextEdit {
clipboard: &mut bevy_clipboard::Clipboard,
max_characters: Option<usize>,
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()
Expand Down Expand Up @@ -315,6 +318,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
| TextEdit::SelectAllIfCollapsed
| TextEdit::SelectAll
| TextEdit::SelectWordAtPoint(_)
| TextEdit::SelectLineAtPoint(_)
| TextEdit::SelectedHardLineAtPoint(_)
| TextEdit::MoveToPoint(_)
| 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::ImeSetCompose { .. }
| TextEdit::ImeCommit { .. } => true,
}
}
}

/// Reason an [`insert_filtered`] call was rejected.
Expand Down
115 changes: 107 additions & 8 deletions crates/bevy_ui/src/widget/text_input_layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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};
Expand Down Expand Up @@ -498,17 +498,21 @@ pub fn scroll_editable_text(
&mut TextScroll,
&ComputedNode,
&TextLayoutInfo,
&mut EditableTextNeedsScroll,
)>,
) {
let current_focus = input_focus
.as_ref()
.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;
}
Expand Down Expand Up @@ -540,20 +544,32 @@ 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(),
}));
needs_scroll.0 = false;
}

*previous_focus = current_focus;
}

Expand Down Expand Up @@ -585,3 +601,86 @@ 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 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 < inner_min {
t_min - inset
} else if inner_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(25., 0., 100., 0., 100., 0., 50.), 0.);
assert_eq!(scroll_axis_with_inset(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(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(25., 0., 100., 0., 100., 90., 95.),
20.
);
}

#[test]
fn test_scroll_axis_with_inset_saturates() {
assert_eq!(
scroll_axis_with_inset(25., 0., 100., 10., 110., 10., 20.),
0.
);
assert_eq!(
scroll_axis_with_inset(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(25., 0., 100., 0., 100., 20., 90.),
0.
);
assert_eq!(
scroll_axis_with_inset(25., 0., 100., 0., 100., 80., 150.),
50.
);
}
}
27 changes: 12 additions & 15 deletions crates/bevy_ui_widgets/src/text_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -267,14 +267,15 @@ fn on_pointer_drag(
&ComputedUiRenderTargetInfo,
&UiGlobalTransform,
&TextScroll,
&mut EditableTextNeedsScroll,
)>,
ui_scale: Res<UiScale>,
) {
if drag.button != PointerButton::Primary {
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;
Expand All @@ -285,23 +286,19 @@ 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(pointer_pos) = transform.try_inverse().map(|inverse| {
inverse
.transform_point2(drag.pointer_location.position * target.scale_factor() / ui_scale.0)
}) else {
return;
};

editable_text.pending_edits.extend([
TextEdit::MoveToPoint(drag_start_local_pos),
TextEdit::ExtendSelectionToPoint(current_local_pos),
]);
needs_scroll.0 = needs_scroll.0 || !node.content_box().contains(pointer_pos);
editable_text
.pending_edits
.push(TextEdit::ExtendSelectionToPoint(
pointer_pos - node.content_box().min + text_scroll.0,
));

drag.propagate(false);
}
Expand Down
Loading