Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c66bacf
Added editable_text scene to UI testbed
ickshonpe Apr 10, 2026
c3e9453
Update styles before layout in update_editable_text_styles
ickshonpe Apr 10, 2026
3adc6b1
Cleanup, apply `TextEdits` after updating editor styles
ickshonpe Apr 10, 2026
f91d3fb
Removed unused
ickshonpe Apr 10, 2026
5b6c959
Add change detection to update_editable_text_styles
ickshonpe Apr 10, 2026
fc116fa
Added change detection logic
ickshonpe Apr 10, 2026
58c0598
Fixed rendering
ickshonpe Apr 10, 2026
6ff7393
removed is empty check
ickshonpe Apr 10, 2026
37042e6
Fixed `FontSize` change detection
ickshonpe Apr 10, 2026
4f4d834
clean up
ickshonpe Apr 10, 2026
fb5fb62
Removed local var
ickshonpe Apr 10, 2026
d8e0f71
Renamed editable_text_system to update_editable_text_layout
ickshonpe Apr 10, 2026
e44d1a5
Merge branch 'main' into editable-text-fixes
ickshonpe Apr 13, 2026
e889d96
Don't get the PlainEditorDriver or reset the cursor position in the c…
ickshonpe Apr 13, 2026
026a848
Added a field to `EditableText` to track the generation of its `TextL…
ickshonpe Apr 13, 2026
0946986
Track the editor generation in a separate component `EditableTextGene…
ickshonpe Apr 13, 2026
2cb92ed
Removed the `text_edited` flag, use editor generation to track changes.
ickshonpe Apr 13, 2026
0be9bc8
Removed unused imports
ickshonpe Apr 13, 2026
b832f5c
Removed unused fields
ickshonpe Apr 13, 2026
5160710
Removed more unused imports
ickshonpe Apr 13, 2026
02b337c
added font size and visible lines fields to `multiline_text_input` ex…
ickshonpe Apr 13, 2026
20e5ded
Add autofocus to example
ickshonpe Apr 13, 2026
514eee5
Set cursor colours
ickshonpe Apr 13, 2026
851b8d5
update text input alignment on changes to TextLayout
ickshonpe Apr 13, 2026
526c8ac
improve example
ickshonpe Apr 13, 2026
1853b09
use on.original_event_target() in the example's observers
ickshonpe Apr 13, 2026
e531775
Update text scroll on generation changes.
ickshonpe Apr 13, 2026
e650159
removed empty line
ickshonpe Apr 13, 2026
948b71b
fixed spelling mistake
ickshonpe Apr 13, 2026
2721e93
fixed scheduling conflicts
ickshonpe Apr 13, 2026
4d45f71
Merge branch 'main' into editable-text-fixes
ickshonpe Apr 14, 2026
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
60 changes: 34 additions & 26 deletions crates/bevy_text/src/text_editable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ use crate::{
TextLayout,
};
use alloc::sync::Arc;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::prelude::*;
use core::time::Duration;
use parley::{FontContext, LayoutContext, PlainEditor, SplitString};
Expand All @@ -96,7 +97,14 @@ pub struct Clipboard(pub String);
/// and provides methods for applying text edits and cursor movements correctly
/// according to Unicode rules.
#[derive(Component, Clone)]
#[require(TextLayout, TextFont, TextColor, LineHeight, FontHinting)]
#[require(
TextLayout,
TextFont,
TextColor,
LineHeight,
FontHinting,
EditableTextGeneration
)]
pub struct EditableText {
/// A [`parley::PlainEditor`], tracking both the text content and cursor position.
///
Expand All @@ -117,8 +125,6 @@ pub struct EditableText {
pub cursor_width: f32,
/// Cursor blink period in seconds.
pub cursor_blink_period: Duration,
/// True if a `TextEdit` was applied this frame
pub text_edited: bool,
/// Maximum number of characters the text input can contain.
///
/// Edits which would cause the length to exceed the maximum are ignored.
Expand All @@ -138,7 +144,6 @@ impl Default for EditableText {
pending_edits: Vec::new(),
cursor_width: 0.2,
cursor_blink_period: Duration::from_secs(1),
text_edited: false,
max_characters: None,
visible_lines: Some(1.),
allow_newlines: false,
Expand Down Expand Up @@ -203,19 +208,19 @@ impl EditableText {
}
}

/// Clears the current input and resets the cursor position.
pub fn clear(
&mut self,
font_context: &mut FontContext,
layout_context: &mut LayoutContext<TextBrush>,
) {
/// Clears the input's text buffer and any pending edits.
pub fn clear(&mut self) {
self.editor.set_text("");
let mut driver = self.editor_mut().driver(font_context, layout_context);
driver.move_to_byte(0);
self.pending_edits.clear();
}
}

/// Wrapper around a `parley::Generation`. Used to track when `TextLayoutInfo` is stale and needs reupdating.
/// The initial `Generation` of the `PlainEditor` is not equal to the default `Generation` value, so the
/// `TextLayoutInfo` will always be given an initial update.
#[derive(Component, PartialEq, Eq, Default, Clone, Copy, Deref, DerefMut)]
pub struct EditableTextGeneration(parley::Generation);

/// Sets a per-character filter for this text input. Insert and paste edits are ignored if the filter rejects any character.
///
/// The filter does not apply to characters already within the `EditableText`'s text buffer.
Expand All @@ -231,26 +236,29 @@ impl EditableTextFilter {

/// Applies pending text edit actions to all [`EditableText`] widgets.
pub fn apply_text_edits(
mut query: Query<(Entity, &mut EditableText, Option<&EditableTextFilter>)>,
mut query: Query<(
Entity,
&mut EditableText,
Option<&EditableTextFilter>,
&EditableTextGeneration,
)>,
mut font_context: ResMut<FontCx>,
mut layout_context: ResMut<LayoutCx>,
mut clipboard_text: ResMut<Clipboard>,
mut commands: Commands,
) {
for (entity, mut editable_text, filter) in query.iter_mut() {
editable_text.text_edited = !editable_text.pending_edits.is_empty();

if editable_text.text_edited {
editable_text.apply_pending_edits(
&mut font_context.0,
&mut layout_context.0,
&mut clipboard_text.0,
match filter {
Some(EditableTextFilter(Some(filter))) => filter.as_ref(),
_ => &|_| true,
},
);
for (entity, mut editable_text, filter, generation) in query.iter_mut() {
editable_text.apply_pending_edits(
&mut font_context.0,
&mut layout_context.0,
&mut clipboard_text.0,
match filter {
Some(EditableTextFilter(Some(filter))) => filter.as_ref(),
_ => &|_| true,
},
);

if **generation != editable_text.editor.generation() {
commands.trigger(TextEditChange { entity });
}
}
Expand Down
28 changes: 16 additions & 12 deletions crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,31 +239,35 @@ fn build_text_interop(app: &mut App) {
.ambiguous_with(bevy_sprite::update_text2d_layout)
// We assume Text is on disjoint UI entities to ImageNode and UiTextureAtlasImage
// FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481.
.ambiguous_with(widget::update_image_content_size_system),
.ambiguous_with(widget::update_image_content_size_system)
.ambiguous_with(EditableTextSystems),
widget::text_system
.in_set(UiSystems::PostLayout)
.after(bevy_text::load_font_assets_into_font_collection)
.before(bevy_asset::AssetEventSystems)
// Text2d and bevy_ui text are entirely on separate entities
.ambiguous_with(bevy_sprite::update_text2d_layout)
.ambiguous_with(bevy_sprite::calculate_bounds_text2d),
widget::update_editable_text_content_size
(
widget::update_editable_text_content_size,
widget::update_editable_text_styles,
)
.chain()
.in_set(UiSystems::Content)
.after(bevy_text::load_font_assets_into_font_collection)
.before(EditableTextSystems)
.ambiguous_with(widget::update_image_content_size_system)
.ambiguous_with(widget::measure_text_system),
(widget::editable_text_system, widget::scroll_editable_text)
(
widget::update_editable_text_layout,
widget::scroll_editable_text,
)
.chain()
.in_set(UiSystems::PostLayout)
// This is unlikely to result in real conflicts,
// as FocusChangeEvents only mutates internal state of InputFocus,
// and editable_text_system only reads from it.
// However, in case this changes in the future, this is a safer choice,
// as editable_text_system or related systems could generate focus changes
// which should be processed ASAP.
.before(bevy_input_focus::InputFocusSystems::FocusChangeEvents)
.ambiguous_with(ui_stack_system)
.ambiguous_with(widget::text_system)
.ambiguous_with(bevy_sprite::calculate_bounds_text2d),
.ambiguous_with(bevy_sprite::calculate_bounds_text2d)
.ambiguous_with(bevy_sprite::update_text2d_layout),
),
);

Expand All @@ -287,5 +291,5 @@ fn build_text_interop(app: &mut App) {
);

// We cannot set this up in bevy_text as this would create a circular dependency between bevy_ui and bevy_text
app.configure_sets(PostUpdate, EditableTextSystems.in_set(UiSystems::Prepare));
app.configure_sets(PostUpdate, EditableTextSystems.in_set(UiSystems::Content));
}
Loading