From 2edb3b27a3657081b7419f89fed1345e441b06e6 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Wed, 11 Feb 2026 14:14:53 -0800 Subject: [PATCH 01/40] Initial port to parley --- Cargo.toml | 13 ++ crates/bevy_text/src/editable_text.rs | 247 ++++++++++++++++++++ crates/bevy_text/src/lib.rs | 9 +- crates/bevy_ui/src/lib.rs | 5 +- crates/bevy_ui_widgets/Cargo.toml | 3 + crates/bevy_ui_widgets/src/editable_text.rs | 112 +++++++++ crates/bevy_ui_widgets/src/lib.rs | 2 + deny.toml | 2 + examples/ui/text/editable_text.rs | 90 +++++++ release-content/release-notes/text_input.md | 25 ++ 10 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_text/src/editable_text.rs create mode 100644 crates/bevy_ui_widgets/src/editable_text.rs create mode 100644 examples/ui/text/editable_text.rs create mode 100644 release-content/release-notes/text_input.md diff --git a/Cargo.toml b/Cargo.toml index 4dc83dcc41e12..e84b3e01bea06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1009,6 +1009,19 @@ description = "Renders text to multiple windows with different scale factors usi category = "2D Rendering" wasm = true +[[example]] +name = "editable_text" +path = "examples/ui/text/editable_text.rs" +# Causes an ICE on docs.rs +doc-scrape-examples = false +required-features = ["experimental_bevy_ui_widgets"] + +[package.metadata.example.editable_text] +name = "Editable Text" +description = "Demonstrates a simple, unstyled text input widget" +category = "UI (User Interface)" +wasm = true + [[example]] name = "texture_atlas" path = "examples/2d/texture_atlas.rs" diff --git a/crates/bevy_text/src/editable_text.rs b/crates/bevy_text/src/editable_text.rs new file mode 100644 index 0000000000000..80d6b3a533816 --- /dev/null +++ b/crates/bevy_text/src/editable_text.rs @@ -0,0 +1,247 @@ +//! A simple text input widget for Bevy UI. +//! +//! The [`EditableText`] widget is an undecorated rectangular text input field, +//! which allows users to input and edit text within a Bevy UI application. +//! Every [`EditableText`] component is also a [`Node`] in the Bevy UI hierarchy, +//! allowing you to position and size it using standard Bevy UI layout techniques. +//! You can think of it as the editable equivalent of [`Text`](bevy_ui::prelude::Text), +//! and components such as [`TextFont`] and [`TextColor`] can be used to style it. +//! +//! [`EditableText`] supports the following functionality: +//! +//! - Text entry +//! - Basic keyboard-driven cursor movement (arrow keys, home/end keys) +//! - Backspace and delete operations +//! +//! You might use this widget as the basis for text input fields in forms, chat boxes, for naming characters, +//! or any other scenario where you want to extract an unformatted text string from the user. +//! +//! Reusable widgets that build on top of this basic text input field (as might be found in Bevy's Feathers UI framework), +//! will typically combine this widget with additional UI elements such as borders, backgrounds, and labels, +//! creating a multi-entity widget that matches the semantics and visual appearance required by the application. +//! +//! ## Handling user input +//! +//! User input is handled via a plugin in `bevy_ui_widgets`: +//! [`bevy_text`](crate) is not aware of input events directly. +//! +//! With the correct plugin enabled, when an [`EditableText`] entity is focused, +//! keyboard input events are captured and processed into [`TextEdit`] actions. +//! +//! ## Limitations +//! +//! The formatting of the text is uniform throughout the entire input field. +//! As a result, rich text-editing is out-of-scope: +//! this widget is not intended to form the basis for a full-featured text editor. +//! +//! Similarly, this widget is "headless": it has no built-in styling, and is intended to be used +//! with a themed UI framework of your choice (e.g. Feathers). This means that no text boxes, borders, or other +//! visual elements are provided by default, and must be added separately using Bevy UI entities / components, +//! and any reactive styling (e.g., focus/hover states) must also be implemented separately. +//! +//! However, the following features are planned but currently not implemented: +//! +//! - Home / End key support for moving the cursor to the start / end of the text +//! - Placeholder text (displayed when the input is empty) +//! - Click to place cursor +//! - Cursor blinking +//! - Clipboard operations (copy, cut, paste) +//! - Text selection +//! - Undo/redo functionality +//! - Newline support for multi-line input +//! - Input Method Editor (IME) support for complex scripts +//! - Text validation (e.g., email format, numeric input, max length) +//! - Password-style character masking +//! - Soft-wrapping of long lines +//! - Vertical scrolling for multi-line input +//! - Horizontal scrolling for long lines +//! - Mobile pop-up keyboard support +//! - Overwrite mode (typically toggled by the `Insert` key) +//! - Bidirectional text support (e.g., mixing left-to-right and right-to-left scripts) +//! - AccessKit integration for screen readers and other assistive technologies +//! - World-space text input +//! - Text input labels (used for accessibility, tooltips or form descriptions) +//! - Input consumption (preventing other systems from receiving keyboard input events when the text input is focused) +//! - Text form submission handling +//! +//! If you require any of these features, please consider contributing it to the crate, +//! one feature at a time! +//! +//! # Usage +//! +//! To use this widget, ensure that the [`EditableTextPlugin`] has been added to your Bevy app. +//! Then, you can add a [`EditableText`] component to any UI node. +// Note: this logic is in `bevy_text`, rather than higher up in `bevy_ui` or `bevy_ui_widgets`, +// because doing so allows us to process `EditableText` in the various systems provided by `bevy_text` +// and `bevy_ui`, such as text layout and font management. + +use std::collections::VecDeque; + +use crate::{ + FontCx, FontHinting, FontSmoothing, LayoutCx, LineHeight, TextColor, TextFont, TextLayout, +}; +use bevy_ecs::prelude::*; +use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; +use smol_str::SmolStr; + +/// A plain-text text input field. +/// +/// Please see the [`editable_text` module](crate::editable_text) for more details on usage and functionality. +/// +/// Note that text editing operations are trickier than they might first appear, +/// due to the complexities of Unicode text handling. +/// +/// As a result, we store an internal [`PlainEditor`] instance, +/// which manages both the text content and the cursor position, +/// and provides methods for applying text edits and cursor movements correctly +/// according to Unicode rules. +#[derive(Component)] +#[require(TextLayout, TextFont, TextColor, LineHeight, FontHinting)] +pub struct EditableText { + /// A [parley::PlainEditor], tracking both the text content and cursor position. + /// + /// This serves as an analogue to [`ComputedTextBlock`](crate::ComputedTextBlock) for editable text. + /// + /// In most cases, you should queue text edits via the [`EditableText::queue_edit`] method instead of directly manipulating the editor, + /// and then allow the [`apply_text_edits`] system to apply the edits at the appropriate time in the update cycle. + /// + /// Note that many more complex editing operations require working with [`PlainEditor::driver`]. + /// These operations should generally be batched together to avoid redundant layout work. + // The B: Brush generic here must match the brush used by `ComputedTextBlock` to ensure that the font system is compatible. + pub editor: PlainEditor<(u32, FontSmoothing)>, + /// Text edit actions that have been requested but not yet applied. + /// + /// These edits are processed in first-in, first-out order. + pub pending_edits: VecDeque, + /// Does the contained text buffer need rerendering / relayout? + /// + /// Analogous to [`ComputedTextBlock::needs_rerender`](crate::ComputedTextBlock::needs_rerender). + pub(crate) needs_rerender: bool, + /// Does the text use `rem` sizes that depend on the base font size? + pub(crate) uses_rem_sizes: bool, + /// Does the text use `vw` / `vh` sizes that depend on the viewport size? + pub(crate) uses_viewport_sizes: bool, +} + +impl Default for EditableText { + fn default() -> Self { + Self { + // Defaults selected to match `Text::default()` + editor: PlainEditor::new(20.), + pending_edits: VecDeque::new(), + needs_rerender: true, + uses_rem_sizes: false, + uses_viewport_sizes: false, + } + } +} + +impl EditableText { + /// Access the internal [`PlainEditor`]. + pub fn editor(&self) -> &PlainEditor<(u32, FontSmoothing)> { + &self.editor + } + + /// Mutably access the internal [`PlainEditor`]. + /// + pub fn editor_mut(&mut self) -> &mut PlainEditor<(u32, FontSmoothing)> { + &mut self.editor + } + + /// Get the current text input as a [`SplitString`]. + /// + /// A [`SplitString`] can be converted into a [`String`] using [`SplitString::to_string`] if needed. + pub fn value(&self) -> SplitString<'_> { + self.editor.text() + } + + /// Queue a [`TextEdit`] action to be applied later by the [`apply_text_edits`] system. + pub fn queue_edit(&mut self, edit: TextEdit) { + self.pending_edits.push_back(edit); + } + + /// Applies all [`TextEdit`]s in `pending_edits` immediately, updating the [`PlainEditor`] text / cursor state accordingly. + /// + /// [`FontContext`] should be gathered from the [`FontCx`] resource, and [`LayoutContext`] should be gathered from the [`LayoutCx`] resource. + pub fn apply_pending_edits( + &mut self, + font_context: &mut FontContext, + layout_context: &mut LayoutContext<(u32, FontSmoothing)>, + ) { + // Take the `pending_edits` out of the struct so we can apply them without mutable aliasing issues. + // We do not need to put the `pending_edits` back into the struct, + // as all edits should be consumed by this method, leaving `pending_edits` empty at the end. + let mut pending_edits = std::mem::take(&mut self.pending_edits); + let mut driver = self.editor_mut().driver(font_context, layout_context); + + while let Some(edit) = pending_edits.pop_front() { + match edit { + TextEdit::Insert(str) => driver.insert_or_replace_selection(&str), + TextEdit::Backspace => driver.backdelete(), + TextEdit::Delete => driver.delete(), + TextEdit::MoveCursorRight => driver.move_right(), + TextEdit::MoveCursorLeft => driver.move_left(), + } + } + } + + /// Sets the entire text input to the given string, replacing any existing content. + pub fn set_input(&mut self, text: &str) { + self.editor.set_text(text); + self.needs_rerender = true; + } + + /// Clears the current input and resets the cursor position. + pub fn clear(&mut self) { + self.editor.set_text(""); + self.needs_rerender = true; + } +} + +/// Deferred text input edit and navigation actions applied by the `apply_text_edits` system. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TextEdit { + /// Insert a character at the cursor. If there is a selection, replaces the selection with the character instead. + /// + /// Typically generated in response to keyboard text input events. + /// + /// This is intended to insert a single Unicode grapheme cluster, such as a letter, digit, punctuation mark, or emoji. + /// Ordinarily, this is derived from [`KeyboardInput::logical_key`](bevy_input::keyboard::KeyboardInput::logical_key), + /// which stores a [`SmolStr`] inside of the [`Key::Character`] variant, which may represent multiple bytes. + Insert(SmolStr), + /// Delete the character behind the cursor. + /// If there is a selection, deletes the selection instead. + /// + /// Typically generated in response to the [`Backspace`](Key::Backspace) key. + /// + /// This operation removes an entire Unicode grapheme cluster, which may consist of multiple bytes, + /// shifting the cursor position accordingly. + Backspace, + /// Delete the character at the cursor. + /// If there is a selection, deletes the selection instead. + /// + /// Typically generated in response to the [`Delete`](Key::Delete) key. + /// + /// This operation removes an entire Unicode grapheme cluster, which may consist of multiple bytes, + /// shifting the cursor position accordingly. + Delete, + /// Moves the cursor by one position to the right. + /// + /// Typically generated in response to the [`Right`](Key::Right) key. + MoveCursorRight, + /// Moves the cursor by one position to the left. + /// + /// Typically generated in response to the [`Left`](Key::Left) key. + MoveCursorLeft, +} + +/// Applies pending text edit actions to all [`EditableText`] widgets. +pub fn apply_text_edits( + mut query: Query<&mut EditableText>, + mut font_context: ResMut, + mut layout_context: ResMut, +) { + for mut editable_text in query.iter_mut() { + editable_text.apply_pending_edits(&mut font_context.0, &mut layout_context.0); + } +} diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index a7d73659bda1a..fbc33e4a3ce34 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -32,6 +32,7 @@ extern crate alloc; mod bounds; +mod editable_text; mod error; mod font; mod font_atlas; @@ -44,6 +45,7 @@ mod text; mod text_access; pub use bounds::*; +pub use editable_text::*; pub use error::*; pub use font::*; pub use font_atlas::*; @@ -86,6 +88,10 @@ pub struct TextPlugin; #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub struct Text2dUpdateSystems; +/// System set where [`EditableText::pending_edits`] are applied. +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +pub struct EditableTextSystems; + impl Plugin for TextPlugin { fn build(&self, app: &mut App) { app.init_asset::() @@ -105,7 +111,8 @@ impl Plugin for TextPlugin { ) .chain(), ) - .add_systems(Last, trim_source_cache); + .add_systems(Last, trim_source_cache) + .add_systems(PostUpdate, apply_text_edits.in_set(EditableTextSystems)); #[cfg(feature = "default_font")] { diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index d2e1e46589baf..2cfd074801452 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -35,7 +35,7 @@ mod layout; mod stack; mod ui_node; -use bevy_text::detect_text_needs_rerender; +use bevy_text::{detect_text_needs_rerender, EditableTextSystems}; pub use focus::*; pub use geometry::*; pub use gradients::*; @@ -265,4 +265,7 @@ fn build_text_interop(app: &mut App) { PostUpdate, AmbiguousWithUpdateText2dLayout.ambiguous_with(bevy_sprite::update_text2d_layout), ); + + // 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)); } diff --git a/crates/bevy_ui_widgets/Cargo.toml b/crates/bevy_ui_widgets/Cargo.toml index 136019c6693cf..ea534d81820c3 100644 --- a/crates/bevy_ui_widgets/Cargo.toml +++ b/crates/bevy_ui_widgets/Cargo.toml @@ -21,9 +21,12 @@ bevy_math = { path = "../bevy_math", version = "0.19.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.19.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.19.0-dev" } bevy_ui = { path = "../bevy_ui", version = "0.19.0-dev" } +bevy_text = { path = "../bevy_text", version = "0.19.0-dev" } # other accesskit = "0.24" +parley = "0.7" # Must match bevy_text's parley version +smol_str = "0.2" # Must match winit's smol_str version [features] default = [] diff --git a/crates/bevy_ui_widgets/src/editable_text.rs b/crates/bevy_ui_widgets/src/editable_text.rs new file mode 100644 index 0000000000000..57afc71d2746d --- /dev/null +++ b/crates/bevy_ui_widgets/src/editable_text.rs @@ -0,0 +1,112 @@ +//! Input handling for [`EditableText`] widgets. +//! +//! This module provides systems to process keyboard input events and apply text edits +//! to focused [`EditableText`] widgets. +//! +//! Only entities that are focused via the [`InputFocus`] resource will receive keyboard input events. +//! +//! Note that this module is distinct from the core `bevy_text` crate to avoid pulling in +//! [`bevy_input`] to that crate, which is intended to be usable in non-interactive contexts. + +use bevy_app::{App, Plugin, PreUpdate}; +use bevy_ecs::prelude::*; +use bevy_input::keyboard::{Key, KeyboardInput}; +use bevy_input::InputSystems; +use bevy_input_focus::{InputFocus, InputFocusSystems}; +use bevy_text::{EditableText, TextEdit}; +use bevy_ui::{widget::TextNodeFlags, ContentSize, Node}; + +/// System that processes keyboard input events into text edit actions for focused [`EditableText`] widgets. +/// +/// See [`EditableText`] for more details on the standard mapping from keyboard events to text edit actions +/// used by this system. +/// +/// Note that this does not immediately apply the edits; they are queued up in [`EditableText::pending_edits`], +/// and then applied later by the [`apply_text_edits`] system. +pub fn process_text_inputs( + focus: Res, + mut query: Query<&mut EditableText>, + mut keyboard_input: MessageReader, +) { + // Check if any EditableText is focused + let focused_entity = if let Some(entity) = focus.get() { + entity + } else { + return; // No focused entity, nothing to do + }; + + let mut editable_text = if let Ok(editable_text) = query.get_mut(focused_entity) { + editable_text + } else { + return; // Focused entity is not an EditableText, nothing to do + }; + + for keyboard_event in keyboard_input.read() { + match keyboard_event { + KeyboardInput { + logical_key: Key::Character(c), + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::Insert(c.clone())); + } + KeyboardInput { + logical_key: Key::Backspace, + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::Backspace); + } + KeyboardInput { + logical_key: Key::Delete, + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::Delete); + } + KeyboardInput { + logical_key: Key::ArrowRight, + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::MoveCursorRight); + } + KeyboardInput { + logical_key: Key::ArrowLeft, + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::MoveCursorLeft); + } + _ => {} + } + } +} + +/// Enables support for the [`EditableText`] widget. +/// +/// Contains the systems and observers necessary to update widget state and handle user input. +/// +/// This plugin is included in the [`UiWidgetsPlugins`](crate::UiWidgetsPlugins) group, but can also be added individually +/// if only editable text input is needed. +/// +/// Note that [`TextEdit`]s are applied during [`PostUpdate`](bevy_app::PostUpdate) +/// in the [`EditableTextSystems`](bevy_text::EditableTextSystems) system set. +pub struct EditableTextInputPlugin; + +impl Plugin for EditableTextInputPlugin { + fn build(&self, app: &mut App) { + app.add_systems( + PreUpdate, + process_text_inputs + .after(InputFocusSystems::Dispatch) + .after(InputSystems), + ); + + // These components cannot be registered in `bevy_text` where `EditableText` is defined, + // because that would create a circular dependency between `bevy_text` and `bevy_ui`. + app.register_required_components::() + .register_required_components::() + .register_required_components::(); + } +} diff --git a/crates/bevy_ui_widgets/src/lib.rs b/crates/bevy_ui_widgets/src/lib.rs index 8f30e2842701a..3f42039607c68 100644 --- a/crates/bevy_ui_widgets/src/lib.rs +++ b/crates/bevy_ui_widgets/src/lib.rs @@ -20,6 +20,7 @@ mod button; mod checkbox; +mod editable_text; mod menu; mod observe; pub mod popover; @@ -29,6 +30,7 @@ mod slider; pub use button::*; pub use checkbox::*; +pub use editable_text::*; pub use menu::*; pub use observe::*; pub use radio::*; diff --git a/deny.toml b/deny.toml index 426ec33d8aa52..aa04dd8f8dcea 100644 --- a/deny.toml +++ b/deny.toml @@ -80,7 +80,9 @@ wildcards = "deny" # Certain crates that we don't want multiple versions of in the dependency tree deny = [ { name = "ahash", deny-multiple-versions = true }, + { name = "accesskit", deny-multiple-versions = true }, { name = "android-activity", deny-multiple-versions = true }, + { name = "parley", deny-multiple-versions = true }, { name = "glam", deny-multiple-versions = true }, { name = "raw-window-handle", deny-multiple-versions = true }, ] diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs new file mode 100644 index 0000000000000..8d8c1a0b71dfc --- /dev/null +++ b/examples/ui/text/editable_text.rs @@ -0,0 +1,90 @@ +//! Demonstrates a simple, unstyled [`EditableText`] widget. +//! +//! [`EditableText`] is a basic primitive for text input in Bevy UI. +//! In most cases, this should be combined with other entities to create a compound widget +//! that includes e.g. a background, border, and text label. +//! +//! See the module documentation for [`editable_text`](bevy::ui_widgets::editable_text) for more details. +use bevy::input_focus::{InputDispatchPlugin, InputFocus}; +use bevy::prelude::*; +use bevy::text::EditableText; +use bevy::ui_widgets::EditableTextInputPlugin; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(( + // This is also part of UiWidgetsPlugins, but we only need EditableText for this example + EditableTextInputPlugin, + // Input focus is required to direct keyboard input to the correct EditableText + InputDispatchPlugin, + )) + .add_systems(Startup, setup) + .add_systems(Update, text_submission) + .run(); +} +fn setup( + mut commands: Commands, + asset_server: Res, + mut input_focus: ResMut, +) { + // Set up a camera + // We need a camera to see the UI + commands.spawn(Camera2d::default()); + + // Create a root UI node, so we can place the input above the output in a column + // TODO: center things nicely + let root = commands.spawn(Node { ..default() }).id(); + + // Set up an EditableText widget + let text_input = commands + .spawn(( + EditableText::default(), + TextFont { + font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), + font_size: FontSize::Px(70.0), + ..default() + }, + )) + .id(); + + // Set the focus to our text input so we can start typing right away + input_focus.set(text_input); + + // Set up a text output to see the result of our text input + let text_output = commands + .spawn(( + Text::new("testing"), + TextFont { + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font_size: FontSize::Px(70.0), + ..default() + }, + )) + .id(); + + // Assemble our hierarchy + commands + .entity(root) + .add_children(&[text_input, text_output]); +} + +// Submit the text when Ctrl+Enter is pressed +fn text_submission( + input_focus: Res, + keyboard_input: Res>, + mut text_input: Query<&mut EditableText>, + mut text_output: Single<&mut Text>, +) { + if keyboard_input.just_pressed(KeyCode::Enter) + && (keyboard_input.pressed(KeyCode::ControlLeft) + || keyboard_input.pressed(KeyCode::ControlRight)) + { + if let Some(focused_entity) = input_focus.get() { + if let Some(mut text_input) = text_input.get_mut(focused_entity).ok() { + text_output.0 = text_input.value().clone().to_string(); + text_input.clear(); + } + } + } +} diff --git a/release-content/release-notes/text_input.md b/release-content/release-notes/text_input.md new file mode 100644 index 0000000000000..00cd277ff924c --- /dev/null +++ b/release-content/release-notes/text_input.md @@ -0,0 +1,25 @@ +--- +title: "Text input" +authors: ["@alice-i-cecile", "@ickshonpe", "@Zeophlite"] +pull_requests: [TODO] +--- + +Entering text into an application is a common task, even for games. +Player names, search bars and chat all rely on the ability to enter and submit plain text. + +In Bevy 0.19, we've added basic support for text entry, in the form of the `EditableText` widget. +Spawning an entity with this component will create a simple unstyled rectangle of editable text. +Our initial text entry supports: + +- Press keys on your keyboard, get text (wow!) +- A cursor that can be moved forward and back in response to the left and right arrow keys +- Backspace and Delete +- Unicode-aware navigation and editing: 1 byte/char != 1 character +- Bidirectional text support, allowing both left-to-right and right-to-left scripts + +`EditableText` integrate with Bevy's `InputFocus` resource, accepting keyboard inputs only when the selected +`EditableText` entity is focused. + +Many important features are currently unimplemented (placeholder text, text selection, clipboard support, undo-redo...). +While we've been careful to expose and document the internals so that you can readily implement these features in your own projects, +we would like to continue to expand the functionality of the base widget: please consider making a PR! \ No newline at end of file From 2a0db900984cbbe236c2f09ac77ebdf11b4ac096 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 25 Feb 2026 12:05:15 +0000 Subject: [PATCH 02/40] Added `TextCursorStyle` component. --- crates/bevy_text/src/editing.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 crates/bevy_text/src/editing.rs diff --git a/crates/bevy_text/src/editing.rs b/crates/bevy_text/src/editing.rs new file mode 100644 index 0000000000000..4f9cfe0d6f6ba --- /dev/null +++ b/crates/bevy_text/src/editing.rs @@ -0,0 +1,13 @@ +use bevy_color::Color; +use bevy_ecs::component::Component; + +/// Text Cursor style +#[derive(Component, Clone, Copy, Debug, PartialEq)] +pub struct TextCursorStyle { + /// Color of the cursor + pub color: Color, + /// Background color of selected text + pub selection_color: Color, + /// Color of text under selection + pub selected_text_color: Option, +} From 3853701b7fc9b7b68f33fb57af17b5a281679fde Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 25 Feb 2026 12:05:53 +0000 Subject: [PATCH 03/40] Added cursor and selection rects to TextLayoutInfo --- crates/bevy_text/src/pipeline.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 7f9bdf8ca01d1..26ec04e6c2e39 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -418,6 +418,10 @@ pub struct TextLayoutInfo { pub run_geometry: Vec, /// The glyphs resulting size pub size: Vec2, + /// Cursor size and position for editing + pub cursor: Rect, + /// Selection rects + pub selection_rects: Vec, } impl TextLayoutInfo { From ec076396de8e44711f281b09c4b27d502236d3de Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 25 Feb 2026 12:07:18 +0000 Subject: [PATCH 04/40] Added cursor and selection rects rendering --- crates/bevy_ui_render/src/lib.rs | 103 +++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 41b3d6d0e0109..2455306f9c2d9 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -25,6 +25,7 @@ use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_shader::load_shader_library; use bevy_sprite_render::SpriteAssetEvents; +use bevy_text::editing::TextCursorStyle; use bevy_ui::widget::{ImageNode, TextShadow, ViewportNode}; use bevy_ui::{ BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedUiTargetCamera, Display, @@ -109,7 +110,9 @@ pub mod stack_z_offsets { pub const BORDER_GRADIENT: f32 = 0.03; pub const IMAGE: f32 = 0.04; pub const MATERIAL: f32 = 0.05; + pub const TEXT_SELECTION: f32 = 0.55; pub const TEXT: f32 = 0.06; + pub const TEXT_CURSOR: f32 = 0.065; pub const TEXT_STRIKETHROUGH: f32 = 0.07; } @@ -1276,6 +1279,106 @@ pub fn extract_text_decorations( } } +pub fn extract_text_cursor( + mut commands: Commands, + mut extracted_uinodes: ResMut, + text_node_query: Extract< + Query<( + Entity, + &ComputedNode, + &UiGlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedUiTargetCamera, + &TextLayoutInfo, + &TextCursorStyle, + )>, + >, + camera_map: Extract, +) { + let mut camera_mapper = camera_map.get_mapper(); + + for ( + entity, + uinode, + global_transform, + inherited_visibility, + maybe_clip, + target_camera, + text_layout_info, + cursor_style, + ) in text_node_query.iter() + { + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + if !inherited_visibility.get() || uinode.is_empty() { + continue; + } + + let Some(extracted_camera_entity) = camera_mapper.map(target_camera) else { + continue; + }; + + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); + + if !text_layout_info.selection_rects.is_empty() + && !cursor_style.selection_color.is_fully_transparent() + { + let selection_color = cursor_style.selection_color.to_linear(); + + for selection in text_layout_info.selection_rects.iter() { + extracted_uinodes.uinodes.push(ExtractedUiNode { + render_entity: commands.spawn(TemporaryRenderEntity).id(), + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT_SELECTION, + clip: maybe_clip.map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + transform: transform * Affine2::from_translation(selection.center()), + item: ExtractedUiItem::Node { + color: selection_color, + rect: Rect { + min: Vec2::ZERO, + max: selection.size(), + }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::default(), + border_radius: ResolvedBorderRadius::default(), + node_type: NodeType::Rect, + }, + main_entity: entity.into(), + }); + } + } + + if !text_layout_info.cursor.is_empty() && !cursor_style.color.is_fully_transparent() { + extracted_uinodes.uinodes.push(ExtractedUiNode { + render_entity: commands.spawn(TemporaryRenderEntity).id(), + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT_CURSOR, + clip: maybe_clip.map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + transform: transform * Affine2::from_translation(text_layout_info.cursor.center()), + item: ExtractedUiItem::Node { + color: cursor_style.color.to_linear(), + rect: Rect { + min: Vec2::ZERO, + max: text_layout_info.cursor.size(), + }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::default(), + border_radius: ResolvedBorderRadius::default(), + node_type: NodeType::Rect, + }, + main_entity: entity.into(), + }); + } + } +} + #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct UiVertex { From 0057f73fcd32bfc145b0d69ad1c162edd99158f5 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 00:01:09 +0800 Subject: [PATCH 05/40] Move cursor rendering to own file --- crates/bevy_ui_render/src/cursor.rs | 100 ++++++++++++++++++++++++++++ crates/bevy_ui_render/src/lib.rs | 100 ---------------------------- 2 files changed, 100 insertions(+), 100 deletions(-) create mode 100644 crates/bevy_ui_render/src/cursor.rs diff --git a/crates/bevy_ui_render/src/cursor.rs b/crates/bevy_ui_render/src/cursor.rs new file mode 100644 index 0000000000000..1aa107f290900 --- /dev/null +++ b/crates/bevy_ui_render/src/cursor.rs @@ -0,0 +1,100 @@ + +pub fn extract_text_cursor( + mut commands: Commands, + mut extracted_uinodes: ResMut, + text_node_query: Extract< + Query<( + Entity, + &ComputedNode, + &UiGlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedUiTargetCamera, + &TextLayoutInfo, + &TextCursorStyle, + )>, + >, + camera_map: Extract, +) { + let mut camera_mapper = camera_map.get_mapper(); + + for ( + entity, + uinode, + global_transform, + inherited_visibility, + maybe_clip, + target_camera, + text_layout_info, + cursor_style, + ) in text_node_query.iter() + { + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + if !inherited_visibility.get() || uinode.is_empty() { + continue; + } + + let Some(extracted_camera_entity) = camera_mapper.map(target_camera) else { + continue; + }; + + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); + + if !text_layout_info.selection_rects.is_empty() + && !cursor_style.selection_color.is_fully_transparent() + { + let selection_color = cursor_style.selection_color.to_linear(); + + for selection in text_layout_info.selection_rects.iter() { + extracted_uinodes.uinodes.push(ExtractedUiNode { + render_entity: commands.spawn(TemporaryRenderEntity).id(), + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT_SELECTION, + clip: maybe_clip.map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + transform: transform * Affine2::from_translation(selection.center()), + item: ExtractedUiItem::Node { + color: selection_color, + rect: Rect { + min: Vec2::ZERO, + max: selection.size(), + }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::default(), + border_radius: ResolvedBorderRadius::default(), + node_type: NodeType::Rect, + }, + main_entity: entity.into(), + }); + } + } + + if !text_layout_info.cursor.is_empty() && !cursor_style.color.is_fully_transparent() { + extracted_uinodes.uinodes.push(ExtractedUiNode { + render_entity: commands.spawn(TemporaryRenderEntity).id(), + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT_CURSOR, + clip: maybe_clip.map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + transform: transform * Affine2::from_translation(text_layout_info.cursor.center()), + item: ExtractedUiItem::Node { + color: cursor_style.color.to_linear(), + rect: Rect { + min: Vec2::ZERO, + max: text_layout_info.cursor.size(), + }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::default(), + border_radius: ResolvedBorderRadius::default(), + node_type: NodeType::Rect, + }, + main_entity: entity.into(), + }); + } + } +} diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 2455306f9c2d9..104338d540a08 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1279,106 +1279,6 @@ pub fn extract_text_decorations( } } -pub fn extract_text_cursor( - mut commands: Commands, - mut extracted_uinodes: ResMut, - text_node_query: Extract< - Query<( - Entity, - &ComputedNode, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &TextLayoutInfo, - &TextCursorStyle, - )>, - >, - camera_map: Extract, -) { - let mut camera_mapper = camera_map.get_mapper(); - - for ( - entity, - uinode, - global_transform, - inherited_visibility, - maybe_clip, - target_camera, - text_layout_info, - cursor_style, - ) in text_node_query.iter() - { - // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) - if !inherited_visibility.get() || uinode.is_empty() { - continue; - } - - let Some(extracted_camera_entity) = camera_mapper.map(target_camera) else { - continue; - }; - - let transform = - Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); - - if !text_layout_info.selection_rects.is_empty() - && !cursor_style.selection_color.is_fully_transparent() - { - let selection_color = cursor_style.selection_color.to_linear(); - - for selection in text_layout_info.selection_rects.iter() { - extracted_uinodes.uinodes.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity).id(), - z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT_SELECTION, - clip: maybe_clip.map(|clip| clip.clip), - image: AssetId::default(), - extracted_camera_entity, - transform: transform * Affine2::from_translation(selection.center()), - item: ExtractedUiItem::Node { - color: selection_color, - rect: Rect { - min: Vec2::ZERO, - max: selection.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius: ResolvedBorderRadius::default(), - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); - } - } - - if !text_layout_info.cursor.is_empty() && !cursor_style.color.is_fully_transparent() { - extracted_uinodes.uinodes.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity).id(), - z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT_CURSOR, - clip: maybe_clip.map(|clip| clip.clip), - image: AssetId::default(), - extracted_camera_entity, - transform: transform * Affine2::from_translation(text_layout_info.cursor.center()), - item: ExtractedUiItem::Node { - color: cursor_style.color.to_linear(), - rect: Rect { - min: Vec2::ZERO, - max: text_layout_info.cursor.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius: ResolvedBorderRadius::default(), - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); - } - } -} - #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct UiVertex { From 8b45b19c94a985238655ffd50e580641cd9de885 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 00:05:26 +0800 Subject: [PATCH 06/40] editable_text.rs -> text_editable.rs --- crates/bevy_text/src/{editable_text.rs => text_editable.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/bevy_text/src/{editable_text.rs => text_editable.rs} (100%) diff --git a/crates/bevy_text/src/editable_text.rs b/crates/bevy_text/src/text_editable.rs similarity index 100% rename from crates/bevy_text/src/editable_text.rs rename to crates/bevy_text/src/text_editable.rs From e7a44e57db4fe395836a35071d0f5c9874f9f422 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 00:06:53 +0800 Subject: [PATCH 07/40] Move TextEdit to own file --- crates/bevy_text/src/text_edit.rs | 37 +++++++++++++++++++++++++++ crates/bevy_text/src/text_editable.rs | 36 -------------------------- 2 files changed, 37 insertions(+), 36 deletions(-) create mode 100644 crates/bevy_text/src/text_edit.rs diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs new file mode 100644 index 0000000000000..3d5a1cd849498 --- /dev/null +++ b/crates/bevy_text/src/text_edit.rs @@ -0,0 +1,37 @@ + +/// Deferred text input edit and navigation actions applied by the `apply_text_edits` system. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TextEdit { + /// Insert a character at the cursor. If there is a selection, replaces the selection with the character instead. + /// + /// Typically generated in response to keyboard text input events. + /// + /// This is intended to insert a single Unicode grapheme cluster, such as a letter, digit, punctuation mark, or emoji. + /// Ordinarily, this is derived from [`KeyboardInput::logical_key`](bevy_input::keyboard::KeyboardInput::logical_key), + /// which stores a [`SmolStr`] inside of the [`Key::Character`] variant, which may represent multiple bytes. + Insert(SmolStr), + /// Delete the character behind the cursor. + /// If there is a selection, deletes the selection instead. + /// + /// Typically generated in response to the [`Backspace`](Key::Backspace) key. + /// + /// This operation removes an entire Unicode grapheme cluster, which may consist of multiple bytes, + /// shifting the cursor position accordingly. + Backspace, + /// Delete the character at the cursor. + /// If there is a selection, deletes the selection instead. + /// + /// Typically generated in response to the [`Delete`](Key::Delete) key. + /// + /// This operation removes an entire Unicode grapheme cluster, which may consist of multiple bytes, + /// shifting the cursor position accordingly. + Delete, + /// Moves the cursor by one position to the right. + /// + /// Typically generated in response to the [`Right`](Key::Right) key. + MoveCursorRight, + /// Moves the cursor by one position to the left. + /// + /// Typically generated in response to the [`Left`](Key::Left) key. + MoveCursorLeft, +} diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 80d6b3a533816..84c5103d277bc 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -198,42 +198,6 @@ impl EditableText { } } -/// Deferred text input edit and navigation actions applied by the `apply_text_edits` system. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TextEdit { - /// Insert a character at the cursor. If there is a selection, replaces the selection with the character instead. - /// - /// Typically generated in response to keyboard text input events. - /// - /// This is intended to insert a single Unicode grapheme cluster, such as a letter, digit, punctuation mark, or emoji. - /// Ordinarily, this is derived from [`KeyboardInput::logical_key`](bevy_input::keyboard::KeyboardInput::logical_key), - /// which stores a [`SmolStr`] inside of the [`Key::Character`] variant, which may represent multiple bytes. - Insert(SmolStr), - /// Delete the character behind the cursor. - /// If there is a selection, deletes the selection instead. - /// - /// Typically generated in response to the [`Backspace`](Key::Backspace) key. - /// - /// This operation removes an entire Unicode grapheme cluster, which may consist of multiple bytes, - /// shifting the cursor position accordingly. - Backspace, - /// Delete the character at the cursor. - /// If there is a selection, deletes the selection instead. - /// - /// Typically generated in response to the [`Delete`](Key::Delete) key. - /// - /// This operation removes an entire Unicode grapheme cluster, which may consist of multiple bytes, - /// shifting the cursor position accordingly. - Delete, - /// Moves the cursor by one position to the right. - /// - /// Typically generated in response to the [`Right`](Key::Right) key. - MoveCursorRight, - /// Moves the cursor by one position to the left. - /// - /// Typically generated in response to the [`Left`](Key::Left) key. - MoveCursorLeft, -} /// Applies pending text edit actions to all [`EditableText`] widgets. pub fn apply_text_edits( From ebc1d6139b7b3865ec3656d1a719559cbd6e971e Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 00:09:07 +0800 Subject: [PATCH 08/40] Move editing.rs to cursor.rs --- crates/bevy_text/src/{editing.rs => cursor.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/bevy_text/src/{editing.rs => cursor.rs} (100%) diff --git a/crates/bevy_text/src/editing.rs b/crates/bevy_text/src/cursor.rs similarity index 100% rename from crates/bevy_text/src/editing.rs rename to crates/bevy_text/src/cursor.rs From 04c411b218d5815eed01e74013ac47f2b2478117 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Mon, 9 Mar 2026 23:16:51 +0800 Subject: [PATCH 09/40] WIP Input Text --- Cargo.toml | 5 +- crates/bevy_sprite/src/text2d.rs | 9 +- crates/bevy_text/src/lib.rs | 9 +- crates/bevy_text/src/pipeline.rs | 33 ++++- crates/bevy_text/src/text_edit.rs | 19 +++ crates/bevy_text/src/text_editable.rs | 61 ++++++--- crates/bevy_ui/src/lib.rs | 6 +- crates/bevy_ui/src/widget/text.rs | 144 ++++++++++++++++++++- crates/bevy_ui_render/src/cursor.rs | 31 ++++- crates/bevy_ui_render/src/lib.rs | 12 +- examples/ui/text/editable_text.rs | 175 +++++++++++++++++++++++--- 11 files changed, 444 insertions(+), 60 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e84b3e01bea06..2ee5b38849f58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1012,9 +1012,8 @@ wasm = true [[example]] name = "editable_text" path = "examples/ui/text/editable_text.rs" -# Causes an ICE on docs.rs -doc-scrape-examples = false -required-features = ["experimental_bevy_ui_widgets"] +doc-scrape-examples = true +required-features = ["bevy_remote","bevy_ui_debug"] [package.metadata.example.editable_text] name = "Editable Text" diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index b2e06af9a3a75..2b3367f771307 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,9 +22,9 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, LineHeight, - RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, - TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, + ComputedTextBlock, EditableText, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, + LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, + TextLayoutInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use bevy_window::{PrimaryWindow, Window}; @@ -183,6 +183,7 @@ pub fn update_text2d_layout( Ref, &mut TextLayoutInfo, &mut ComputedTextBlock, + Option<&mut EditableText>, Ref, )>, mut text_reader: Text2dReader, @@ -225,6 +226,7 @@ pub fn update_text2d_layout( bounds, mut text_layout_info, mut computed, + mut maybe_editable_text, hinting, ) in &mut text_query { @@ -312,6 +314,7 @@ pub fn update_text2d_layout( &mut textures, &mut computed, &mut scale_cx, + &mut maybe_editable_text, text_bounds, block.justify, *hinting, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index fbc33e4a3ce34..3daa0841032c8 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -32,7 +32,7 @@ extern crate alloc; mod bounds; -mod editable_text; +mod cursor; mod error; mod font; mod font_atlas; @@ -43,9 +43,11 @@ mod parley_context; mod pipeline; mod text; mod text_access; +mod text_edit; +mod text_editable; pub use bounds::*; -pub use editable_text::*; +pub use cursor::*; pub use error::*; pub use font::*; pub use font_atlas::*; @@ -56,6 +58,8 @@ pub use parley_context::*; pub use pipeline::*; pub use text::*; pub use text_access::*; +pub use text_edit::*; +pub use text_editable::*; /// The text prelude. /// @@ -112,6 +116,7 @@ impl Plugin for TextPlugin { .chain(), ) .add_systems(Last, trim_source_cache) + .add_systems(PreUpdate, edit_to_computed) .add_systems(PostUpdate, apply_text_edits.in_set(EditableTextSystems)); #[cfg(feature = "default_font")] diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 26ec04e6c2e39..37edc3aa2b5fd 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,4 +1,5 @@ use alloc::borrow::Cow; +use bevy_ecs::world::Mut; use core::hash::BuildHasher; @@ -15,11 +16,12 @@ use bevy_platform::hash::FixedHasher; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use parley::style::{OverflowWrap, TextWrapMode}; use parley::{ - Alignment, AlignmentOptions, FontFamily, FontStack, Layout, PositionedLayoutItem, + Alignment, AlignmentOptions, BoundingBox, FontFamily, FontStack, Layout, PositionedLayoutItem, StyleProperty, WordBreakStrength, }; use swash::FontRef; +use crate::EditableText; use crate::{ add_glyph_to_atlas, error::TextError, @@ -267,6 +269,7 @@ impl TextPipeline { textures: &mut Assets, computed: &mut ComputedTextBlock, scale_cx: &mut ScaleCx, + maybe_editable_text: &mut Option>, bounds: TextBounds, justify: Justify, hinting: FontHinting, @@ -278,7 +281,7 @@ impl TextPipeline { layout_with_bounds(layout, bounds, justify); for (line_index, line) in layout.lines().enumerate() { - for item in line.items() { + for item in line.items().into_iter() { if let PositionedLayoutItem::GlyphRun(glyph_run) = item { let span_index = glyph_run.style().brush.0 as usize; let font_smoothing = glyph_run.style().brush.1; @@ -369,10 +372,32 @@ impl TextPipeline { } layout_info.size = Vec2::new(layout.full_width(), layout.height()).ceil(); + + if let Some(editable_text) = maybe_editable_text { + let geom = editable_text + .editor + .cursor_geometry(editable_text.cursor_width); + + layout_info.cursor = geom.map_or(None, |f| Some(bounding_box_to_rect(f))); + } + Ok(()) } } +fn bounding_box_to_rect(geom: BoundingBox) -> Rect { + Rect { + min: Vec2 { + x: geom.x0 as f32, + y: geom.y0 as f32, + }, + max: Vec2 { + x: geom.x1 as f32, + y: geom.y1 as f32, + }, + } +} + fn resolve_font_source<'a>( font: &'a FontSource, fonts: &'a Assets, @@ -419,7 +444,7 @@ pub struct TextLayoutInfo { /// The glyphs resulting size pub size: Vec2, /// Cursor size and position for editing - pub cursor: Rect, + pub cursor: Option, /// Selection rects pub selection_rects: Vec, } @@ -431,6 +456,8 @@ impl TextLayoutInfo { self.glyphs.clear(); self.run_geometry.clear(); self.size = Vec2::ZERO; + self.cursor = None; + self.selection_rects.clear(); } } diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index 3d5a1cd849498..d9a1e81ad096e 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -1,3 +1,7 @@ +use parley::PlainEditorDriver; +use smol_str::SmolStr; + +use crate::FontSmoothing; /// Deferred text input edit and navigation actions applied by the `apply_text_edits` system. #[derive(Debug, Clone, PartialEq, Eq)] @@ -35,3 +39,18 @@ pub enum TextEdit { /// Typically generated in response to the [`Left`](Key::Left) key. MoveCursorLeft, } + +/// Takes a `TextEdit` and applies to `PlainEditorDriver` +pub fn apply_edit<'a>( + edit: TextEdit, + mut driver: PlainEditorDriver<'a, (u32, FontSmoothing)>, +) -> PlainEditorDriver<'a, (u32, FontSmoothing)> { + match edit { + TextEdit::Insert(str) => driver.insert_or_replace_selection(&str), + TextEdit::Backspace => driver.backdelete(), + TextEdit::Delete => driver.delete(), + TextEdit::MoveCursorRight => driver.move_right(), + TextEdit::MoveCursorLeft => driver.move_left(), + } + return driver; +} diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 84c5103d277bc..f13e1b0b41bde 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -78,11 +78,11 @@ use std::collections::VecDeque; use crate::{ - FontCx, FontHinting, FontSmoothing, LayoutCx, LineHeight, TextColor, TextFont, TextLayout, + apply_edit, text_edit::TextEdit, ComputedTextBlock, FontCx, FontHinting, FontSmoothing, + LayoutCx, LineHeight, TextColor, TextFont, TextLayout, }; use bevy_ecs::prelude::*; use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; -use smol_str::SmolStr; /// A plain-text text input field. /// @@ -117,26 +117,30 @@ pub struct EditableText { /// /// Analogous to [`ComputedTextBlock::needs_rerender`](crate::ComputedTextBlock::needs_rerender). pub(crate) needs_rerender: bool, - /// Does the text use `rem` sizes that depend on the base font size? - pub(crate) uses_rem_sizes: bool, - /// Does the text use `vw` / `vh` sizes that depend on the viewport size? - pub(crate) uses_viewport_sizes: bool, + /// cursor width + pub cursor_width: f32, } impl Default for EditableText { fn default() -> Self { Self { // Defaults selected to match `Text::default()` - editor: PlainEditor::new(20.), + editor: PlainEditor::new(100.), // TODO: font size is 70, so how does this work? pending_edits: VecDeque::new(), needs_rerender: true, - uses_rem_sizes: false, - uses_viewport_sizes: false, + cursor_width: 20.0, } } } impl EditableText { + /// build with initial text + pub fn new(text: &str) -> Self { + let mut a = Self::default(); + a.set_input(text); + return a; + } + /// Access the internal [`PlainEditor`]. pub fn editor(&self) -> &PlainEditor<(u32, FontSmoothing)> { &self.editor @@ -175,30 +179,33 @@ impl EditableText { let mut driver = self.editor_mut().driver(font_context, layout_context); while let Some(edit) = pending_edits.pop_front() { - match edit { - TextEdit::Insert(str) => driver.insert_or_replace_selection(&str), - TextEdit::Backspace => driver.backdelete(), - TextEdit::Delete => driver.delete(), - TextEdit::MoveCursorRight => driver.move_right(), - TextEdit::MoveCursorLeft => driver.move_left(), - } + driver = apply_edit(edit, driver); } + self.needs_rerender = true; } /// Sets the entire text input to the given string, replacing any existing content. pub fn set_input(&mut self, text: &str) { self.editor.set_text(text); + // TODO: reset cursor location + self.pending_edits.clear(); self.needs_rerender = true; } /// Clears the current input and resets the cursor position. - pub fn clear(&mut self) { + pub fn clear( + &mut self, + font_context: &mut FontContext, + layout_context: &mut LayoutContext<(u32, FontSmoothing)>, + ) { self.editor.set_text(""); + let mut driver = self.editor_mut().driver(font_context, layout_context); + driver.move_to_byte(0); + self.pending_edits.clear(); self.needs_rerender = true; } } - /// Applies pending text edit actions to all [`EditableText`] widgets. pub fn apply_text_edits( mut query: Query<&mut EditableText>, @@ -209,3 +216,21 @@ pub fn apply_text_edits( editable_text.apply_pending_edits(&mut font_context.0, &mut layout_context.0); } } + +/// A +pub fn edit_to_computed( + mut query: Query<(&mut EditableText, &mut ComputedTextBlock)>, + mut font_context: ResMut, + mut layout_context: ResMut, +) { + for (mut editable_text, mut computed) in query.iter_mut() { + // TODO: calculate cursor width + editable_text.cursor_width = 20.0; + + let editor = editable_text.editor_mut(); + let layout = editor.layout(&mut font_context.0, &mut layout_context.0); + + computed.layout = layout.clone(); + computed.needs_rerender = true; + } +} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 2cfd074801452..61e0028b57f5e 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -44,6 +44,7 @@ pub use layout::*; pub use measurement::*; pub use ui_node::*; pub use ui_transform::*; +pub use widget::TextNodeFlags; /// The UI prelude. /// @@ -224,7 +225,10 @@ fn build_text_interop(app: &mut App) { app.add_systems( PostUpdate, ( - widget::measure_text_system + ( + widget::measure_text_system, + widget::measure_editable_text_system, + ) .chain() .after(detect_text_needs_rerender) .after(bevy_text::load_font_assets_into_font_collection) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 8e51fc7e4b03e..c3e2752984a6b 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -19,9 +19,10 @@ use bevy_log::warn_once; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, LineHeight, - RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, - TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, + ComputedTextBlock, EditableText, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, + LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, + TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, + TextWriter, }; use taffy::style::AvailableSpace; use tracing::error; @@ -324,6 +325,130 @@ pub fn measure_text_system( } } +/// holder to simulate text spans +pub struct EditableTextAsSpan<'a> { + item: Option<(Entity, usize, &'a str, &'a TextFont, Color, LineHeight)>, +} + +impl<'a> Iterator for EditableTextAsSpan<'a> { + type Item = (Entity, usize, &'a str, &'a TextFont, Color, LineHeight); + + fn next(&mut self) -> Option { + self.item.take() + } +} + +impl<'a> EditableTextAsSpan<'a> { + pub fn new( + entity: Entity, + text: &'a str, + text_font: &'a TextFont, + text_color: TextColor, + line_height: LineHeight, + ) -> Self { + Self { + item: Some((entity, 1, text, text_font, text_color.0, line_height)), + } + } +} + +/// is the same as measure_text_system but we coerce a EditableText to an iter of TextSpan's +pub fn measure_editable_text_system( + fonts: Res>, + mut text_query: Query< + ( + Entity, + Ref, + Ref, + &mut ContentSize, + &mut TextNodeFlags, + &mut ComputedTextBlock, + Ref, + &ComputedNode, + Ref, + Ref, + Ref, + ), + With, + >, + mut text_pipeline: ResMut, + mut font_system: ResMut, + mut layout_cx: ResMut, + rem_size: Res, +) { + for ( + entity, + text, + block, + mut content_size, + mut text_flags, + mut computed, + computed_target, + computed_node, + text_font, + text_color, + line_height, + ) in &mut text_query + { + // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure(). + // 1e-5 epsilon to ignore tiny scale factor float errors + if !(1e-5 + < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() + || computed.needs_rerender(computed_target.is_changed(), rem_size.is_changed()) + || text.is_changed() + || text_flags.needs_measure_fn + || content_size.is_added()) + { + continue; + } + + let t = text.value().to_string(); + let text_spans = EditableTextAsSpan::new(entity, &t, &text_font, *text_color, *line_height); + + match text_pipeline.create_text_measure( + entity, + fonts.as_ref(), + text_spans, + computed_target.scale_factor, + &block, + computed.as_mut(), + &mut font_system, + &mut layout_cx, + computed_target.logical_size(), + rem_size.0, + ) { + Ok(measure) => { + if block.linebreak == LineBreak::NoWrap { + content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max })); + } else { + content_size.set(NodeMeasure::Text(TextMeasure { info: measure })); + } + + // Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute + text_flags.needs_measure_fn = false; + text_flags.needs_recompute = true; + } + Err( + TextError::NoSuchFont + | TextError::NoSuchFontFamily(_) + | TextError::DegenerateScaleFactor, + ) => { + // Try again next frame + text_flags.needs_measure_fn = true; + } + Err( + e @ (TextError::FailedToAddGlyph(_) + | TextError::FailedToGetGlyphImage(_) + | TextError::MissingAtlasLayout + | TextError::MissingAtlasTexture + | TextError::InconsistentAtlasState), + ) => { + panic!("Fatal error when processing text: {e}."); + } + }; + } +} + /// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component, /// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true. /// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`]. @@ -342,12 +467,20 @@ pub fn text_system( &mut TextLayoutInfo, &mut TextNodeFlags, &mut ComputedTextBlock, + Option<&mut EditableText>, Ref, )>, mut scale_cx: ResMut, ) { - for (node, block, mut text_layout_info, mut text_flags, mut computed, hinting) in - &mut text_query + for ( + node, + block, + mut text_layout_info, + mut text_flags, + mut computed, + mut maybe_editable_text, + hinting, + ) in &mut text_query { if node.is_changed() || text_flags.needs_recompute || hinting.is_changed() { // Skip the text node if it is waiting for a new measure func @@ -369,6 +502,7 @@ pub fn text_system( &mut textures, &mut computed, &mut scale_cx, + &mut maybe_editable_text, physical_node_size, block.justify, *hinting, diff --git a/crates/bevy_ui_render/src/cursor.rs b/crates/bevy_ui_render/src/cursor.rs index 1aa107f290900..e3cf22e018ce0 100644 --- a/crates/bevy_ui_render/src/cursor.rs +++ b/crates/bevy_ui_render/src/cursor.rs @@ -1,3 +1,18 @@ +use bevy_asset::AssetId; +use bevy_camera::visibility::InheritedVisibility; +use bevy_color::Alpha; +use bevy_ecs::prelude::*; +use bevy_math::{Affine2, Rect, Vec2}; +use bevy_render::{sync_world::TemporaryRenderEntity, Extract}; +use bevy_sprite::BorderRect; +use bevy_text::{TextCursorStyle, TextLayoutInfo}; +use bevy_ui::{ + CalculatedClip, ComputedNode, ComputedUiTargetCamera, ResolvedBorderRadius, UiGlobalTransform, +}; + +use crate::{ + stack_z_offsets, ExtractedUiItem, ExtractedUiNode, ExtractedUiNodes, NodeType, UiCameraMap, +}; pub fn extract_text_cursor( mut commands: Commands, @@ -38,8 +53,7 @@ pub fn extract_text_cursor( continue; }; - let transform = - Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); + let transform = Affine2::from(global_transform); // * Affine2::from_translation(0.5 * uinode.size()); if !text_layout_info.selection_rects.is_empty() && !cursor_style.selection_color.is_fully_transparent() @@ -53,7 +67,7 @@ pub fn extract_text_cursor( clip: maybe_clip.map(|clip| clip.clip), image: AssetId::default(), extracted_camera_entity, - transform: transform * Affine2::from_translation(selection.center()), + transform: transform * Affine2::from_translation(selection.min), item: ExtractedUiItem::Node { color: selection_color, rect: Rect { @@ -72,19 +86,24 @@ pub fn extract_text_cursor( } } - if !text_layout_info.cursor.is_empty() && !cursor_style.color.is_fully_transparent() { + if text_layout_info.cursor.is_some() + && !text_layout_info.cursor.unwrap().is_empty() + && !cursor_style.color.is_fully_transparent() + { + let x = text_layout_info.cursor.unwrap(); + extracted_uinodes.uinodes.push(ExtractedUiNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT_CURSOR, clip: maybe_clip.map(|clip| clip.clip), image: AssetId::default(), extracted_camera_entity, - transform: transform * Affine2::from_translation(text_layout_info.cursor.center()), + transform: transform * Affine2::from_translation(x.min), item: ExtractedUiItem::Node { color: cursor_style.color.to_linear(), rect: Rect { min: Vec2::ZERO, - max: text_layout_info.cursor.size(), + max: x.size(), }, atlas_scaling: None, flip_x: false, diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 104338d540a08..e27bc38b77b41 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -9,6 +9,7 @@ pub mod box_shadow; mod color_space; +mod cursor; mod gradient; mod pipeline; mod render_pass; @@ -25,7 +26,6 @@ use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_shader::load_shader_library; use bevy_sprite_render::SpriteAssetEvents; -use bevy_text::editing::TextCursorStyle; use bevy_ui::widget::{ImageNode, TextShadow, ViewportNode}; use bevy_ui::{ BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedUiTargetCamera, Display, @@ -64,8 +64,8 @@ use gradient::GradientPlugin; use bevy_platform::collections::{HashMap, HashSet}; use bevy_text::{ - ComputedTextBlock, PositionedGlyph, Strikethrough, StrikethroughColor, TextBackgroundColor, - TextColor, TextLayoutInfo, Underline, UnderlineColor, + ComputedTextBlock, EditableText, PositionedGlyph, Strikethrough, StrikethroughColor, + TextBackgroundColor, TextColor, TextLayoutInfo, Underline, UnderlineColor, }; use bevy_transform::components::GlobalTransform; use box_shadow::BoxShadowPlugin; @@ -77,6 +77,7 @@ pub use render_pass::*; pub use ui_material_pipeline::*; use ui_texture_slice_pipeline::UiTextureSlicerPlugin; +use crate::cursor::extract_text_cursor; use crate::shader_flags::INVERT; pub mod prelude { @@ -128,6 +129,7 @@ pub enum RenderUiSystems { ExtractTextBackgrounds, ExtractTextShadows, ExtractText, + ExtractCursor, ExtractDebug, ExtractGradient, } @@ -225,6 +227,7 @@ impl Plugin for UiRenderPlugin { RenderUiSystems::ExtractTextBackgrounds, RenderUiSystems::ExtractTextShadows, RenderUiSystems::ExtractText, + RenderUiSystems::ExtractCursor, RenderUiSystems::ExtractDebug, ) .chain(), @@ -241,6 +244,7 @@ impl Plugin for UiRenderPlugin { extract_text_decorations.in_set(RenderUiSystems::ExtractTextBackgrounds), extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), extract_text_sections.in_set(RenderUiSystems::ExtractText), + extract_text_cursor.in_set(RenderUiSystems::ExtractCursor), #[cfg(feature = "bevy_ui_debug")] debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), ), @@ -891,6 +895,7 @@ pub fn extract_text_sections( uinode_query: Extract< Query<( Entity, + Option<&EditableText>, &ComputedNode, &UiGlobalTransform, &InheritedVisibility, @@ -910,6 +915,7 @@ pub fn extract_text_sections( let mut camera_mapper = camera_map.get_mapper(); for ( entity, + _maybe_et, uinode, transform, inherited_visibility, diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index 8d8c1a0b71dfc..37c0d38476586 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -5,14 +5,30 @@ //! that includes e.g. a background, border, and text label. //! //! See the module documentation for [`editable_text`](bevy::ui_widgets::editable_text) for more details. +use bevy::color::palettes::css::{BLUE, GREEN, RED, YELLOW}; use bevy::input_focus::{InputDispatchPlugin, InputFocus}; use bevy::prelude::*; -use bevy::text::EditableText; +use bevy::remote::{http::RemoteHttpPlugin, RemotePlugin}; +use bevy::text::{EditableText, FontCx, LayoutCx, TextCursorStyle}; use bevy::ui_widgets::EditableTextInputPlugin; fn main() { App::new() .add_plugins(DefaultPlugins) + .insert_resource(GlobalUiDebugOptions { + enabled: true, + line_width: 1., + line_color_override: None, + show_hidden: true, + show_clipped: true, + ignore_border_radius: false, + outline_border_box: true, + outline_padding_box: true, + outline_content_box: true, + outline_scrollbars: false, + }) + .add_plugins(RemotePlugin::default()) + .add_plugins(RemoteHttpPlugin::default()) .add_plugins(( // This is also part of UiWidgetsPlugins, but we only need EditableText for this example EditableTextInputPlugin, @@ -20,9 +36,14 @@ fn main() { InputDispatchPlugin, )) .add_systems(Startup, setup) + .add_systems(PreUpdate, rotate_tab) .add_systems(Update, text_submission) .run(); } + +#[derive(Component)] +struct TextOutput; + fn setup( mut commands: Commands, asset_server: Res, @@ -34,56 +55,178 @@ fn setup( // Create a root UI node, so we can place the input above the output in a column // TODO: center things nicely - let root = commands.spawn(Node { ..default() }).id(); + let root = commands + .spawn(Node { + display: Display::Block, + ..default() + }) + .id(); - // Set up an EditableText widget - let text_input = commands + let font: FontSource = asset_server.load("fonts/FiraMono-Medium.ttf").into(); + + // Instructions + let text_instructions = commands .spawn(( - EditableText::default(), + Node { + width: px(400), + height: px(100), + ..Default::default() + }, + BorderColor::from(Color::from(YELLOW)), + Text::new("Tab to change input, Ctrl+Enter to submit"), TextFont { - font: asset_server.load("fonts/FiraMono-Medium.ttf").into(), - font_size: FontSize::Px(70.0), + font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), + font_size: FontSize::Px(30.0), ..default() }, + UiTransform::from_translation(Val2 { + x: Val::Px(0.0), + y: Val::Px(0.0), + }), )) .id(); + // Set up an EditableText widget + let (text_input_left, text_input_left_edit) = + build_input_text(&mut commands, &font, true, 70.0); + let (text_input_right, _text_input_right_edit) = + build_input_text(&mut commands, &font, false, 50.0); + // Set the focus to our text input so we can start typing right away - input_focus.set(text_input); + input_focus.set(text_input_left_edit); // Set up a text output to see the result of our text input let text_output = commands .spawn(( + Node { + width: px(400), + height: px(100), + border: UiRect { + left: px(5), + right: px(5), + top: px(5), + bottom: px(5), + }, + ..Default::default() + }, + BorderColor::from(Color::from(YELLOW)), Text::new("testing"), + TextOutput, TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(70.0), ..default() }, + UiTransform::from_translation(Val2 { + x: Val::Px(5.0), + y: Val::Px(200.0), + }), )) .id(); // Assemble our hierarchy - commands - .entity(root) - .add_children(&[text_input, text_output]); + commands.entity(root).add_children(&[ + text_instructions, + text_input_left, + text_input_right, + text_output, + ]); +} + +fn build_input_text( + commands: &mut Commands, + font: &FontSource, + is_left: bool, + font_size: f32, +) -> (Entity, Entity) { + let outer = commands + .spawn(( + Node { + width: px(200), + height: px(100), + border: UiRect { + left: px(5), + right: px(5), + top: px(5), + bottom: px(5), + }, + ..Default::default() + }, + BorderColor::from(Color::from(YELLOW)), + UiTransform::from_translation(Val2 { + x: Val::Px(if is_left { 0.0 } else { 300.0 }), + y: Val::Px(50.0), + }), + )) + .id(); + + let edit = commands + .spawn(( + Name::new(if is_left { "Left" } else { "Right" }), + EditableText::new(""), + TextFont { + font: font.clone(), + font_size: FontSize::Px(font_size), + ..default() + }, + TextCursorStyle { + color: RED.into(), + selection_color: GREEN.into(), + selected_text_color: Some(BLUE.into()), + }, + UiTransform::from_translation(Val2 { + x: Val::Px(10.0), + y: Val::Px(10.0), + }), + )) + .id(); + + commands.entity(outer).add_children(&[edit]); + + return (outer, edit); } // Submit the text when Ctrl+Enter is pressed fn text_submission( input_focus: Res, keyboard_input: Res>, - mut text_input: Query<&mut EditableText>, - mut text_output: Single<&mut Text>, + mut text_input: Query<(&mut EditableText, &Name)>, + mut text_output: Single<&mut Text, With>, + mut font_context: ResMut, + mut layout_context: ResMut, ) { if keyboard_input.just_pressed(KeyCode::Enter) && (keyboard_input.pressed(KeyCode::ControlLeft) || keyboard_input.pressed(KeyCode::ControlRight)) { if let Some(focused_entity) = input_focus.get() { - if let Some(mut text_input) = text_input.get_mut(focused_entity).ok() { - text_output.0 = text_input.value().clone().to_string(); - text_input.clear(); + if let Some((mut text_input, name)) = text_input.get_mut(focused_entity).ok() { + let input = text_input.value().clone().to_string(); + text_output.0 = format!("{:}: {:}", name, input); + + text_input.clear(&mut font_context.0, &mut layout_context.0); + } + } + } +} + +fn rotate_tab( + mut input_focus: ResMut, + keyboard_input: Res>, + text_input: Query>, +) { + if keyboard_input.just_pressed(KeyCode::Tab) { + let focused_entity = input_focus.0; + + for entity in text_input.iter() { + if focused_entity.is_none() { + (*input_focus).0 = Some(entity); + return; + } else { + if entity != focused_entity.unwrap() { + (*input_focus).0 = Some(entity); + return; + } } } } From 8e0689736f1472ed51ef03ff1a224f96672c1630 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 00:59:18 +0800 Subject: [PATCH 10/40] CI CI docfixs docs --- .github/actions/install-linux-deps/action.yml | 2 +- Cargo.toml | 2 +- crates/bevy_text/src/pipeline.rs | 4 ++-- crates/bevy_text/src/text_edit.rs | 12 ++++------ crates/bevy_text/src/text_editable.rs | 23 ++++++++----------- crates/bevy_ui/src/widget/text.rs | 2 +- crates/bevy_ui_widgets/src/editable_text.rs | 2 +- examples/README.md | 1 + release-content/release-notes/text_input.md | 4 ++-- 9 files changed, 23 insertions(+), 29 deletions(-) diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml index 10f2b2fd609f7..bad9de31d6193 100644 --- a/.github/actions/install-linux-deps/action.yml +++ b/.github/actions/install-linux-deps/action.yml @@ -40,7 +40,7 @@ inputs: fontconfig: description: Install fontconfig (libfontconfig1-dev) required: false - default: "false" + default: "true" runs: using: composite steps: diff --git a/Cargo.toml b/Cargo.toml index 2ee5b38849f58..dc8c5c3bd68e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1013,7 +1013,7 @@ wasm = true name = "editable_text" path = "examples/ui/text/editable_text.rs" doc-scrape-examples = true -required-features = ["bevy_remote","bevy_ui_debug"] +required-features = ["bevy_remote", "bevy_ui_debug"] [package.metadata.example.editable_text] name = "Editable Text" diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 37edc3aa2b5fd..d927551e62412 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -281,7 +281,7 @@ impl TextPipeline { layout_with_bounds(layout, bounds, justify); for (line_index, line) in layout.lines().enumerate() { - for item in line.items().into_iter() { + for item in line.items() { if let PositionedLayoutItem::GlyphRun(glyph_run) = item { let span_index = glyph_run.style().brush.0 as usize; let font_smoothing = glyph_run.style().brush.1; @@ -378,7 +378,7 @@ impl TextPipeline { .editor .cursor_geometry(editable_text.cursor_width); - layout_info.cursor = geom.map_or(None, |f| Some(bounding_box_to_rect(f))); + layout_info.cursor = geom.map(bounding_box_to_rect); } Ok(()) diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index d9a1e81ad096e..8986d85a889a9 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -11,13 +11,11 @@ pub enum TextEdit { /// Typically generated in response to keyboard text input events. /// /// This is intended to insert a single Unicode grapheme cluster, such as a letter, digit, punctuation mark, or emoji. - /// Ordinarily, this is derived from [`KeyboardInput::logical_key`](bevy_input::keyboard::KeyboardInput::logical_key), - /// which stores a [`SmolStr`] inside of the [`Key::Character`] variant, which may represent multiple bytes. Insert(SmolStr), /// Delete the character behind the cursor. /// If there is a selection, deletes the selection instead. /// - /// Typically generated in response to the [`Backspace`](Key::Backspace) key. + /// Typically generated in response to the backspace key. /// /// This operation removes an entire Unicode grapheme cluster, which may consist of multiple bytes, /// shifting the cursor position accordingly. @@ -25,18 +23,18 @@ pub enum TextEdit { /// Delete the character at the cursor. /// If there is a selection, deletes the selection instead. /// - /// Typically generated in response to the [`Delete`](Key::Delete) key. + /// Typically generated in response to the delete key. /// /// This operation removes an entire Unicode grapheme cluster, which may consist of multiple bytes, /// shifting the cursor position accordingly. Delete, /// Moves the cursor by one position to the right. /// - /// Typically generated in response to the [`Right`](Key::Right) key. + /// Typically generated in response to the right key. MoveCursorRight, /// Moves the cursor by one position to the left. /// - /// Typically generated in response to the [`Left`](Key::Left) key. + /// Typically generated in response to the left key. MoveCursorLeft, } @@ -52,5 +50,5 @@ pub fn apply_edit<'a>( TextEdit::MoveCursorRight => driver.move_right(), TextEdit::MoveCursorLeft => driver.move_left(), } - return driver; + driver } diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index f13e1b0b41bde..0f05aacbbe1f0 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -2,9 +2,9 @@ //! //! The [`EditableText`] widget is an undecorated rectangular text input field, //! which allows users to input and edit text within a Bevy UI application. -//! Every [`EditableText`] component is also a [`Node`] in the Bevy UI hierarchy, +//! Every [`EditableText`] component is also a [`Node`](https://docs.rs/bevy/latest/bevy/prelude/struct.Node.html) in the Bevy UI hierarchy, //! allowing you to position and size it using standard Bevy UI layout techniques. -//! You can think of it as the editable equivalent of [`Text`](bevy_ui::prelude::Text), +//! You can think of it as the editable equivalent of [`Text`](https://docs.rs/bevy/latest/bevy/prelude/struct.Text.html), //! and components such as [`TextFont`] and [`TextColor`] can be used to style it. //! //! [`EditableText`] supports the following functionality: @@ -66,16 +66,11 @@ //! //! If you require any of these features, please consider contributing it to the crate, //! one feature at a time! -//! -//! # Usage -//! -//! To use this widget, ensure that the [`EditableTextPlugin`] has been added to your Bevy app. -//! Then, you can add a [`EditableText`] component to any UI node. // Note: this logic is in `bevy_text`, rather than higher up in `bevy_ui` or `bevy_ui_widgets`, // because doing so allows us to process `EditableText` in the various systems provided by `bevy_text` // and `bevy_ui`, such as text layout and font management. -use std::collections::VecDeque; +use alloc::collections::VecDeque; use crate::{ apply_edit, text_edit::TextEdit, ComputedTextBlock, FontCx, FontHinting, FontSmoothing, @@ -86,7 +81,7 @@ use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; /// A plain-text text input field. /// -/// Please see the [`editable_text` module](crate::editable_text) for more details on usage and functionality. +/// Please see the [`text_editable` module](crate::text_editable) for more details on usage and functionality. /// /// Note that text editing operations are trickier than they might first appear, /// due to the complexities of Unicode text handling. @@ -98,9 +93,9 @@ use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; #[derive(Component)] #[require(TextLayout, TextFont, TextColor, LineHeight, FontHinting)] pub struct EditableText { - /// A [parley::PlainEditor], tracking both the text content and cursor position. + /// A [`parley::PlainEditor`], tracking both the text content and cursor position. /// - /// This serves as an analogue to [`ComputedTextBlock`](crate::ComputedTextBlock) for editable text. + /// This serves as an analogue to [`ComputedTextBlock`] for editable text. /// /// In most cases, you should queue text edits via the [`EditableText::queue_edit`] method instead of directly manipulating the editor, /// and then allow the [`apply_text_edits`] system to apply the edits at the appropriate time in the update cycle. @@ -138,7 +133,7 @@ impl EditableText { pub fn new(text: &str) -> Self { let mut a = Self::default(); a.set_input(text); - return a; + a } /// Access the internal [`PlainEditor`]. @@ -154,7 +149,7 @@ impl EditableText { /// Get the current text input as a [`SplitString`]. /// - /// A [`SplitString`] can be converted into a [`String`] using [`SplitString::to_string`] if needed. + /// A [`SplitString`] can be converted into a [`String`] using `to_string` if needed. pub fn value(&self) -> SplitString<'_> { self.editor.text() } @@ -175,7 +170,7 @@ impl EditableText { // Take the `pending_edits` out of the struct so we can apply them without mutable aliasing issues. // We do not need to put the `pending_edits` back into the struct, // as all edits should be consumed by this method, leaving `pending_edits` empty at the end. - let mut pending_edits = std::mem::take(&mut self.pending_edits); + let mut pending_edits = core::mem::take(&mut self.pending_edits); let mut driver = self.editor_mut().driver(font_context, layout_context); while let Some(edit) = pending_edits.pop_front() { diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index c3e2752984a6b..18d06ae744cce 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -352,7 +352,7 @@ impl<'a> EditableTextAsSpan<'a> { } } -/// is the same as measure_text_system but we coerce a EditableText to an iter of TextSpan's +/// is the same as [`measure_text_system`] but we coerce a [`EditableText`] to an iter of [`TextSpan`](bevy_text::TextSpan)'s pub fn measure_editable_text_system( fonts: Res>, mut text_query: Query< diff --git a/crates/bevy_ui_widgets/src/editable_text.rs b/crates/bevy_ui_widgets/src/editable_text.rs index 57afc71d2746d..cf9debc2e3bb7 100644 --- a/crates/bevy_ui_widgets/src/editable_text.rs +++ b/crates/bevy_ui_widgets/src/editable_text.rs @@ -22,7 +22,7 @@ use bevy_ui::{widget::TextNodeFlags, ContentSize, Node}; /// used by this system. /// /// Note that this does not immediately apply the edits; they are queued up in [`EditableText::pending_edits`], -/// and then applied later by the [`apply_text_edits`] system. +/// and then applied later by the [`apply_text_edits`](`bevy_text::apply_text_edits`) system. pub fn process_text_inputs( focus: Res, mut query: Query<&mut EditableText>, diff --git a/examples/README.md b/examples/README.md index 5a43b3a9d5c95..cb247c443373f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -588,6 +588,7 @@ Example | Description [Directional Navigation Overrides](../examples/ui/navigation/directional_navigation_overrides.rs) | Demonstration of automatic directional navigation between UI elements with manual overrides [Display and Visibility](../examples/ui/layout/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI. [Drag to Scroll](../examples/ui/scroll_and_overflow/drag_to_scroll.rs) | This example tests scale factor, dragging and scrolling +[Editable Text](../examples/ui/text/editable_text.rs) | Demonstrates a simple, unstyled text input widget [Feathers Widgets](../examples/ui/widgets/feathers.rs) | Gallery of Feathers Widgets [Flex Layout](../examples/ui/layout/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text [Font Atlas Debug](../examples/ui/text/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) diff --git a/release-content/release-notes/text_input.md b/release-content/release-notes/text_input.md index 00cd277ff924c..dc9ef621ad45a 100644 --- a/release-content/release-notes/text_input.md +++ b/release-content/release-notes/text_input.md @@ -1,7 +1,7 @@ --- title: "Text input" authors: ["@alice-i-cecile", "@ickshonpe", "@Zeophlite"] -pull_requests: [TODO] +pull_requests: [23282] --- Entering text into an application is a common task, even for games. @@ -22,4 +22,4 @@ Our initial text entry supports: Many important features are currently unimplemented (placeholder text, text selection, clipboard support, undo-redo...). While we've been careful to expose and document the internals so that you can readily implement these features in your own projects, -we would like to continue to expand the functionality of the base widget: please consider making a PR! \ No newline at end of file +we would like to continue to expand the functionality of the base widget: please consider making a PR! From 0a4c80235cbf73ac131701e0d2d4d1e6dc6c51fa Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 01:45:06 +0800 Subject: [PATCH 11/40] CI --- crates/bevy_sprite/src/lib.rs | 1 + crates/bevy_text/src/text_editable.rs | 2 +- crates/bevy_ui/src/lib.rs | 8 ++++---- crates/bevy_ui/src/picking_backend.rs | 8 ++++++-- crates/bevy_ui_render/src/cursor.rs | 10 ++++------ crates/bevy_ui_widgets/src/editable_text.rs | 8 ++------ 6 files changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index db281746bdd17..2c0f0ac0ced57 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -93,6 +93,7 @@ impl Plugin for SpritePlugin { .chain() .after(detect_text_needs_rerender) .after(bevy_text::load_font_assets_into_font_collection) + .after(bevy_text::apply_text_edits) .after(bevy_app::AnimationSystems) .before(bevy_asset::AssetEventSystems), ) diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 0f05aacbbe1f0..924c223937f82 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -81,7 +81,7 @@ use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; /// A plain-text text input field. /// -/// Please see the [`text_editable` module](crate::text_editable) for more details on usage and functionality. +/// Please see this module docs for more details on usage and functionality. /// /// Note that text editing operations are trickier than they might first appear, /// due to the complexities of Unicode text handling. diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 61e0028b57f5e..54d7aa63ac2e6 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -81,6 +81,8 @@ use stack::ui_stack_system; pub use stack::UiStack; use update::{propagate_ui_target_cameras, update_clipping_system}; +use crate::widget::measure_editable_text_system; + /// The basic plugin for Bevy UI #[derive(Default)] pub struct UiPlugin; @@ -192,6 +194,7 @@ impl Plugin for UiPlugin { ui_layout_system_config, ui_stack_system .in_set(UiSystems::Stack) + .after(measure_editable_text_system) // These systems don't care about stack index .ambiguous_with(widget::measure_text_system) .ambiguous_with(update_clipping_system) @@ -225,10 +228,7 @@ fn build_text_interop(app: &mut App) { app.add_systems( PostUpdate, ( - ( - widget::measure_text_system, - widget::measure_editable_text_system, - ) + (widget::measure_text_system, measure_editable_text_system) .chain() .after(detect_text_needs_rerender) .after(bevy_text::load_font_assets_into_font_collection) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 49c186207db02..7da0034fb29ab 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -76,8 +76,12 @@ impl Default for UiPickingSettings { pub struct UiPickingPlugin; impl Plugin for UiPickingPlugin { fn build(&self, app: &mut App) { - app.init_resource::() - .add_systems(PreUpdate, ui_picking.in_set(PickingSystems::Backend)); + app.init_resource::().add_systems( + PreUpdate, + ui_picking + .in_set(PickingSystems::Backend) + .after(bevy_text::edit_to_computed), + ); } } diff --git a/crates/bevy_ui_render/src/cursor.rs b/crates/bevy_ui_render/src/cursor.rs index e3cf22e018ce0..39cdf262c5a79 100644 --- a/crates/bevy_ui_render/src/cursor.rs +++ b/crates/bevy_ui_render/src/cursor.rs @@ -86,24 +86,22 @@ pub fn extract_text_cursor( } } - if text_layout_info.cursor.is_some() - && !text_layout_info.cursor.unwrap().is_empty() + if let Some(cursor_rect) = text_layout_info.cursor + && !cursor_rect.is_empty() && !cursor_style.color.is_fully_transparent() { - let x = text_layout_info.cursor.unwrap(); - extracted_uinodes.uinodes.push(ExtractedUiNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT_CURSOR, clip: maybe_clip.map(|clip| clip.clip), image: AssetId::default(), extracted_camera_entity, - transform: transform * Affine2::from_translation(x.min), + transform: transform * Affine2::from_translation(cursor_rect.min), item: ExtractedUiItem::Node { color: cursor_style.color.to_linear(), rect: Rect { min: Vec2::ZERO, - max: x.size(), + max: cursor_rect.size(), }, atlas_scaling: None, flip_x: false, diff --git a/crates/bevy_ui_widgets/src/editable_text.rs b/crates/bevy_ui_widgets/src/editable_text.rs index cf9debc2e3bb7..f26b323d264a5 100644 --- a/crates/bevy_ui_widgets/src/editable_text.rs +++ b/crates/bevy_ui_widgets/src/editable_text.rs @@ -29,15 +29,11 @@ pub fn process_text_inputs( mut keyboard_input: MessageReader, ) { // Check if any EditableText is focused - let focused_entity = if let Some(entity) = focus.get() { - entity - } else { + let Some(focused_entity) = focus.get() else { return; // No focused entity, nothing to do }; - let mut editable_text = if let Ok(editable_text) = query.get_mut(focused_entity) { - editable_text - } else { + let Ok(mut editable_text) = query.get_mut(focused_entity) else { return; // Focused entity is not an EditableText, nothing to do }; From 1a8b3cc139c724340feb583f4ed0c0db8de97d31 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 06:55:17 +0800 Subject: [PATCH 12/40] Clippy --- examples/ui/text/editable_text.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index 37c0d38476586..f073cfe920b72 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -51,7 +51,7 @@ fn setup( ) { // Set up a camera // We need a camera to see the UI - commands.spawn(Camera2d::default()); + commands.spawn(Camera2d); // Create a root UI node, so we can place the input above the output in a column // TODO: center things nicely @@ -183,7 +183,7 @@ fn build_input_text( commands.entity(outer).add_children(&[edit]); - return (outer, edit); + (outer, edit) } // Submit the text when Ctrl+Enter is pressed @@ -198,15 +198,13 @@ fn text_submission( if keyboard_input.just_pressed(KeyCode::Enter) && (keyboard_input.pressed(KeyCode::ControlLeft) || keyboard_input.pressed(KeyCode::ControlRight)) + && let Some(focused_entity) = input_focus.get() + && let Ok((mut text_input, name)) = text_input.get_mut(focused_entity) { - if let Some(focused_entity) = input_focus.get() { - if let Some((mut text_input, name)) = text_input.get_mut(focused_entity).ok() { - let input = text_input.value().clone().to_string(); - text_output.0 = format!("{:}: {:}", name, input); + let input = text_input.value().clone().to_string(); + text_output.0 = format!("{:}: {:}", name, input); - text_input.clear(&mut font_context.0, &mut layout_context.0); - } - } + text_input.clear(&mut font_context.0, &mut layout_context.0); } } @@ -220,13 +218,15 @@ fn rotate_tab( for entity in text_input.iter() { if focused_entity.is_none() { - (*input_focus).0 = Some(entity); + input_focus.0 = Some(entity); + return; + } + + if let Some(focused_entity) = focused_entity + && entity != focused_entity + { + input_focus.0 = Some(entity); return; - } else { - if entity != focused_entity.unwrap() { - (*input_focus).0 = Some(entity); - return; - } } } } From 85fd548cc805a3a5cfeabf67aa9d11795f546004 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 19:06:27 +0800 Subject: [PATCH 13/40] Apply suggestions from code review Co-authored-by: Alice Cecile --- crates/bevy_ui/src/widget/text.rs | 2 +- release-content/release-notes/text_input.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 18d06ae744cce..f7f033db101d7 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -352,7 +352,7 @@ impl<'a> EditableTextAsSpan<'a> { } } -/// is the same as [`measure_text_system`] but we coerce a [`EditableText`] to an iter of [`TextSpan`](bevy_text::TextSpan)'s +/// Analagous to [`measure_text_system`] but we coerce a [`EditableText`] to an iter of [`TextSpan`](bevy_text::TextSpan)'s pub fn measure_editable_text_system( fonts: Res>, mut text_query: Query< diff --git a/release-content/release-notes/text_input.md b/release-content/release-notes/text_input.md index dc9ef621ad45a..7cbc392965651 100644 --- a/release-content/release-notes/text_input.md +++ b/release-content/release-notes/text_input.md @@ -1,6 +1,6 @@ --- title: "Text input" -authors: ["@alice-i-cecile", "@ickshonpe", "@Zeophlite"] +authors: ["@ickshonpe", "@Zeophlite", "@alice-i-cecile"] pull_requests: [23282] --- From 8a8cc574ac8eb3df96eb5469fe152e1e1460e188 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 19:20:15 +0800 Subject: [PATCH 14/40] Further feedback --- Cargo.toml | 1 - crates/bevy_text/src/text_editable.rs | 19 +++---------------- crates/bevy_ui/src/widget/text.rs | 5 +++-- crates/bevy_ui_render/src/cursor.rs | 2 +- examples/ui/text/editable_text.rs | 15 --------------- 5 files changed, 7 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dc8c5c3bd68e2..adb4267939942 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1013,7 +1013,6 @@ wasm = true name = "editable_text" path = "examples/ui/text/editable_text.rs" doc-scrape-examples = true -required-features = ["bevy_remote", "bevy_ui_debug"] [package.metadata.example.editable_text] name = "Editable Text" diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 924c223937f82..b04ce0f76389d 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -129,13 +129,6 @@ impl Default for EditableText { } impl EditableText { - /// build with initial text - pub fn new(text: &str) -> Self { - let mut a = Self::default(); - a.set_input(text); - a - } - /// Access the internal [`PlainEditor`]. pub fn editor(&self) -> &PlainEditor<(u32, FontSmoothing)> { &self.editor @@ -179,14 +172,6 @@ impl EditableText { self.needs_rerender = true; } - /// Sets the entire text input to the given string, replacing any existing content. - pub fn set_input(&mut self, text: &str) { - self.editor.set_text(text); - // TODO: reset cursor location - self.pending_edits.clear(); - self.needs_rerender = true; - } - /// Clears the current input and resets the cursor position. pub fn clear( &mut self, @@ -212,12 +197,14 @@ pub fn apply_text_edits( } } -/// A +/// Copies the layout from the [`parley::PlainEditor`] to the [`ComputedTextBlock`] pub fn edit_to_computed( mut query: Query<(&mut EditableText, &mut ComputedTextBlock)>, mut font_context: ResMut, mut layout_context: ResMut, ) { + // TODO: optimise with change detection + for (mut editable_text, mut computed) in query.iter_mut() { // TODO: calculate cursor width editable_text.cursor_width = 20.0; diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index f7f033db101d7..aac005d186818 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -325,7 +325,8 @@ pub fn measure_text_system( } } -/// holder to simulate text spans +/// [`measure_text_system`] iterates over [`TextSpan`](bevy_text::TextSpan). +/// An [`EditableText`] is a single instance, so we use this helper struct. pub struct EditableTextAsSpan<'a> { item: Option<(Entity, usize, &'a str, &'a TextFont, Color, LineHeight)>, } @@ -352,7 +353,7 @@ impl<'a> EditableTextAsSpan<'a> { } } -/// Analagous to [`measure_text_system`] but we coerce a [`EditableText`] to an iter of [`TextSpan`](bevy_text::TextSpan)'s +/// Analagous to [`measure_text_system`] but for a single [`EditableText`] pub fn measure_editable_text_system( fonts: Res>, mut text_query: Query< diff --git a/crates/bevy_ui_render/src/cursor.rs b/crates/bevy_ui_render/src/cursor.rs index 39cdf262c5a79..5a02fed964bf9 100644 --- a/crates/bevy_ui_render/src/cursor.rs +++ b/crates/bevy_ui_render/src/cursor.rs @@ -53,7 +53,7 @@ pub fn extract_text_cursor( continue; }; - let transform = Affine2::from(global_transform); // * Affine2::from_translation(0.5 * uinode.size()); + let transform = Affine2::from(global_transform); if !text_layout_info.selection_rects.is_empty() && !cursor_style.selection_color.is_fully_transparent() diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index f073cfe920b72..d63614b1a4c54 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -8,27 +8,12 @@ use bevy::color::palettes::css::{BLUE, GREEN, RED, YELLOW}; use bevy::input_focus::{InputDispatchPlugin, InputFocus}; use bevy::prelude::*; -use bevy::remote::{http::RemoteHttpPlugin, RemotePlugin}; use bevy::text::{EditableText, FontCx, LayoutCx, TextCursorStyle}; use bevy::ui_widgets::EditableTextInputPlugin; fn main() { App::new() .add_plugins(DefaultPlugins) - .insert_resource(GlobalUiDebugOptions { - enabled: true, - line_width: 1., - line_color_override: None, - show_hidden: true, - show_clipped: true, - ignore_border_radius: false, - outline_border_box: true, - outline_padding_box: true, - outline_content_box: true, - outline_scrollbars: false, - }) - .add_plugins(RemotePlugin::default()) - .add_plugins(RemoteHttpPlugin::default()) .add_plugins(( // This is also part of UiWidgetsPlugins, but we only need EditableText for this example EditableTextInputPlugin, From ca339951d83970dd1dd2edde23234a03c0bc940f Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 21:43:33 +0800 Subject: [PATCH 15/40] more review --- crates/bevy_text/src/cursor.rs | 5 ++++- crates/bevy_text/src/text_editable.rs | 2 +- crates/bevy_ui/src/widget/text.rs | 2 +- examples/ui/text/editable_text.rs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/cursor.rs b/crates/bevy_text/src/cursor.rs index 4f9cfe0d6f6ba..e6983e11894c2 100644 --- a/crates/bevy_text/src/cursor.rs +++ b/crates/bevy_text/src/cursor.rs @@ -1,7 +1,10 @@ use bevy_color::Color; use bevy_ecs::component::Component; -/// Text Cursor style +/// When this component is alongside an [`EditableText`](`crate::EditableText`) , +/// and the [`UiRenderPlugin`](https://docs.rs/bevy/latest/bevy/ui_render/struct.UiRenderPlugin.html) +/// is active, a simple rectangle will be drawn for the cursor. +/// This is an optional component, to allow for stylistic cursors. #[derive(Component, Clone, Copy, Debug, PartialEq)] pub struct TextCursorStyle { /// Color of the cursor diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index b04ce0f76389d..d5ad7b5e2de87 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -203,7 +203,7 @@ pub fn edit_to_computed( mut font_context: ResMut, mut layout_context: ResMut, ) { - // TODO: optimise with change detection + // TODO: optimize with change detection for (mut editable_text, mut computed) in query.iter_mut() { // TODO: calculate cursor width diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index aac005d186818..fc81f03947fcd 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -353,7 +353,7 @@ impl<'a> EditableTextAsSpan<'a> { } } -/// Analagous to [`measure_text_system`] but for a single [`EditableText`] +/// Analogous to [`measure_text_system`] but for a single [`EditableText`] pub fn measure_editable_text_system( fonts: Res>, mut text_query: Query< diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index d63614b1a4c54..976e3cbd69b55 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -148,7 +148,7 @@ fn build_input_text( let edit = commands .spawn(( Name::new(if is_left { "Left" } else { "Right" }), - EditableText::new(""), + EditableText, TextFont { font: font.clone(), font_size: FontSize::Px(font_size), From e66f0b58101c41104e1cf7d1e76c7203d50a05bc Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 21:52:36 +0800 Subject: [PATCH 16/40] default --- examples/ui/text/editable_text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index 976e3cbd69b55..d6912dd8c21a5 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -148,7 +148,7 @@ fn build_input_text( let edit = commands .spawn(( Name::new(if is_left { "Left" } else { "Right" }), - EditableText, + EditableText::default(), TextFont { font: font.clone(), font_size: FontSize::Px(font_size), From 5b3b8ec74c4771df52b5bc7a05fc17c768023daa Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 22:16:20 +0800 Subject: [PATCH 17/40] Support changing font size --- crates/bevy_text/src/text_editable.rs | 6 ++---- crates/bevy_ui/Cargo.toml | 1 + crates/bevy_ui/src/widget/text.rs | 18 +++++++++++++++--- examples/ui/text/editable_text.rs | 21 ++++++++++++++------- 4 files changed, 32 insertions(+), 14 deletions(-) diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index d5ad7b5e2de87..02c29386f6e3e 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -120,7 +120,7 @@ impl Default for EditableText { fn default() -> Self { Self { // Defaults selected to match `Text::default()` - editor: PlainEditor::new(100.), // TODO: font size is 70, so how does this work? + editor: PlainEditor::new(100.), pending_edits: VecDeque::new(), needs_rerender: true, cursor_width: 20.0, @@ -206,10 +206,8 @@ pub fn edit_to_computed( // TODO: optimize with change detection for (mut editable_text, mut computed) in query.iter_mut() { - // TODO: calculate cursor width - editable_text.cursor_width = 20.0; - let editor = editable_text.editor_mut(); + let layout = editor.layout(&mut font_context.0, &mut layout_context.0); computed.layout = layout.clone(); diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 893145bad2cd7..fe22a7adb4062 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -50,6 +50,7 @@ thiserror = { version = "2", default-features = false } derive_more = { version = "2", default-features = false, features = ["from"] } smallvec = { version = "1", default-features = false } accesskit = "0.24" +parley = { version = "0.7.0", default-features = false, features = ["std"] } tracing = { version = "0.1", default-features = false, features = ["std"] } [dev-dependencies] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index fc81f03947fcd..ea529a07e158c 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -359,7 +359,7 @@ pub fn measure_editable_text_system( mut text_query: Query< ( Entity, - Ref, + &mut EditableText, Ref, &mut ContentSize, &mut TextNodeFlags, @@ -379,7 +379,7 @@ pub fn measure_editable_text_system( ) { for ( entity, - text, + mut text, block, mut content_size, mut text_flags, @@ -403,6 +403,18 @@ pub fn measure_editable_text_system( continue; } + // TODO: detect if sizes change, then set text_flags.needs_measure_fn and continue + let logical_viewport_size = computed_target.logical_size(); + + let font_size = text_font.font_size.eval(logical_viewport_size, rem_size.0); + + let editor = text.editor_mut(); + let styles = editor.edit_styles(); + + styles.insert(parley::StyleProperty::FontSize(font_size * 1.42)); + + text.cursor_width = font_size * 0.28; + let t = text.value().to_string(); let text_spans = EditableTextAsSpan::new(entity, &t, &text_font, *text_color, *line_height); @@ -415,7 +427,7 @@ pub fn measure_editable_text_system( computed.as_mut(), &mut font_system, &mut layout_cx, - computed_target.logical_size(), + logical_viewport_size, rem_size.0, ) { Ok(measure) => { diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index d6912dd8c21a5..ad18f381256fc 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -39,7 +39,6 @@ fn setup( commands.spawn(Camera2d); // Create a root UI node, so we can place the input above the output in a column - // TODO: center things nicely let root = commands .spawn(Node { display: Display::Block, @@ -80,6 +79,13 @@ fn setup( // Set the focus to our text input so we can start typing right away input_focus.set(text_input_left_edit); + let input_container = commands + .spawn(Node { + display: Display::Flex, + ..default() + }) + .id(); + // Set up a text output to see the result of our text input let text_output = commands .spawn(( @@ -110,12 +116,13 @@ fn setup( .id(); // Assemble our hierarchy - commands.entity(root).add_children(&[ - text_instructions, - text_input_left, - text_input_right, - text_output, - ]); + commands + .entity(input_container) + .add_children(&[text_input_left, text_input_right]); + + commands + .entity(root) + .add_children(&[text_instructions, input_container, text_output]); } fn build_input_text( From 4fe43dc9a8f651e8cd0f504e6b5f4a59e2102046 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 10 Mar 2026 22:46:36 +0800 Subject: [PATCH 18/40] Enable selection --- crates/bevy_text/src/pipeline.rs | 7 +++++++ crates/bevy_text/src/text_edit.rs | 10 ++++++++++ crates/bevy_ui_widgets/src/editable_text.rs | 20 ++++++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index d927551e62412..d03be3f5386ed 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -379,6 +379,13 @@ impl TextPipeline { .cursor_geometry(editable_text.cursor_width); layout_info.cursor = geom.map(bounding_box_to_rect); + + layout_info.selection_rects = editable_text + .editor + .selection_geometry() + .iter() + .map(|&b| bounding_box_to_rect(b.0)) + .collect(); } Ok(()) diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index 8986d85a889a9..d2a5ef2555725 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -36,6 +36,14 @@ pub enum TextEdit { /// /// Typically generated in response to the left key. MoveCursorLeft, + /// Move selection on to the right. + /// + /// Typically generated in response to shift and the right key. + SelectRight, + /// Move selection on to the left. + /// + /// Typically generated in response to shift and the left key. + SelectLeft, } /// Takes a `TextEdit` and applies to `PlainEditorDriver` @@ -49,6 +57,8 @@ pub fn apply_edit<'a>( TextEdit::Delete => driver.delete(), TextEdit::MoveCursorRight => driver.move_right(), TextEdit::MoveCursorLeft => driver.move_left(), + TextEdit::SelectRight => driver.select_right(), + TextEdit::SelectLeft => driver.select_left(), } driver } diff --git a/crates/bevy_ui_widgets/src/editable_text.rs b/crates/bevy_ui_widgets/src/editable_text.rs index f26b323d264a5..bc4973b1b68d6 100644 --- a/crates/bevy_ui_widgets/src/editable_text.rs +++ b/crates/bevy_ui_widgets/src/editable_text.rs @@ -10,8 +10,8 @@ use bevy_app::{App, Plugin, PreUpdate}; use bevy_ecs::prelude::*; -use bevy_input::keyboard::{Key, KeyboardInput}; -use bevy_input::InputSystems; +use bevy_input::keyboard::{Key, KeyCode, KeyboardInput}; +use bevy_input::{ButtonInput, InputSystems}; use bevy_input_focus::{InputFocus, InputFocusSystems}; use bevy_text::{EditableText, TextEdit}; use bevy_ui::{widget::TextNodeFlags, ContentSize, Node}; @@ -27,6 +27,7 @@ pub fn process_text_inputs( focus: Res, mut query: Query<&mut EditableText>, mut keyboard_input: MessageReader, + keyboard_button_input: Res>, ) { // Check if any EditableText is focused let Some(focused_entity) = focus.get() else { @@ -37,6 +38,9 @@ pub fn process_text_inputs( return; // Focused entity is not an EditableText, nothing to do }; + let shift = keyboard_button_input.pressed(KeyCode::ShiftLeft) + || keyboard_button_input.pressed(KeyCode::ShiftRight); + for keyboard_event in keyboard_input.read() { match keyboard_event { KeyboardInput { @@ -65,14 +69,22 @@ pub fn process_text_inputs( state: bevy_input::ButtonState::Pressed, .. } => { - editable_text.queue_edit(TextEdit::MoveCursorRight); + if shift { + editable_text.queue_edit(TextEdit::SelectRight); + } else { + editable_text.queue_edit(TextEdit::MoveCursorRight); + } } KeyboardInput { logical_key: Key::ArrowLeft, state: bevy_input::ButtonState::Pressed, .. } => { - editable_text.queue_edit(TextEdit::MoveCursorLeft); + if shift { + editable_text.queue_edit(TextEdit::SelectLeft); + } else { + editable_text.queue_edit(TextEdit::MoveCursorLeft); + } } _ => {} } From 3bb5c7458cb2523872e651098918437f4e3ecebe Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Thu, 12 Mar 2026 06:39:10 +0800 Subject: [PATCH 19/40] Only need remeasure on font size change --- crates/bevy_ui/src/widget/text.rs | 62 ++++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index ea529a07e158c..f9d961a1cb8c9 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -12,7 +12,7 @@ use bevy_ecs::{ query::With, reflect::ReflectComponent, system::{Query, Res, ResMut}, - world::Ref, + world::{Mut, Ref}, }; use bevy_image::prelude::*; use bevy_log::warn_once; @@ -403,17 +403,13 @@ pub fn measure_editable_text_system( continue; } - // TODO: detect if sizes change, then set text_flags.needs_measure_fn and continue let logical_viewport_size = computed_target.logical_size(); - let font_size = text_font.font_size.eval(logical_viewport_size, rem_size.0); + if check_style_and_cursor(&mut text, logical_viewport_size, &text_font, &rem_size) { + text_flags.needs_measure_fn = true; - let editor = text.editor_mut(); - let styles = editor.edit_styles(); - - styles.insert(parley::StyleProperty::FontSize(font_size * 1.42)); - - text.cursor_width = font_size * 0.28; + continue; + } let t = text.value().to_string(); let text_spans = EditableTextAsSpan::new(entity, &t, &text_font, *text_color, *line_height); @@ -462,6 +458,54 @@ pub fn measure_editable_text_system( } } +fn check_style_and_cursor( + text: &mut Mut, + logical_viewport_size: Vec2, + text_font: &Ref, + rem_size: &Res, +) -> bool { + // TODO: these factors don't have any science to them + const CURSOR_SCALE: f32 = 0.28; + const STYLE_SCALE: f32 = 1.52; + + let font_size = text_font.font_size.eval(logical_viewport_size, rem_size.0); + + let editor = text.editor_mut(); + let styles = editor.edit_styles(); + + let target_font_size_style = parley::StyleProperty::FontSize(font_size * STYLE_SCALE); + + let dis = core::mem::discriminant(&target_font_size_style); + + let maybe_current_font_size = styles.inner().get(&dis); + + let mut font_changes_required = false; + + if let Some(current_font_size) = maybe_current_font_size { + if let parley::StyleProperty::FontSize(x) = current_font_size { + if (x - font_size * STYLE_SCALE).abs() > 1e-5 { + styles.insert(target_font_size_style); + + font_changes_required = true; + } + } + + if (text.cursor_width - font_size * CURSOR_SCALE).abs() > 1e-5 { + text.cursor_width = font_size * CURSOR_SCALE; + + font_changes_required = true; + } + } else { + styles.insert(target_font_size_style); + + text.cursor_width = font_size * CURSOR_SCALE; + + font_changes_required = true; + } + + font_changes_required +} + /// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component, /// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true. /// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`]. From c93be2205641ff4ccdd2a7d6106017c1012f7f75 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Thu, 12 Mar 2026 09:20:33 +0800 Subject: [PATCH 20/40] CI --- crates/bevy_ui/src/widget/text.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index f9d961a1cb8c9..95029ac450a11 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -482,12 +482,12 @@ fn check_style_and_cursor( let mut font_changes_required = false; if let Some(current_font_size) = maybe_current_font_size { - if let parley::StyleProperty::FontSize(x) = current_font_size { - if (x - font_size * STYLE_SCALE).abs() > 1e-5 { - styles.insert(target_font_size_style); + if let parley::StyleProperty::FontSize(x) = current_font_size + && (x - font_size * STYLE_SCALE).abs() > 1e-5 + { + styles.insert(target_font_size_style); - font_changes_required = true; - } + font_changes_required = true; } if (text.cursor_width - font_size * CURSOR_SCALE).abs() > 1e-5 { From f6610b72ddde9381503139be4e24dd498e005efc Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Thu, 12 Mar 2026 13:59:13 +0800 Subject: [PATCH 21/40] Fix rects --- crates/bevy_ui/src/widget/text.rs | 56 ++++++++++++--------- crates/bevy_ui_render/src/cursor.rs | 7 +-- crates/bevy_ui_widgets/src/editable_text.rs | 8 +++ examples/ui/text/editable_text.rs | 4 +- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 95029ac450a11..417a07ac6ba00 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -24,6 +24,9 @@ use bevy_text::{ TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, }; +extern crate alloc; +use alloc::borrow::Cow; +use parley::{FontStack, StyleProperty}; use taffy::style::AvailableSpace; use tracing::error; @@ -405,7 +408,14 @@ pub fn measure_editable_text_system( let logical_viewport_size = computed_target.logical_size(); - if check_style_and_cursor(&mut text, logical_viewport_size, &text_font, &rem_size) { + let (font_changes_required, font_size) = + check_style(&mut text, logical_viewport_size, &text_font, &rem_size); + if font_changes_required { + let editor = text.editor_mut(); + + editor.set_scale(computed_target.scale_factor); + text.cursor_width = font_size * CURSOR_SCALE; + text_flags.needs_measure_fn = true; continue; @@ -458,22 +468,23 @@ pub fn measure_editable_text_system( } } -fn check_style_and_cursor( - text: &mut Mut, +// TODO: this factor is a guess +const CURSOR_SCALE: f32 = 0.28; + +fn check_style<'a>( + text: &mut Mut<'a, EditableText>, logical_viewport_size: Vec2, text_font: &Ref, rem_size: &Res, -) -> bool { - // TODO: these factors don't have any science to them - const CURSOR_SCALE: f32 = 0.28; - const STYLE_SCALE: f32 = 1.52; - +) -> (bool, f32) { let font_size = text_font.font_size.eval(logical_viewport_size, rem_size.0); + // NOTE: this is messy as the editor doesn't expose much to read + let editor = text.editor_mut(); let styles = editor.edit_styles(); - let target_font_size_style = parley::StyleProperty::FontSize(font_size * STYLE_SCALE); + let target_font_size_style = StyleProperty::FontSize(font_size); let dis = core::mem::discriminant(&target_font_size_style); @@ -482,28 +493,27 @@ fn check_style_and_cursor( let mut font_changes_required = false; if let Some(current_font_size) = maybe_current_font_size { - if let parley::StyleProperty::FontSize(x) = current_font_size - && (x - font_size * STYLE_SCALE).abs() > 1e-5 + if let StyleProperty::FontSize(x) = current_font_size + && (x - font_size).abs() > 1e-5 { - styles.insert(target_font_size_style); - - font_changes_required = true; - } - - if (text.cursor_width - font_size * CURSOR_SCALE).abs() > 1e-5 { - text.cursor_width = font_size * CURSOR_SCALE; - font_changes_required = true; } } else { - styles.insert(target_font_size_style); + font_changes_required = true; + } - text.cursor_width = font_size * CURSOR_SCALE; + if font_changes_required { + styles.insert(target_font_size_style); - font_changes_required = true; + // TODO: don't hardcode this + // let family = font_system.get_family(&text_font.font); + // Above returns None, as its a Handle ? + styles.insert(StyleProperty::FontStack(FontStack::Source(Cow::Borrowed( + "monospace", + )))); } - font_changes_required + (font_changes_required, font_size) } /// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component, diff --git a/crates/bevy_ui_render/src/cursor.rs b/crates/bevy_ui_render/src/cursor.rs index 5a02fed964bf9..1b2276e6cd743 100644 --- a/crates/bevy_ui_render/src/cursor.rs +++ b/crates/bevy_ui_render/src/cursor.rs @@ -53,7 +53,8 @@ pub fn extract_text_cursor( continue; }; - let transform = Affine2::from(global_transform); + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); if !text_layout_info.selection_rects.is_empty() && !cursor_style.selection_color.is_fully_transparent() @@ -67,7 +68,7 @@ pub fn extract_text_cursor( clip: maybe_clip.map(|clip| clip.clip), image: AssetId::default(), extracted_camera_entity, - transform: transform * Affine2::from_translation(selection.min), + transform: transform * Affine2::from_translation(selection.center()), item: ExtractedUiItem::Node { color: selection_color, rect: Rect { @@ -96,7 +97,7 @@ pub fn extract_text_cursor( clip: maybe_clip.map(|clip| clip.clip), image: AssetId::default(), extracted_camera_entity, - transform: transform * Affine2::from_translation(cursor_rect.min), + transform: transform * Affine2::from_translation(cursor_rect.center()), item: ExtractedUiItem::Node { color: cursor_style.color.to_linear(), rect: Rect { diff --git a/crates/bevy_ui_widgets/src/editable_text.rs b/crates/bevy_ui_widgets/src/editable_text.rs index bc4973b1b68d6..5068797654cea 100644 --- a/crates/bevy_ui_widgets/src/editable_text.rs +++ b/crates/bevy_ui_widgets/src/editable_text.rs @@ -15,6 +15,7 @@ use bevy_input::{ButtonInput, InputSystems}; use bevy_input_focus::{InputFocus, InputFocusSystems}; use bevy_text::{EditableText, TextEdit}; use bevy_ui::{widget::TextNodeFlags, ContentSize, Node}; +use smol_str::SmolStr; /// System that processes keyboard input events into text edit actions for focused [`EditableText`] widgets. /// @@ -50,6 +51,13 @@ pub fn process_text_inputs( } => { editable_text.queue_edit(TextEdit::Insert(c.clone())); } + KeyboardInput { + logical_key: Key::Space, + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::Insert(SmolStr::new_inline(" "))); + } KeyboardInput { logical_key: Key::Backspace, state: bevy_input::ButtonState::Pressed, diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index ad18f381256fc..2e4d74775e094 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -72,7 +72,7 @@ fn setup( // Set up an EditableText widget let (text_input_left, text_input_left_edit) = - build_input_text(&mut commands, &font, true, 70.0); + build_input_text(&mut commands, &font, true, 30.0); let (text_input_right, _text_input_right_edit) = build_input_text(&mut commands, &font, false, 50.0); @@ -163,7 +163,7 @@ fn build_input_text( }, TextCursorStyle { color: RED.into(), - selection_color: GREEN.into(), + selection_color: Color::from(GREEN).with_alpha(0.5), selected_text_color: Some(BLUE.into()), }, UiTransform::from_translation(Val2 { From ae3cc93fa8e0229dd43dbf44f20a359451cc3515 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Thu, 12 Mar 2026 14:27:38 +0800 Subject: [PATCH 22/40] Try without fontconfig --- .github/actions/install-linux-deps/action.yml | 2 +- crates/bevy_ui_widgets/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml index bad9de31d6193..10f2b2fd609f7 100644 --- a/.github/actions/install-linux-deps/action.yml +++ b/.github/actions/install-linux-deps/action.yml @@ -40,7 +40,7 @@ inputs: fontconfig: description: Install fontconfig (libfontconfig1-dev) required: false - default: "true" + default: "false" runs: using: composite steps: diff --git a/crates/bevy_ui_widgets/Cargo.toml b/crates/bevy_ui_widgets/Cargo.toml index ea534d81820c3..37f42608109f7 100644 --- a/crates/bevy_ui_widgets/Cargo.toml +++ b/crates/bevy_ui_widgets/Cargo.toml @@ -25,8 +25,8 @@ bevy_text = { path = "../bevy_text", version = "0.19.0-dev" } # other accesskit = "0.24" -parley = "0.7" # Must match bevy_text's parley version -smol_str = "0.2" # Must match winit's smol_str version +parley = { version = "0.7", default-features = false } +smol_str = "0.2" [features] default = [] From 19459c36c670f7fecf1983e7abea9fd7639aff62 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 14 Mar 2026 19:45:44 +0800 Subject: [PATCH 23/40] Partial parley-input --- crates/bevy_text/src/pipeline.rs | 12 +- crates/bevy_text/src/text.rs | 3 +- crates/bevy_ui/src/lib.rs | 1 + crates/bevy_ui/src/widget/mod.rs | 2 + crates/bevy_ui/src/widget/text_field.rs | 186 ++++++++++++++++++++++++ crates/bevy_ui_render/src/lib.rs | 105 ++++++++++++- 6 files changed, 298 insertions(+), 11 deletions(-) create mode 100644 crates/bevy_ui/src/widget/text_field.rs diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index d03be3f5386ed..ab0c177e2843c 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -178,10 +178,7 @@ impl TextPipeline { range.clone(), ); builder.push(StyleProperty::FontSize(font_size), range.clone()); - builder.push( - StyleProperty::LineHeight(line_height.eval(font_size)), - range.clone(), - ); + builder.push(StyleProperty::LineHeight(line_height.eval()), range.clone()); builder.push( StyleProperty::FontWeight(text_font.weight.into()), range.clone(), @@ -405,14 +402,15 @@ fn bounding_box_to_rect(geom: BoundingBox) -> Rect { } } -fn resolve_font_source<'a>( +/// resolve a font source +pub fn resolve_font_source<'a>( font: &'a FontSource, - fonts: &'a Assets, + fonts: &Assets, ) -> Result, TextError> { Ok(match font { FontSource::Handle(handle) => { let font = fonts.get(handle.id()).ok_or(TextError::NoSuchFont)?; - FontFamily::Named(Cow::Borrowed(font.family_name.as_str())) + FontFamily::Named(Cow::Owned(font.family_name.as_str().to_owned())) } FontSource::Family(family) => FontFamily::Named(Cow::Borrowed(family.as_str())), FontSource::Serif => FontFamily::Generic(parley::GenericFamily::Serif), diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index 9874573fa1aa9..0666d35e8bcec 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -926,7 +926,8 @@ pub enum LineHeight { } impl LineHeight { - pub(crate) fn eval(self, _font_size: f32) -> parley::LineHeight { + /// eval a line height + pub fn eval(self) -> parley::LineHeight { match self { LineHeight::Px(px) => parley::LineHeight::Absolute(px), LineHeight::RelativeToFont(scale) => parley::LineHeight::FontSizeRelative(scale), diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 54d7aa63ac2e6..459796d9b3b48 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -248,6 +248,7 @@ fn build_text_interop(app: &mut App) { // 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_editor_system.in_set(UiSystems::PostLayout), ), ); diff --git a/crates/bevy_ui/src/widget/mod.rs b/crates/bevy_ui/src/widget/mod.rs index bbd319e986f09..f582d27113214 100644 --- a/crates/bevy_ui/src/widget/mod.rs +++ b/crates/bevy_ui/src/widget/mod.rs @@ -4,10 +4,12 @@ mod button; mod image; mod label; mod text; +mod text_field; mod viewport; pub use button::*; pub use image::*; pub use label::*; pub use text::*; +pub use text_field::*; pub use viewport::*; diff --git a/crates/bevy_ui/src/widget/text_field.rs b/crates/bevy_ui/src/widget/text_field.rs new file mode 100644 index 0000000000000..4defbd918f0b8 --- /dev/null +++ b/crates/bevy_ui/src/widget/text_field.rs @@ -0,0 +1,186 @@ +use std::hash::BuildHasher; + +use crate::{ComputedNode, ComputedUiRenderTargetInfo, ContentSize, Node}; +use bevy_asset::Assets; + +use bevy_ecs::component::Component; +use bevy_ecs::lifecycle::HookContext; +use bevy_ecs::observer::{Observer, On}; +use bevy_ecs::resource::Resource; +use bevy_ecs::world::DeferredWorld; +use bevy_ecs::{ + change_detection::DetectChanges, + system::{Query, Res, ResMut}, + world::Ref, +}; +use bevy_image::prelude::*; +use bevy_input::keyboard::{Key, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; +use bevy_math::{Rect, Vec2}; +use bevy_platform::hash::FixedHasher; +use bevy_text::*; +use bevy_text::{ + add_glyph_to_atlas, get_glyph_atlas_info, ComputedTextBlock, FontAtlasKey, FontAtlasSet, + FontCx, GlyphCacheKey, LayoutCx, LineHeight, RunGeometry, ScaleCx, TextColor, TextFont, + TextLayoutInfo, +}; +use parley::swash::FontRef; +use parley::{FontFamily, FontStack, PlainEditor, PositionedLayoutItem}; + + +pub fn update_editor_system( + fonts: Res>, + mut font_cx: ResMut, + mut layout_cx: ResMut, + mut scale_cx: ResMut, + mut font_atlas_set: ResMut, + mut textures: ResMut>, + mut input_field_query: Query<( + &TextFont, + &LineHeight, + &FontHinting, + Ref, + &mut TextEditor, + &mut TextInput, + &mut TextLayoutInfo, + Ref, + )>, +) { + for ( + text_font, + line_height, + hinting, + target, + mut editor, + text_field, + mut info, + computed_node, + ) in input_field_query.iter_mut() + { + let Ok(font_family) = resolve_font_source(&text_font.font, fonts.as_ref()) else { + continue; + }; + + let family = match font_family { + FontFamily::Named(name) => FontFamily::Named(name.into_owned().into()), + FontFamily::Generic(generic) => FontFamily::Generic(generic), + }; + let style_set = editor.editor.edit_styles(); + style_set.insert(parley::StyleProperty::LineHeight(line_height.eval())); + style_set.insert(parley::StyleProperty::FontStack(FontStack::Single(family))); + + if text_field.is_changed() { + editor.editor.set_text(text_field.0.as_str()); + } + if target.is_changed() { + editor.editor.set_scale(target.scale_factor()); + } + + if computed_node.is_changed() { + editor.editor.set_width(Some(computed_node.size().x)); + } + + let mut driver = editor.editor.driver(&mut font_cx.0, &mut layout_cx.0); + + driver.refresh_layout(); + + let layout = driver.layout(); + + info.scale_factor = layout.scale(); + info.size = ( + layout.width() / layout.scale(), + layout.height() / layout.scale(), + ) + .into(); + + info.glyphs.clear(); + info.run_geometry.clear(); + + for line in layout.lines() { + for (line_index, item) in line.items().enumerate() { + match item { + PositionedLayoutItem::GlyphRun(glyph_run) => { + let (span_index, smoothing) = glyph_run.style().brush; + + let run = glyph_run.run(); + + let font_data = run.font(); + let font_size = run.font_size(); + let coords = run.normalized_coords(); + + let font_atlas_key = FontAtlasKey { + id: font_data.data.id() as u32, + index: font_data.index, + font_size_bits: font_size.to_bits(), + variations_hash: FixedHasher.hash_one(coords), + hinting: *hinting, + font_smoothing: smoothing, + }; + + for glyph in glyph_run.positioned_glyphs() { + let font_atlases = font_atlas_set.entry(font_atlas_key).or_default(); + let Ok(atlas_info) = get_glyph_atlas_info( + font_atlases, + GlyphCacheKey { + glyph_id: glyph.id as u16, + }, + ) + .map(Ok) + .unwrap_or_else(|| { + let font_ref = FontRef::from_index( + font_data.data.as_ref(), + font_data.index as usize, + ) + .unwrap(); + let mut scaler = scale_cx + .builder(font_ref) + .size(font_size) + .hint(true) + .normalized_coords(coords) + .build(); + add_glyph_to_atlas( + font_atlases, + textures.as_mut(), + &mut scaler, + text_font.font_smoothing, + glyph.id as u16, + ) + }) else { + continue; + }; + + info.glyphs.push(PositionedGlyph { + position: Vec2::new(glyph.x, glyph.y) + + atlas_info.rect.size() / 2. + + atlas_info.offset, + atlas_info, + span_index: span_index as usize, + line_index, + byte_index: line.text_range().start, + byte_length: line.text_range().len(), + }); + } + + info.run_geometry.push(RunGeometry { + span_index: span_index as usize, + bounds: Rect { + min: Vec2::new(glyph_run.offset(), line.metrics().min_coord), + max: Vec2::new( + glyph_run.offset() + glyph_run.advance(), + line.metrics().max_coord, + ), + }, + strikethrough_y: glyph_run.baseline() + - run.metrics().strikethrough_offset, + strikethrough_thickness: run.metrics().strikethrough_size, + underline_y: glyph_run.baseline() - run.metrics().underline_offset, + underline_thickness: run.metrics().underline_size, + }); + } + _ => {} + } + } + } + } +} diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index e27bc38b77b41..0f33c75323d83 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -26,7 +26,7 @@ use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_shader::load_shader_library; use bevy_sprite_render::SpriteAssetEvents; -use bevy_ui::widget::{ImageNode, TextShadow, ViewportNode}; +use bevy_ui::widget::{ImageNode, TextInput, TextShadow, ViewportNode}; use bevy_ui::{ BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedUiTargetCamera, Display, Node, OuterColor, Outline, ResolvedBorderRadius, UiGlobalTransform, @@ -245,6 +245,7 @@ impl Plugin for UiRenderPlugin { extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), extract_text_sections.in_set(RenderUiSystems::ExtractText), extract_text_cursor.in_set(RenderUiSystems::ExtractCursor), + extract_text_fields.in_set(RenderUiSystems::ExtractText), #[cfg(feature = "bevy_ui_debug")] debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), ), @@ -1076,7 +1077,13 @@ pub fn extract_text_shadows( } for run in text_layout_info.run_geometry.iter() { - let section_entity = computed_block.entities()[run.span_index].entity; + let Some(section_entity) = computed_block + .entities() + .get(run.span_index) + .map(|t| t.entity) + else { + continue; + }; let Ok((has_strikethrough, has_underline)) = text_decoration_query.get(section_entity) else { continue; @@ -1186,7 +1193,13 @@ pub fn extract_text_decorations( Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); for run in text_layout_info.run_geometry.iter() { - let section_entity = computed_block.entities()[run.span_index].entity; + let Some(section_entity) = computed_block + .entities() + .get(run.span_index) + .map(|t| t.entity) + else { + continue; + }; let Ok(( (text_background_color, maybe_strikethrough, maybe_underline), text_color, @@ -1285,6 +1298,92 @@ pub fn extract_text_decorations( } } +pub fn extract_text_fields( + mut commands: Commands, + mut extracted_uinodes: ResMut, + uinode_query: Extract< + Query< + ( + Entity, + &ComputedNode, + &UiGlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedUiTargetCamera, + &TextColor, + &TextLayoutInfo, + ), + With, + >, + >, + camera_map: Extract, +) { + let mut start = extracted_uinodes.glyphs.len(); + let mut end = start + 1; + + let mut camera_mapper = camera_map.get_mapper(); + for ( + entity, + uinode, + transform, + inherited_visibility, + clip, + camera, + text_color, + text_layout_info, + ) in &uinode_query + { + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + if !inherited_visibility.get() || uinode.is_empty() { + continue; + } + + let Some(extracted_camera_entity) = camera_mapper.map(camera) else { + continue; + }; + + let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); + + let color = text_color.0.to_linear(); + + for ( + i, + PositionedGlyph { + position, + atlas_info, + .. + }, + ) in text_layout_info.glyphs.iter().enumerate() + { + extracted_uinodes.glyphs.push(ExtractedGlyph { + color, + translation: *position, + rect: atlas_info.rect, + }); + + if text_layout_info + .glyphs + .get(i + 1) + .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) + { + extracted_uinodes.uinodes.push(ExtractedUiNode { + z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, + render_entity: commands.spawn(TemporaryRenderEntity).id(), + image: atlas_info.texture, + clip: clip.map(|clip| clip.clip), + extracted_camera_entity, + item: ExtractedUiItem::Glyphs { range: start..end }, + main_entity: entity.into(), + transform, + }); + start = end; + } + + end += 1; + } + } +} + #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct UiVertex { From a1fe7565d41c2ff0401c48b7ea18ae2e216f43de Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Sun, 15 Mar 2026 10:43:43 +0800 Subject: [PATCH 24/40] Remove unneeded --- crates/bevy_sprite/src/text2d.rs | 9 +- crates/bevy_text/src/lib.rs | 1 - crates/bevy_text/src/pipeline.rs | 33 +--- crates/bevy_text/src/text_editable.rs | 22 +-- crates/bevy_ui/src/lib.rs | 5 +- crates/bevy_ui/src/picking_backend.rs | 8 +- crates/bevy_ui/src/widget/text.rs | 212 +----------------------- crates/bevy_ui/src/widget/text_field.rs | 74 +++++---- crates/bevy_ui_render/src/cursor.rs | 3 +- crates/bevy_ui_render/src/lib.rs | 11 +- 10 files changed, 63 insertions(+), 315 deletions(-) diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 2b3367f771307..b2e06af9a3a75 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -22,9 +22,9 @@ use bevy_image::prelude::*; use bevy_math::{FloatOrd, Vec2, Vec3}; use bevy_reflect::{prelude::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, EditableText, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, - LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, - TextLayoutInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, + ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, LineHeight, + RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, + TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, }; use bevy_transform::components::Transform; use bevy_window::{PrimaryWindow, Window}; @@ -183,7 +183,6 @@ pub fn update_text2d_layout( Ref, &mut TextLayoutInfo, &mut ComputedTextBlock, - Option<&mut EditableText>, Ref, )>, mut text_reader: Text2dReader, @@ -226,7 +225,6 @@ pub fn update_text2d_layout( bounds, mut text_layout_info, mut computed, - mut maybe_editable_text, hinting, ) in &mut text_query { @@ -314,7 +312,6 @@ pub fn update_text2d_layout( &mut textures, &mut computed, &mut scale_cx, - &mut maybe_editable_text, text_bounds, block.justify, *hinting, diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 3daa0841032c8..942b42a2b3115 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -116,7 +116,6 @@ impl Plugin for TextPlugin { .chain(), ) .add_systems(Last, trim_source_cache) - .add_systems(PreUpdate, edit_to_computed) .add_systems(PostUpdate, apply_text_edits.in_set(EditableTextSystems)); #[cfg(feature = "default_font")] diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index ab0c177e2843c..8c43f383a05da 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,5 +1,4 @@ use alloc::borrow::Cow; -use bevy_ecs::world::Mut; use core::hash::BuildHasher; @@ -16,12 +15,11 @@ use bevy_platform::hash::FixedHasher; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use parley::style::{OverflowWrap, TextWrapMode}; use parley::{ - Alignment, AlignmentOptions, BoundingBox, FontFamily, FontStack, Layout, PositionedLayoutItem, + Alignment, AlignmentOptions, FontFamily, FontStack, Layout, PositionedLayoutItem, StyleProperty, WordBreakStrength, }; use swash::FontRef; -use crate::EditableText; use crate::{ add_glyph_to_atlas, error::TextError, @@ -266,7 +264,6 @@ impl TextPipeline { textures: &mut Assets, computed: &mut ComputedTextBlock, scale_cx: &mut ScaleCx, - maybe_editable_text: &mut Option>, bounds: TextBounds, justify: Justify, hinting: FontHinting, @@ -370,38 +367,10 @@ impl TextPipeline { layout_info.size = Vec2::new(layout.full_width(), layout.height()).ceil(); - if let Some(editable_text) = maybe_editable_text { - let geom = editable_text - .editor - .cursor_geometry(editable_text.cursor_width); - - layout_info.cursor = geom.map(bounding_box_to_rect); - - layout_info.selection_rects = editable_text - .editor - .selection_geometry() - .iter() - .map(|&b| bounding_box_to_rect(b.0)) - .collect(); - } - Ok(()) } } -fn bounding_box_to_rect(geom: BoundingBox) -> Rect { - Rect { - min: Vec2 { - x: geom.x0 as f32, - y: geom.y0 as f32, - }, - max: Vec2 { - x: geom.x1 as f32, - y: geom.y1 as f32, - }, - } -} - /// resolve a font source pub fn resolve_font_source<'a>( font: &'a FontSource, diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 02c29386f6e3e..9f6debc34278d 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -73,8 +73,8 @@ use alloc::collections::VecDeque; use crate::{ - apply_edit, text_edit::TextEdit, ComputedTextBlock, FontCx, FontHinting, FontSmoothing, - LayoutCx, LineHeight, TextColor, TextFont, TextLayout, + apply_edit, text_edit::TextEdit, FontCx, FontHinting, FontSmoothing, LayoutCx, LineHeight, + TextColor, TextFont, TextLayout, }; use bevy_ecs::prelude::*; use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; @@ -196,21 +196,3 @@ pub fn apply_text_edits( editable_text.apply_pending_edits(&mut font_context.0, &mut layout_context.0); } } - -/// Copies the layout from the [`parley::PlainEditor`] to the [`ComputedTextBlock`] -pub fn edit_to_computed( - mut query: Query<(&mut EditableText, &mut ComputedTextBlock)>, - mut font_context: ResMut, - mut layout_context: ResMut, -) { - // TODO: optimize with change detection - - for (mut editable_text, mut computed) in query.iter_mut() { - let editor = editable_text.editor_mut(); - - let layout = editor.layout(&mut font_context.0, &mut layout_context.0); - - computed.layout = layout.clone(); - computed.needs_rerender = true; - } -} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 459796d9b3b48..b9483d49d59d9 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -81,8 +81,6 @@ use stack::ui_stack_system; pub use stack::UiStack; use update::{propagate_ui_target_cameras, update_clipping_system}; -use crate::widget::measure_editable_text_system; - /// The basic plugin for Bevy UI #[derive(Default)] pub struct UiPlugin; @@ -194,7 +192,6 @@ impl Plugin for UiPlugin { ui_layout_system_config, ui_stack_system .in_set(UiSystems::Stack) - .after(measure_editable_text_system) // These systems don't care about stack index .ambiguous_with(widget::measure_text_system) .ambiguous_with(update_clipping_system) @@ -228,7 +225,7 @@ fn build_text_interop(app: &mut App) { app.add_systems( PostUpdate, ( - (widget::measure_text_system, measure_editable_text_system) + widget::measure_text_system .chain() .after(detect_text_needs_rerender) .after(bevy_text::load_font_assets_into_font_collection) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 7da0034fb29ab..49c186207db02 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -76,12 +76,8 @@ impl Default for UiPickingSettings { pub struct UiPickingPlugin; impl Plugin for UiPickingPlugin { fn build(&self, app: &mut App) { - app.init_resource::().add_systems( - PreUpdate, - ui_picking - .in_set(PickingSystems::Backend) - .after(bevy_text::edit_to_computed), - ); + app.init_resource::() + .add_systems(PreUpdate, ui_picking.in_set(PickingSystems::Backend)); } } diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 417a07ac6ba00..da324e15425b4 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -12,21 +12,18 @@ use bevy_ecs::{ query::With, reflect::ReflectComponent, system::{Query, Res, ResMut}, - world::{Mut, Ref}, + world::Ref, }; use bevy_image::prelude::*; use bevy_log::warn_once; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, EditableText, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, - LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, - TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, - TextWriter, + ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LineBreak, LineHeight, + RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, + TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, }; extern crate alloc; -use alloc::borrow::Cow; -use parley::{FontStack, StyleProperty}; use taffy::style::AvailableSpace; use tracing::error; @@ -328,194 +325,6 @@ pub fn measure_text_system( } } -/// [`measure_text_system`] iterates over [`TextSpan`](bevy_text::TextSpan). -/// An [`EditableText`] is a single instance, so we use this helper struct. -pub struct EditableTextAsSpan<'a> { - item: Option<(Entity, usize, &'a str, &'a TextFont, Color, LineHeight)>, -} - -impl<'a> Iterator for EditableTextAsSpan<'a> { - type Item = (Entity, usize, &'a str, &'a TextFont, Color, LineHeight); - - fn next(&mut self) -> Option { - self.item.take() - } -} - -impl<'a> EditableTextAsSpan<'a> { - pub fn new( - entity: Entity, - text: &'a str, - text_font: &'a TextFont, - text_color: TextColor, - line_height: LineHeight, - ) -> Self { - Self { - item: Some((entity, 1, text, text_font, text_color.0, line_height)), - } - } -} - -/// Analogous to [`measure_text_system`] but for a single [`EditableText`] -pub fn measure_editable_text_system( - fonts: Res>, - mut text_query: Query< - ( - Entity, - &mut EditableText, - Ref, - &mut ContentSize, - &mut TextNodeFlags, - &mut ComputedTextBlock, - Ref, - &ComputedNode, - Ref, - Ref, - Ref, - ), - With, - >, - mut text_pipeline: ResMut, - mut font_system: ResMut, - mut layout_cx: ResMut, - rem_size: Res, -) { - for ( - entity, - mut text, - block, - mut content_size, - mut text_flags, - mut computed, - computed_target, - computed_node, - text_font, - text_color, - line_height, - ) in &mut text_query - { - // Note: the ComputedTextBlock::needs_rerender bool is cleared in create_text_measure(). - // 1e-5 epsilon to ignore tiny scale factor float errors - if !(1e-5 - < (computed_target.scale_factor() - computed_node.inverse_scale_factor.recip()).abs() - || computed.needs_rerender(computed_target.is_changed(), rem_size.is_changed()) - || text.is_changed() - || text_flags.needs_measure_fn - || content_size.is_added()) - { - continue; - } - - let logical_viewport_size = computed_target.logical_size(); - - let (font_changes_required, font_size) = - check_style(&mut text, logical_viewport_size, &text_font, &rem_size); - if font_changes_required { - let editor = text.editor_mut(); - - editor.set_scale(computed_target.scale_factor); - text.cursor_width = font_size * CURSOR_SCALE; - - text_flags.needs_measure_fn = true; - - continue; - } - - let t = text.value().to_string(); - let text_spans = EditableTextAsSpan::new(entity, &t, &text_font, *text_color, *line_height); - - match text_pipeline.create_text_measure( - entity, - fonts.as_ref(), - text_spans, - computed_target.scale_factor, - &block, - computed.as_mut(), - &mut font_system, - &mut layout_cx, - logical_viewport_size, - rem_size.0, - ) { - Ok(measure) => { - if block.linebreak == LineBreak::NoWrap { - content_size.set(NodeMeasure::Fixed(FixedMeasure { size: measure.max })); - } else { - content_size.set(NodeMeasure::Text(TextMeasure { info: measure })); - } - - // Text measure func created successfully, so set `TextNodeFlags` to schedule a recompute - text_flags.needs_measure_fn = false; - text_flags.needs_recompute = true; - } - Err( - TextError::NoSuchFont - | TextError::NoSuchFontFamily(_) - | TextError::DegenerateScaleFactor, - ) => { - // Try again next frame - text_flags.needs_measure_fn = true; - } - Err( - e @ (TextError::FailedToAddGlyph(_) - | TextError::FailedToGetGlyphImage(_) - | TextError::MissingAtlasLayout - | TextError::MissingAtlasTexture - | TextError::InconsistentAtlasState), - ) => { - panic!("Fatal error when processing text: {e}."); - } - }; - } -} - -// TODO: this factor is a guess -const CURSOR_SCALE: f32 = 0.28; - -fn check_style<'a>( - text: &mut Mut<'a, EditableText>, - logical_viewport_size: Vec2, - text_font: &Ref, - rem_size: &Res, -) -> (bool, f32) { - let font_size = text_font.font_size.eval(logical_viewport_size, rem_size.0); - - // NOTE: this is messy as the editor doesn't expose much to read - - let editor = text.editor_mut(); - let styles = editor.edit_styles(); - - let target_font_size_style = StyleProperty::FontSize(font_size); - - let dis = core::mem::discriminant(&target_font_size_style); - - let maybe_current_font_size = styles.inner().get(&dis); - - let mut font_changes_required = false; - - if let Some(current_font_size) = maybe_current_font_size { - if let StyleProperty::FontSize(x) = current_font_size - && (x - font_size).abs() > 1e-5 - { - font_changes_required = true; - } - } else { - font_changes_required = true; - } - - if font_changes_required { - styles.insert(target_font_size_style); - - // TODO: don't hardcode this - // let family = font_system.get_family(&text_font.font); - // Above returns None, as its a Handle ? - styles.insert(StyleProperty::FontStack(FontStack::Source(Cow::Borrowed( - "monospace", - )))); - } - - (font_changes_required, font_size) -} - /// Updates the layout and size information for a UI text node on changes to the size value of its [`Node`] component, /// or when the `needs_recompute` field of [`TextNodeFlags`] is set to true. /// This information is computed by the [`TextPipeline`] and then stored in [`TextLayoutInfo`]. @@ -534,20 +343,12 @@ pub fn text_system( &mut TextLayoutInfo, &mut TextNodeFlags, &mut ComputedTextBlock, - Option<&mut EditableText>, Ref, )>, mut scale_cx: ResMut, ) { - for ( - node, - block, - mut text_layout_info, - mut text_flags, - mut computed, - mut maybe_editable_text, - hinting, - ) in &mut text_query + for (node, block, mut text_layout_info, mut text_flags, mut computed, hinting) in + &mut text_query { if node.is_changed() || text_flags.needs_recompute || hinting.is_changed() { // Skip the text node if it is waiting for a new measure func @@ -569,7 +370,6 @@ pub fn text_system( &mut textures, &mut computed, &mut scale_cx, - &mut maybe_editable_text, physical_node_size, block.justify, *hinting, diff --git a/crates/bevy_ui/src/widget/text_field.rs b/crates/bevy_ui/src/widget/text_field.rs index 4defbd918f0b8..1dfd14cde9ee8 100644 --- a/crates/bevy_ui/src/widget/text_field.rs +++ b/crates/bevy_ui/src/widget/text_field.rs @@ -1,33 +1,23 @@ use std::hash::BuildHasher; -use crate::{ComputedNode, ComputedUiRenderTargetInfo, ContentSize, Node}; +use crate::{ComputedNode, ComputedUiRenderTargetInfo}; use bevy_asset::Assets; -use bevy_ecs::component::Component; -use bevy_ecs::lifecycle::HookContext; -use bevy_ecs::observer::{Observer, On}; -use bevy_ecs::resource::Resource; -use bevy_ecs::world::DeferredWorld; use bevy_ecs::{ change_detection::DetectChanges, system::{Query, Res, ResMut}, world::Ref, }; use bevy_image::prelude::*; -use bevy_input::keyboard::{Key, KeyboardInput}; -use bevy_input::ButtonState; -use bevy_input_focus::FocusedInput; use bevy_math::{Rect, Vec2}; use bevy_platform::hash::FixedHasher; use bevy_text::*; use bevy_text::{ - add_glyph_to_atlas, get_glyph_atlas_info, ComputedTextBlock, FontAtlasKey, FontAtlasSet, - FontCx, GlyphCacheKey, LayoutCx, LineHeight, RunGeometry, ScaleCx, TextColor, TextFont, - TextLayoutInfo, + add_glyph_to_atlas, get_glyph_atlas_info, FontAtlasKey, FontAtlasSet, FontCx, GlyphCacheKey, + LayoutCx, LineHeight, RunGeometry, ScaleCx, TextFont, TextLayoutInfo, }; -use parley::swash::FontRef; -use parley::{FontFamily, FontStack, PlainEditor, PositionedLayoutItem}; - +use parley::{swash::FontRef, BoundingBox}; +use parley::{FontFamily, FontStack, PositionedLayoutItem}; pub fn update_editor_system( fonts: Res>, @@ -41,22 +31,13 @@ pub fn update_editor_system( &LineHeight, &FontHinting, Ref, - &mut TextEditor, - &mut TextInput, + &mut EditableText, &mut TextLayoutInfo, Ref, )>, ) { - for ( - text_font, - line_height, - hinting, - target, - mut editor, - text_field, - mut info, - computed_node, - ) in input_field_query.iter_mut() + for (text_font, line_height, hinting, target, mut editable_text, mut info, computed_node) in + input_field_query.iter_mut() { let Ok(font_family) = resolve_font_source(&text_font.font, fonts.as_ref()) else { continue; @@ -66,22 +47,21 @@ pub fn update_editor_system( FontFamily::Named(name) => FontFamily::Named(name.into_owned().into()), FontFamily::Generic(generic) => FontFamily::Generic(generic), }; - let style_set = editor.editor.edit_styles(); + let style_set = editable_text.editor.edit_styles(); style_set.insert(parley::StyleProperty::LineHeight(line_height.eval())); style_set.insert(parley::StyleProperty::FontStack(FontStack::Single(family))); - if text_field.is_changed() { - editor.editor.set_text(text_field.0.as_str()); - } if target.is_changed() { - editor.editor.set_scale(target.scale_factor()); + editable_text.editor.set_scale(target.scale_factor()); } if computed_node.is_changed() { - editor.editor.set_width(Some(computed_node.size().x)); + editable_text.editor.set_width(Some(computed_node.size().x)); } - let mut driver = editor.editor.driver(&mut font_cx.0, &mut layout_cx.0); + let mut driver = editable_text + .editor + .driver(&mut font_cx.0, &mut layout_cx.0); driver.refresh_layout(); @@ -182,5 +162,31 @@ pub fn update_editor_system( } } } + + let geom = editable_text + .editor + .cursor_geometry(editable_text.cursor_width); + + info.cursor = geom.map(bounding_box_to_rect); + + info.selection_rects = editable_text + .editor + .selection_geometry() + .iter() + .map(|&b| bounding_box_to_rect(b.0)) + .collect(); + } +} + +fn bounding_box_to_rect(geom: BoundingBox) -> Rect { + Rect { + min: Vec2 { + x: geom.x0 as f32, + y: geom.y0 as f32, + }, + max: Vec2 { + x: geom.x1 as f32, + y: geom.y1 as f32, + }, } } diff --git a/crates/bevy_ui_render/src/cursor.rs b/crates/bevy_ui_render/src/cursor.rs index 1b2276e6cd743..1a0972a75dc55 100644 --- a/crates/bevy_ui_render/src/cursor.rs +++ b/crates/bevy_ui_render/src/cursor.rs @@ -45,7 +45,8 @@ pub fn extract_text_cursor( ) in text_node_query.iter() { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) - if !inherited_visibility.get() || uinode.is_empty() { + if !inherited_visibility.get() { + // || uinode.is_empty() { continue; } diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 0f33c75323d83..c9c321baf736e 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -26,7 +26,7 @@ use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_shader::load_shader_library; use bevy_sprite_render::SpriteAssetEvents; -use bevy_ui::widget::{ImageNode, TextInput, TextShadow, ViewportNode}; +use bevy_ui::widget::{ImageNode, TextShadow, ViewportNode}; use bevy_ui::{ BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedUiTargetCamera, Display, Node, OuterColor, Outline, ResolvedBorderRadius, UiGlobalTransform, @@ -245,7 +245,7 @@ impl Plugin for UiRenderPlugin { extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), extract_text_sections.in_set(RenderUiSystems::ExtractText), extract_text_cursor.in_set(RenderUiSystems::ExtractCursor), - extract_text_fields.in_set(RenderUiSystems::ExtractText), + extract_text_editable.in_set(RenderUiSystems::ExtractText), #[cfg(feature = "bevy_ui_debug")] debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), ), @@ -1298,7 +1298,7 @@ pub fn extract_text_decorations( } } -pub fn extract_text_fields( +pub fn extract_text_editable( mut commands: Commands, mut extracted_uinodes: ResMut, uinode_query: Extract< @@ -1313,7 +1313,7 @@ pub fn extract_text_fields( &TextColor, &TextLayoutInfo, ), - With, + With, >, >, camera_map: Extract, @@ -1334,7 +1334,8 @@ pub fn extract_text_fields( ) in &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) - if !inherited_visibility.get() || uinode.is_empty() { + if !inherited_visibility.get() { + // || uinode.is_empty() { continue; } From 38c33636f91daf5e88317d572c6a41a94ffd264c Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Sun, 15 Mar 2026 14:09:58 +0800 Subject: [PATCH 25/40] Fixes --- crates/bevy_text/src/text_edit.rs | 3 ++- crates/bevy_ui/src/widget/text_field.rs | 24 +++++++++++++++++++++--- crates/bevy_ui_render/src/cursor.rs | 4 ++-- crates/bevy_ui_render/src/lib.rs | 5 +---- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index d2a5ef2555725..ce770abc26d7a 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -1,10 +1,11 @@ +use bevy_reflect::Reflect; use parley::PlainEditorDriver; use smol_str::SmolStr; use crate::FontSmoothing; /// Deferred text input edit and navigation actions applied by the `apply_text_edits` system. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Reflect)] pub enum TextEdit { /// Insert a character at the cursor. If there is a selection, replaces the selection with the character instead. /// diff --git a/crates/bevy_ui/src/widget/text_field.rs b/crates/bevy_ui/src/widget/text_field.rs index 1dfd14cde9ee8..27cd3620f580c 100644 --- a/crates/bevy_ui/src/widget/text_field.rs +++ b/crates/bevy_ui/src/widget/text_field.rs @@ -1,6 +1,6 @@ use std::hash::BuildHasher; -use crate::{ComputedNode, ComputedUiRenderTargetInfo}; +use crate::{ComputedNode, ComputedUiRenderTargetInfo, ContentSize, FixedMeasure, NodeMeasure}; use bevy_asset::Assets; use bevy_ecs::{ @@ -33,11 +33,21 @@ pub fn update_editor_system( Ref, &mut EditableText, &mut TextLayoutInfo, + &mut ContentSize, Ref, )>, + rem_size: Res, ) { - for (text_font, line_height, hinting, target, mut editable_text, mut info, computed_node) in - input_field_query.iter_mut() + for ( + text_font, + line_height, + hinting, + target, + mut editable_text, + mut info, + mut content_size, + computed_node, + ) in input_field_query.iter_mut() { let Ok(font_family) = resolve_font_source(&text_font.font, fonts.as_ref()) else { continue; @@ -51,6 +61,10 @@ pub fn update_editor_system( style_set.insert(parley::StyleProperty::LineHeight(line_height.eval())); style_set.insert(parley::StyleProperty::FontStack(FontStack::Single(family))); + let logical_viewport_size = target.logical_size(); + let font_size = text_font.font_size.eval(logical_viewport_size, rem_size.0); + style_set.insert(parley::StyleProperty::FontSize(font_size)); + if target.is_changed() { editable_text.editor.set_scale(target.scale_factor()); } @@ -74,6 +88,10 @@ pub fn update_editor_system( ) .into(); + content_size.set(NodeMeasure::Fixed(FixedMeasure { + size: info.size * target.scale_factor(), + })); + info.glyphs.clear(); info.run_geometry.clear(); diff --git a/crates/bevy_ui_render/src/cursor.rs b/crates/bevy_ui_render/src/cursor.rs index 1a0972a75dc55..347b29e250fc2 100644 --- a/crates/bevy_ui_render/src/cursor.rs +++ b/crates/bevy_ui_render/src/cursor.rs @@ -44,9 +44,9 @@ pub fn extract_text_cursor( cursor_style, ) in text_node_query.iter() { - // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + // Skip if not visible + // Note that `uinode.is_empty()` may be empty when there is no text, but we still want to display the cursor if !inherited_visibility.get() { - // || uinode.is_empty() { continue; } diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index c9c321baf736e..edf172da4d97a 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -896,7 +896,6 @@ pub fn extract_text_sections( uinode_query: Extract< Query<( Entity, - Option<&EditableText>, &ComputedNode, &UiGlobalTransform, &InheritedVisibility, @@ -916,7 +915,6 @@ pub fn extract_text_sections( let mut camera_mapper = camera_map.get_mapper(); for ( entity, - _maybe_et, uinode, transform, inherited_visibility, @@ -1334,8 +1332,7 @@ pub fn extract_text_editable( ) in &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) - if !inherited_visibility.get() { - // || uinode.is_empty() { + if !inherited_visibility.get() || uinode.is_empty() { continue; } From cd1c713af65994b0789c38742b80884343ab1835 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Sun, 15 Mar 2026 15:09:13 +0800 Subject: [PATCH 26/40] CI --- crates/bevy_text/src/text_editable.rs | 2 +- crates/bevy_ui/src/lib.rs | 6 +++++- crates/bevy_ui/src/widget/text_field.rs | 6 ++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 9f6debc34278d..1118ca2b7ebf4 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -95,7 +95,7 @@ use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; pub struct EditableText { /// A [`parley::PlainEditor`], tracking both the text content and cursor position. /// - /// This serves as an analogue to [`ComputedTextBlock`] for editable text. + /// This serves as an analogue to [`ComputedTextBlock`](crate::ComputedTextBlock) for editable text. /// /// In most cases, you should queue text edits via the [`EditableText::queue_edit`] method instead of directly manipulating the editor, /// and then allow the [`apply_text_edits`] system to apply the edits at the appropriate time in the update cycle. diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index b9483d49d59d9..7a3e6c50cec94 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -245,7 +245,11 @@ fn build_text_interop(app: &mut App) { // 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_editor_system.in_set(UiSystems::PostLayout), + widget::update_editor_system + .in_set(UiSystems::PostLayout) + .ambiguous_with(ui_stack_system) + .ambiguous_with(widget::text_system) + .ambiguous_with(bevy_sprite::calculate_bounds_text2d), ), ); diff --git a/crates/bevy_ui/src/widget/text_field.rs b/crates/bevy_ui/src/widget/text_field.rs index 27cd3620f580c..4d800dc2cba7c 100644 --- a/crates/bevy_ui/src/widget/text_field.rs +++ b/crates/bevy_ui/src/widget/text_field.rs @@ -1,4 +1,4 @@ -use std::hash::BuildHasher; +use core::hash::BuildHasher; use crate::{ComputedNode, ComputedUiRenderTargetInfo, ContentSize, FixedMeasure, NodeMeasure}; use bevy_asset::Assets; @@ -176,7 +176,9 @@ pub fn update_editor_system( underline_thickness: run.metrics().underline_size, }); } - _ => {} + PositionedLayoutItem::InlineBox(_inline) => { + // TODO: handle inline + } } } } From 3d62f8ae3f22993e7fc6500cb1d3d01d23e69f37 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Sun, 15 Mar 2026 16:24:20 +0800 Subject: [PATCH 27/40] Fix up cursor width --- crates/bevy_text/src/text_editable.rs | 4 ++-- crates/bevy_ui/src/widget/text_field.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 1118ca2b7ebf4..0d59c0508788c 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -112,7 +112,7 @@ pub struct EditableText { /// /// Analogous to [`ComputedTextBlock::needs_rerender`](crate::ComputedTextBlock::needs_rerender). pub(crate) needs_rerender: bool, - /// cursor width + /// cursor width, relative to font size pub cursor_width: f32, } @@ -123,7 +123,7 @@ impl Default for EditableText { editor: PlainEditor::new(100.), pending_edits: VecDeque::new(), needs_rerender: true, - cursor_width: 20.0, + cursor_width: 0.2, } } } diff --git a/crates/bevy_ui/src/widget/text_field.rs b/crates/bevy_ui/src/widget/text_field.rs index 4d800dc2cba7c..c9fc6c0ea6c3e 100644 --- a/crates/bevy_ui/src/widget/text_field.rs +++ b/crates/bevy_ui/src/widget/text_field.rs @@ -177,7 +177,7 @@ pub fn update_editor_system( }); } PositionedLayoutItem::InlineBox(_inline) => { - // TODO: handle inline + // TODO: handle inline boxes } } } @@ -185,7 +185,7 @@ pub fn update_editor_system( let geom = editable_text .editor - .cursor_geometry(editable_text.cursor_width); + .cursor_geometry(editable_text.cursor_width * font_size); info.cursor = geom.map(bounding_box_to_rect); From ad6cde0b722dd5848ee23118c01904d20618847c Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Sun, 15 Mar 2026 17:20:35 +0800 Subject: [PATCH 28/40] Clean up --- crates/bevy_text/src/cursor.rs | 2 -- crates/bevy_text/src/text_editable.rs | 10 +--------- crates/bevy_ui/src/lib.rs | 2 +- crates/bevy_ui/src/widget/mod.rs | 4 ++-- .../src/widget/{text_field.rs => text_editable.rs} | 5 ++++- crates/bevy_ui_render/src/lib.rs | 2 +- examples/ui/text/editable_text.rs | 3 +-- release-content/release-notes/text_input.md | 3 ++- 8 files changed, 12 insertions(+), 19 deletions(-) rename crates/bevy_ui/src/widget/{text_field.rs => text_editable.rs} (97%) diff --git a/crates/bevy_text/src/cursor.rs b/crates/bevy_text/src/cursor.rs index e6983e11894c2..f04a91a74110d 100644 --- a/crates/bevy_text/src/cursor.rs +++ b/crates/bevy_text/src/cursor.rs @@ -11,6 +11,4 @@ pub struct TextCursorStyle { pub color: Color, /// Background color of selected text pub selection_color: Color, - /// Color of text under selection - pub selected_text_color: Option, } diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 0d59c0508788c..34b0d28104fae 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -46,7 +46,6 @@ //! - Click to place cursor //! - Cursor blinking //! - Clipboard operations (copy, cut, paste) -//! - Text selection //! - Undo/redo functionality //! - Newline support for multi-line input //! - Input Method Editor (IME) support for complex scripts @@ -108,11 +107,7 @@ pub struct EditableText { /// /// These edits are processed in first-in, first-out order. pub pending_edits: VecDeque, - /// Does the contained text buffer need rerendering / relayout? - /// - /// Analogous to [`ComputedTextBlock::needs_rerender`](crate::ComputedTextBlock::needs_rerender). - pub(crate) needs_rerender: bool, - /// cursor width, relative to font size + /// Cursor width, relative to font size pub cursor_width: f32, } @@ -122,7 +117,6 @@ impl Default for EditableText { // Defaults selected to match `Text::default()` editor: PlainEditor::new(100.), pending_edits: VecDeque::new(), - needs_rerender: true, cursor_width: 0.2, } } @@ -169,7 +163,6 @@ impl EditableText { while let Some(edit) = pending_edits.pop_front() { driver = apply_edit(edit, driver); } - self.needs_rerender = true; } /// Clears the current input and resets the cursor position. @@ -182,7 +175,6 @@ impl EditableText { let mut driver = self.editor_mut().driver(font_context, layout_context); driver.move_to_byte(0); self.pending_edits.clear(); - self.needs_rerender = true; } } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 7a3e6c50cec94..7c187b62e6707 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -245,7 +245,7 @@ fn build_text_interop(app: &mut App) { // 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_editor_system + widget::editable_text_system .in_set(UiSystems::PostLayout) .ambiguous_with(ui_stack_system) .ambiguous_with(widget::text_system) diff --git a/crates/bevy_ui/src/widget/mod.rs b/crates/bevy_ui/src/widget/mod.rs index f582d27113214..04398a111fe78 100644 --- a/crates/bevy_ui/src/widget/mod.rs +++ b/crates/bevy_ui/src/widget/mod.rs @@ -4,12 +4,12 @@ mod button; mod image; mod label; mod text; -mod text_field; +mod text_editable; mod viewport; pub use button::*; pub use image::*; pub use label::*; pub use text::*; -pub use text_field::*; +pub use text_editable::*; pub use viewport::*; diff --git a/crates/bevy_ui/src/widget/text_field.rs b/crates/bevy_ui/src/widget/text_editable.rs similarity index 97% rename from crates/bevy_ui/src/widget/text_field.rs rename to crates/bevy_ui/src/widget/text_editable.rs index c9fc6c0ea6c3e..35b508c1c1371 100644 --- a/crates/bevy_ui/src/widget/text_field.rs +++ b/crates/bevy_ui/src/widget/text_editable.rs @@ -19,7 +19,10 @@ use bevy_text::{ use parley::{swash::FontRef, BoundingBox}; use parley::{FontFamily, FontStack, PositionedLayoutItem}; -pub fn update_editor_system( +/// Updates [`EditableText::editor`] to match e.g. [`TextFont`] +/// Writes layout to [`TextLayoutInfo`] for rendering +/// Adds required glyphs to the texture atlas +pub fn editable_text_system( fonts: Res>, mut font_cx: ResMut, mut layout_cx: ResMut, diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index edf172da4d97a..84ceecd57ec00 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -244,8 +244,8 @@ impl Plugin for UiRenderPlugin { extract_text_decorations.in_set(RenderUiSystems::ExtractTextBackgrounds), extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), extract_text_sections.in_set(RenderUiSystems::ExtractText), - extract_text_cursor.in_set(RenderUiSystems::ExtractCursor), extract_text_editable.in_set(RenderUiSystems::ExtractText), + extract_text_cursor.in_set(RenderUiSystems::ExtractCursor), #[cfg(feature = "bevy_ui_debug")] debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), ), diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index 2e4d74775e094..f7ece5c550d02 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -5,7 +5,7 @@ //! that includes e.g. a background, border, and text label. //! //! See the module documentation for [`editable_text`](bevy::ui_widgets::editable_text) for more details. -use bevy::color::palettes::css::{BLUE, GREEN, RED, YELLOW}; +use bevy::color::palettes::css::{GREEN, RED, YELLOW}; use bevy::input_focus::{InputDispatchPlugin, InputFocus}; use bevy::prelude::*; use bevy::text::{EditableText, FontCx, LayoutCx, TextCursorStyle}; @@ -164,7 +164,6 @@ fn build_input_text( TextCursorStyle { color: RED.into(), selection_color: Color::from(GREEN).with_alpha(0.5), - selected_text_color: Some(BLUE.into()), }, UiTransform::from_translation(Val2 { x: Val::Px(10.0), diff --git a/release-content/release-notes/text_input.md b/release-content/release-notes/text_input.md index 7cbc392965651..7213014dd8600 100644 --- a/release-content/release-notes/text_input.md +++ b/release-content/release-notes/text_input.md @@ -13,6 +13,7 @@ Our initial text entry supports: - Press keys on your keyboard, get text (wow!) - A cursor that can be moved forward and back in response to the left and right arrow keys +- Selection rectangles (hold shift) - Backspace and Delete - Unicode-aware navigation and editing: 1 byte/char != 1 character - Bidirectional text support, allowing both left-to-right and right-to-left scripts @@ -20,6 +21,6 @@ Our initial text entry supports: `EditableText` integrate with Bevy's `InputFocus` resource, accepting keyboard inputs only when the selected `EditableText` entity is focused. -Many important features are currently unimplemented (placeholder text, text selection, clipboard support, undo-redo...). +Many important features are currently unimplemented (placeholder text, clipboard support, undo-redo...). While we've been careful to expose and document the internals so that you can readily implement these features in your own projects, we would like to continue to expand the functionality of the base widget: please consider making a PR! From 09eed5117588474a3ac89e7f089ec234da6cb20f Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 17 Mar 2026 08:02:11 +0800 Subject: [PATCH 29/40] Apply suggestions from code review Co-authored-by: Alice Cecile --- crates/bevy_text/src/cursor.rs | 4 +++- crates/bevy_text/src/pipeline.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/cursor.rs b/crates/bevy_text/src/cursor.rs index f04a91a74110d..2cce78068391f 100644 --- a/crates/bevy_text/src/cursor.rs +++ b/crates/bevy_text/src/cursor.rs @@ -1,7 +1,9 @@ use bevy_color::Color; use bevy_ecs::component::Component; -/// When this component is alongside an [`EditableText`](`crate::EditableText`) , +/// Controls text cursor appearance. +/// +/// When this component on the same entity as an [`EditableText`](`crate::EditableText`) , /// and the [`UiRenderPlugin`](https://docs.rs/bevy/latest/bevy/ui_render/struct.UiRenderPlugin.html) /// is active, a simple rectangle will be drawn for the cursor. /// This is an optional component, to allow for stylistic cursors. diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 8c43f383a05da..fa9960b8cf1f5 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -371,7 +371,7 @@ impl TextPipeline { } } -/// resolve a font source +/// Resolve a [`FontSource`], producing a [`FontFamily`], by looking it up in the [`Assets`] collection. pub fn resolve_font_source<'a>( font: &'a FontSource, fonts: &Assets, From c1449b4e1d2926d7fd28a16d16d33b7a2b25fa7c Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 17 Mar 2026 09:14:16 +0800 Subject: [PATCH 30/40] Review --- crates/bevy_text/src/cursor.rs | 14 +++++++++++- crates/bevy_ui/src/lib.rs | 2 ++ crates/bevy_ui/src/widget/text.rs | 2 +- crates/bevy_ui/src/widget/text_editable.rs | 1 + crates/bevy_ui_render/src/lib.rs | 2 +- crates/bevy_ui_widgets/src/lib.rs | 1 + examples/ui/text/editable_text.rs | 25 +++++++++++++--------- 7 files changed, 34 insertions(+), 13 deletions(-) diff --git a/crates/bevy_text/src/cursor.rs b/crates/bevy_text/src/cursor.rs index 2cce78068391f..ef68689538e7e 100644 --- a/crates/bevy_text/src/cursor.rs +++ b/crates/bevy_text/src/cursor.rs @@ -1,4 +1,7 @@ -use bevy_color::Color; +use bevy_color::{ + palettes::css::{GREEN, RED}, + Alpha as _, Color, +}; use bevy_ecs::component::Component; /// Controls text cursor appearance. @@ -14,3 +17,12 @@ pub struct TextCursorStyle { /// Background color of selected text pub selection_color: Color, } + +impl Default for TextCursorStyle { + fn default() -> Self { + Self { + color: RED.into(), + selection_color: Color::from(GREEN).with_alpha(0.5), + } + } +} diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 7c187b62e6707..0420c8698fa25 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -10,6 +10,8 @@ //! Spawn UI elements with [`widget::Button`], [`ImageNode`](widget::ImageNode), [`Text`](prelude::Text) and [`Node`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) +extern crate alloc; + pub mod auto_directional_navigation; pub mod interaction_states; pub mod measurement; diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index da324e15425b4..1793761011254 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -23,7 +23,7 @@ use bevy_text::{ RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, TextWriter, }; -extern crate alloc; + use taffy::style::AvailableSpace; use tracing::error; diff --git a/crates/bevy_ui/src/widget/text_editable.rs b/crates/bevy_ui/src/widget/text_editable.rs index 35b508c1c1371..297f9df9c005d 100644 --- a/crates/bevy_ui/src/widget/text_editable.rs +++ b/crates/bevy_ui/src/widget/text_editable.rs @@ -22,6 +22,7 @@ use parley::{FontFamily, FontStack, PositionedLayoutItem}; /// Updates [`EditableText::editor`] to match e.g. [`TextFont`] /// Writes layout to [`TextLayoutInfo`] for rendering /// Adds required glyphs to the texture atlas +// TODO: add change detection logic here to improve performance pub fn editable_text_system( fonts: Res>, mut font_cx: ResMut, diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 84ceecd57ec00..c577b58199016 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -111,10 +111,10 @@ pub mod stack_z_offsets { pub const BORDER_GRADIENT: f32 = 0.03; pub const IMAGE: f32 = 0.04; pub const MATERIAL: f32 = 0.05; - pub const TEXT_SELECTION: f32 = 0.55; pub const TEXT: f32 = 0.06; pub const TEXT_CURSOR: f32 = 0.065; pub const TEXT_STRIKETHROUGH: f32 = 0.07; + pub const TEXT_SELECTION: f32 = 0.08; } #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] diff --git a/crates/bevy_ui_widgets/src/lib.rs b/crates/bevy_ui_widgets/src/lib.rs index 3f42039607c68..52d3d665f31ff 100644 --- a/crates/bevy_ui_widgets/src/lib.rs +++ b/crates/bevy_ui_widgets/src/lib.rs @@ -56,6 +56,7 @@ impl PluginGroup for UiWidgetsPlugins { .add(RadioGroupPlugin) .add(ScrollbarPlugin) .add(SliderPlugin) + .add(EditableTextInputPlugin) } } diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index f7ece5c550d02..9b869b44a1f39 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -6,7 +6,10 @@ //! //! See the module documentation for [`editable_text`](bevy::ui_widgets::editable_text) for more details. use bevy::color::palettes::css::{GREEN, RED, YELLOW}; -use bevy::input_focus::{InputDispatchPlugin, InputFocus}; +use bevy::input_focus::{ + tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, + InputDispatchPlugin, InputFocus, +}; use bevy::prelude::*; use bevy::text::{EditableText, FontCx, LayoutCx, TextCursorStyle}; use bevy::ui_widgets::EditableTextInputPlugin; @@ -19,9 +22,10 @@ fn main() { EditableTextInputPlugin, // Input focus is required to direct keyboard input to the correct EditableText InputDispatchPlugin, + TabNavigationPlugin, )) .add_systems(Startup, setup) - .add_systems(PreUpdate, rotate_tab) + // .add_systems(PreUpdate, rotate_tab) .add_systems(Update, text_submission) .run(); } @@ -80,10 +84,13 @@ fn setup( input_focus.set(text_input_left_edit); let input_container = commands - .spawn(Node { - display: Display::Flex, - ..default() - }) + .spawn(( + Node { + display: Display::Flex, + ..default() + }, + TabGroup::new(0), + )) .id(); // Set up a text output to see the result of our text input @@ -161,10 +168,8 @@ fn build_input_text( font_size: FontSize::Px(font_size), ..default() }, - TextCursorStyle { - color: RED.into(), - selection_color: Color::from(GREEN).with_alpha(0.5), - }, + TextCursorStyle::default(), + TabIndex(if is_left { 0 } else { 1 }), UiTransform::from_translation(Val2 { x: Val::Px(10.0), y: Val::Px(10.0), From c59943d7a4cf1f15e9bf3a9a255656d84482270e Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 17 Mar 2026 09:29:07 +0800 Subject: [PATCH 31/40] Use TextBrush now --- crates/bevy_text/src/text_edit.rs | 8 +++---- crates/bevy_text/src/text_editable.rs | 12 +++++----- crates/bevy_ui/src/widget/text_editable.rs | 8 +++---- examples/ui/text/editable_text.rs | 27 +--------------------- 4 files changed, 15 insertions(+), 40 deletions(-) diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index ce770abc26d7a..79e7006168236 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -2,9 +2,9 @@ use bevy_reflect::Reflect; use parley::PlainEditorDriver; use smol_str::SmolStr; -use crate::FontSmoothing; +use crate::TextBrush; -/// Deferred text input edit and navigation actions applied by the `apply_text_edits` system. +/// crate::{FontSmoothing, TextBrush}edit and navigation actions applied by the `apply_text_edits` system. #[derive(Debug, Clone, PartialEq, Eq, Reflect)] pub enum TextEdit { /// Insert a character at the cursor. If there is a selection, replaces the selection with the character instead. @@ -50,8 +50,8 @@ pub enum TextEdit { /// Takes a `TextEdit` and applies to `PlainEditorDriver` pub fn apply_edit<'a>( edit: TextEdit, - mut driver: PlainEditorDriver<'a, (u32, FontSmoothing)>, -) -> PlainEditorDriver<'a, (u32, FontSmoothing)> { + mut driver: PlainEditorDriver<'a, TextBrush>, +) -> PlainEditorDriver<'a, TextBrush> { match edit { TextEdit::Insert(str) => driver.insert_or_replace_selection(&str), TextEdit::Backspace => driver.backdelete(), diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 34b0d28104fae..276b4c767a9c3 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -72,7 +72,7 @@ use alloc::collections::VecDeque; use crate::{ - apply_edit, text_edit::TextEdit, FontCx, FontHinting, FontSmoothing, LayoutCx, LineHeight, + apply_edit, text_edit::TextEdit, FontCx, FontHinting, LayoutCx, LineHeight, TextBrush, TextColor, TextFont, TextLayout, }; use bevy_ecs::prelude::*; @@ -102,7 +102,7 @@ pub struct EditableText { /// Note that many more complex editing operations require working with [`PlainEditor::driver`]. /// These operations should generally be batched together to avoid redundant layout work. // The B: Brush generic here must match the brush used by `ComputedTextBlock` to ensure that the font system is compatible. - pub editor: PlainEditor<(u32, FontSmoothing)>, + pub editor: PlainEditor, /// Text edit actions that have been requested but not yet applied. /// /// These edits are processed in first-in, first-out order. @@ -124,13 +124,13 @@ impl Default for EditableText { impl EditableText { /// Access the internal [`PlainEditor`]. - pub fn editor(&self) -> &PlainEditor<(u32, FontSmoothing)> { + pub fn editor(&self) -> &PlainEditor { &self.editor } /// Mutably access the internal [`PlainEditor`]. /// - pub fn editor_mut(&mut self) -> &mut PlainEditor<(u32, FontSmoothing)> { + pub fn editor_mut(&mut self) -> &mut PlainEditor { &mut self.editor } @@ -152,7 +152,7 @@ impl EditableText { pub fn apply_pending_edits( &mut self, font_context: &mut FontContext, - layout_context: &mut LayoutContext<(u32, FontSmoothing)>, + layout_context: &mut LayoutContext, ) { // Take the `pending_edits` out of the struct so we can apply them without mutable aliasing issues. // We do not need to put the `pending_edits` back into the struct, @@ -169,7 +169,7 @@ impl EditableText { pub fn clear( &mut self, font_context: &mut FontContext, - layout_context: &mut LayoutContext<(u32, FontSmoothing)>, + layout_context: &mut LayoutContext, ) { self.editor.set_text(""); let mut driver = self.editor_mut().driver(font_context, layout_context); diff --git a/crates/bevy_ui/src/widget/text_editable.rs b/crates/bevy_ui/src/widget/text_editable.rs index 297f9df9c005d..bf94489081a8e 100644 --- a/crates/bevy_ui/src/widget/text_editable.rs +++ b/crates/bevy_ui/src/widget/text_editable.rs @@ -103,7 +103,7 @@ pub fn editable_text_system( for (line_index, item) in line.items().enumerate() { match item { PositionedLayoutItem::GlyphRun(glyph_run) => { - let (span_index, smoothing) = glyph_run.style().brush; + let brush = glyph_run.style().brush; let run = glyph_run.run(); @@ -117,7 +117,7 @@ pub fn editable_text_system( font_size_bits: font_size.to_bits(), variations_hash: FixedHasher.hash_one(coords), hinting: *hinting, - font_smoothing: smoothing, + font_smoothing: brush.font_smoothing, }; for glyph in glyph_run.positioned_glyphs() { @@ -157,7 +157,7 @@ pub fn editable_text_system( + atlas_info.rect.size() / 2. + atlas_info.offset, atlas_info, - span_index: span_index as usize, + section_index: brush.section_index as usize, line_index, byte_index: line.text_range().start, byte_length: line.text_range().len(), @@ -165,7 +165,7 @@ pub fn editable_text_system( } info.run_geometry.push(RunGeometry { - span_index: span_index as usize, + section_index: brush.section_index as usize, bounds: Rect { min: Vec2::new(glyph_run.offset(), line.metrics().min_coord), max: Vec2::new( diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index 9b869b44a1f39..be0e2f74d573b 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -5,7 +5,7 @@ //! that includes e.g. a background, border, and text label. //! //! See the module documentation for [`editable_text`](bevy::ui_widgets::editable_text) for more details. -use bevy::color::palettes::css::{GREEN, RED, YELLOW}; +use bevy::color::palettes::css::YELLOW; use bevy::input_focus::{ tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, InputDispatchPlugin, InputFocus, @@ -25,7 +25,6 @@ fn main() { TabNavigationPlugin, )) .add_systems(Startup, setup) - // .add_systems(PreUpdate, rotate_tab) .add_systems(Update, text_submission) .run(); } @@ -203,27 +202,3 @@ fn text_submission( text_input.clear(&mut font_context.0, &mut layout_context.0); } } - -fn rotate_tab( - mut input_focus: ResMut, - keyboard_input: Res>, - text_input: Query>, -) { - if keyboard_input.just_pressed(KeyCode::Tab) { - let focused_entity = input_focus.0; - - for entity in text_input.iter() { - if focused_entity.is_none() { - input_focus.0 = Some(entity); - return; - } - - if let Some(focused_entity) = focused_entity - && entity != focused_entity - { - input_focus.0 = Some(entity); - return; - } - } - } -} From 49d7294196a784e0e2fba12f4223f4aada71b569 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Tue, 17 Mar 2026 09:41:02 +0800 Subject: [PATCH 32/40] cat walked over my keyboard --- 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 79e7006168236..ee70a4f569d07 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -4,7 +4,7 @@ use smol_str::SmolStr; use crate::TextBrush; -/// crate::{FontSmoothing, TextBrush}edit and navigation actions applied by the `apply_text_edits` system. +/// Deferred text input edit and navigation actions applied by the `apply_text_edits` system. #[derive(Debug, Clone, PartialEq, Eq, Reflect)] pub enum TextEdit { /// Insert a character at the cursor. If there is a selection, replaces the selection with the character instead. From dd060e6c474a6869f4c9d4fb7683f13826dad1af Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 16 Mar 2026 22:33:22 -0400 Subject: [PATCH 33/40] Fix outdated instructions in example Co-authored-by: Alice Cecile --- examples/ui/text/editable_text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index be0e2f74d573b..bf2da6019f61a 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -60,7 +60,7 @@ fn setup( ..Default::default() }, BorderColor::from(Color::from(YELLOW)), - Text::new("Tab to change input, Ctrl+Enter to submit"), + Text::new("Ctrl+Enter to submit text"), TextFont { font: asset_server.load("fonts/FiraSans-Bold.ttf").into(), font_size: FontSize::Px(30.0), From 448b0e0e14f5dd228133fd1ef87f925f598c492f Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Fri, 20 Mar 2026 06:46:44 +0800 Subject: [PATCH 34/40] Apply suggestions from code review Co-authored-by: ickshonpe --- crates/bevy_text/src/text_editable.rs | 14 ++++++++------ crates/bevy_ui/src/widget/text_editable.rs | 11 ++++++----- crates/bevy_ui_render/src/lib.rs | 2 +- examples/ui/text/editable_text.rs | 7 +++++-- 4 files changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 276b4c767a9c3..8a883df149b6b 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -154,13 +154,15 @@ impl EditableText { font_context: &mut FontContext, layout_context: &mut LayoutContext, ) { - // Take the `pending_edits` out of the struct so we can apply them without mutable aliasing issues. - // We do not need to put the `pending_edits` back into the struct, - // as all edits should be consumed by this method, leaving `pending_edits` empty at the end. - let mut pending_edits = core::mem::take(&mut self.pending_edits); - let mut driver = self.editor_mut().driver(font_context, layout_context); + let Self { + editor, + pending_edits, + .. + } = self; + + let mut driver = editor.driver(font_context, layout_context); - while let Some(edit) = pending_edits.pop_front() { + for edit in pending_edits.drain(..) { driver = apply_edit(edit, driver); } } diff --git a/crates/bevy_ui/src/widget/text_editable.rs b/crates/bevy_ui/src/widget/text_editable.rs index bf94489081a8e..e2a7f6cd39757 100644 --- a/crates/bevy_ui/src/widget/text_editable.rs +++ b/crates/bevy_ui/src/widget/text_editable.rs @@ -68,7 +68,11 @@ pub fn editable_text_system( let logical_viewport_size = target.logical_size(); let font_size = text_font.font_size.eval(logical_viewport_size, rem_size.0); style_set.insert(parley::StyleProperty::FontSize(font_size)); - + style_set.insert(parley::StyleProperty::Brush(TextBrush::new( + 0, + text_font.font_smoothing, + ))); + if target.is_changed() { editable_text.editor.set_scale(target.scale_factor()); } @@ -92,9 +96,6 @@ pub fn editable_text_system( ) .into(); - content_size.set(NodeMeasure::Fixed(FixedMeasure { - size: info.size * target.scale_factor(), - })); info.glyphs.clear(); info.run_geometry.clear(); @@ -138,7 +139,7 @@ pub fn editable_text_system( let mut scaler = scale_cx .builder(font_ref) .size(font_size) - .hint(true) + .hint(matches!(hinting, FontHinting::Enabled)) .normalized_coords(coords) .build(); add_glyph_to_atlas( diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index de3c9dad52b25..e68e7e572bc9d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -114,7 +114,7 @@ pub mod stack_z_offsets { pub const TEXT: f32 = 0.06; pub const TEXT_CURSOR: f32 = 0.065; pub const TEXT_STRIKETHROUGH: f32 = 0.07; - pub const TEXT_SELECTION: f32 = 0.08; + pub const TEXT_SELECTION: f32 = 0.055; } #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] diff --git a/examples/ui/text/editable_text.rs b/examples/ui/text/editable_text.rs index bf2da6019f61a..5b30f6092773e 100644 --- a/examples/ui/text/editable_text.rs +++ b/examples/ui/text/editable_text.rs @@ -140,8 +140,6 @@ fn build_input_text( let outer = commands .spawn(( Node { - width: px(200), - height: px(100), border: UiRect { left: px(5), right: px(5), @@ -160,6 +158,11 @@ fn build_input_text( let edit = commands .spawn(( + Node { + width: px(200), + height: px(100), + ..Default::default() + }, Name::new(if is_left { "Left" } else { "Right" }), EditableText::default(), TextFont { From b6c2576f6c6e7c0ce6faf3f197ebffbff35d5cfa Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Fri, 20 Mar 2026 07:06:07 +0800 Subject: [PATCH 35/40] Cleanup --- crates/bevy_text/src/text_editable.rs | 8 ++++---- crates/bevy_ui/src/widget/text_editable.rs | 3 +-- crates/bevy_ui_render/src/lib.rs | 8 ++++---- crates/bevy_ui_render/src/{cursor.rs => text.rs} | 5 ++--- 4 files changed, 11 insertions(+), 13 deletions(-) rename crates/bevy_ui_render/src/{cursor.rs => text.rs} (95%) diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index 8a883df149b6b..e31875df8a637 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -106,7 +106,7 @@ pub struct EditableText { /// Text edit actions that have been requested but not yet applied. /// /// These edits are processed in first-in, first-out order. - pub pending_edits: VecDeque, + pub pending_edits: Vec, /// Cursor width, relative to font size pub cursor_width: f32, } @@ -116,7 +116,7 @@ impl Default for EditableText { Self { // Defaults selected to match `Text::default()` editor: PlainEditor::new(100.), - pending_edits: VecDeque::new(), + pending_edits: Vec::new(), cursor_width: 0.2, } } @@ -143,7 +143,7 @@ impl EditableText { /// Queue a [`TextEdit`] action to be applied later by the [`apply_text_edits`] system. pub fn queue_edit(&mut self, edit: TextEdit) { - self.pending_edits.push_back(edit); + self.pending_edits.push(edit); } /// Applies all [`TextEdit`]s in `pending_edits` immediately, updating the [`PlainEditor`] text / cursor state accordingly. @@ -159,7 +159,7 @@ impl EditableText { pending_edits, .. } = self; - + let mut driver = editor.driver(font_context, layout_context); for edit in pending_edits.drain(..) { diff --git a/crates/bevy_ui/src/widget/text_editable.rs b/crates/bevy_ui/src/widget/text_editable.rs index e2a7f6cd39757..cb411897abb0f 100644 --- a/crates/bevy_ui/src/widget/text_editable.rs +++ b/crates/bevy_ui/src/widget/text_editable.rs @@ -72,7 +72,7 @@ pub fn editable_text_system( 0, text_font.font_smoothing, ))); - + if target.is_changed() { editable_text.editor.set_scale(target.scale_factor()); } @@ -96,7 +96,6 @@ pub fn editable_text_system( ) .into(); - info.glyphs.clear(); info.run_geometry.clear(); diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index e68e7e572bc9d..e6726fd15b2aa 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -9,10 +9,10 @@ pub mod box_shadow; mod color_space; -mod cursor; mod gradient; mod pipeline; mod render_pass; +mod text; pub mod ui_material; mod ui_material_pipeline; pub mod ui_texture_slice_pipeline; @@ -77,8 +77,8 @@ pub use render_pass::*; pub use ui_material_pipeline::*; use ui_texture_slice_pipeline::UiTextureSlicerPlugin; -use crate::cursor::extract_text_cursor; use crate::shader_flags::INVERT; +use crate::text::extract_text_cursor; pub mod prelude { #[cfg(feature = "bevy_ui_debug")] @@ -112,9 +112,9 @@ pub mod stack_z_offsets { pub const IMAGE: f32 = 0.04; pub const MATERIAL: f32 = 0.05; pub const TEXT: f32 = 0.06; - pub const TEXT_CURSOR: f32 = 0.065; pub const TEXT_STRIKETHROUGH: f32 = 0.07; - pub const TEXT_SELECTION: f32 = 0.055; + pub const TEXT_SELECTION: f32 = 0.08; + pub const TEXT_CURSOR: f32 = 0.085; } #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] diff --git a/crates/bevy_ui_render/src/cursor.rs b/crates/bevy_ui_render/src/text.rs similarity index 95% rename from crates/bevy_ui_render/src/cursor.rs rename to crates/bevy_ui_render/src/text.rs index 347b29e250fc2..1b2276e6cd743 100644 --- a/crates/bevy_ui_render/src/cursor.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -44,9 +44,8 @@ pub fn extract_text_cursor( cursor_style, ) in text_node_query.iter() { - // Skip if not visible - // Note that `uinode.is_empty()` may be empty when there is no text, but we still want to display the cursor - if !inherited_visibility.get() { + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + if !inherited_visibility.get() || uinode.is_empty() { continue; } From 577b6944b0a2ff1d20bef6245d21b479da253f5f Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Fri, 20 Mar 2026 07:24:59 +0800 Subject: [PATCH 36/40] Change to observer --- crates/bevy_text/src/text_editable.rs | 2 - crates/bevy_ui/src/widget/text_editable.rs | 15 +-- crates/bevy_ui_widgets/src/editable_text.rs | 132 ++++++++++---------- 3 files changed, 66 insertions(+), 83 deletions(-) diff --git a/crates/bevy_text/src/text_editable.rs b/crates/bevy_text/src/text_editable.rs index e31875df8a637..f4ee9edc5c83d 100644 --- a/crates/bevy_text/src/text_editable.rs +++ b/crates/bevy_text/src/text_editable.rs @@ -69,8 +69,6 @@ // because doing so allows us to process `EditableText` in the various systems provided by `bevy_text` // and `bevy_ui`, such as text layout and font management. -use alloc::collections::VecDeque; - use crate::{ apply_edit, text_edit::TextEdit, FontCx, FontHinting, LayoutCx, LineHeight, TextBrush, TextColor, TextFont, TextLayout, diff --git a/crates/bevy_ui/src/widget/text_editable.rs b/crates/bevy_ui/src/widget/text_editable.rs index cb411897abb0f..95a449245e289 100644 --- a/crates/bevy_ui/src/widget/text_editable.rs +++ b/crates/bevy_ui/src/widget/text_editable.rs @@ -1,6 +1,6 @@ use core::hash::BuildHasher; -use crate::{ComputedNode, ComputedUiRenderTargetInfo, ContentSize, FixedMeasure, NodeMeasure}; +use crate::{ComputedNode, ComputedUiRenderTargetInfo}; use bevy_asset::Assets; use bevy_ecs::{ @@ -37,21 +37,12 @@ pub fn editable_text_system( Ref, &mut EditableText, &mut TextLayoutInfo, - &mut ContentSize, Ref, )>, rem_size: Res, ) { - for ( - text_font, - line_height, - hinting, - target, - mut editable_text, - mut info, - mut content_size, - computed_node, - ) in input_field_query.iter_mut() + for (text_font, line_height, hinting, target, mut editable_text, mut info, computed_node) in + input_field_query.iter_mut() { let Ok(font_family) = resolve_font_source(&text_font.font, fonts.as_ref()) else { continue; diff --git a/crates/bevy_ui_widgets/src/editable_text.rs b/crates/bevy_ui_widgets/src/editable_text.rs index 5068797654cea..b446c9a733f15 100644 --- a/crates/bevy_ui_widgets/src/editable_text.rs +++ b/crates/bevy_ui_widgets/src/editable_text.rs @@ -8,11 +8,11 @@ //! Note that this module is distinct from the core `bevy_text` crate to avoid pulling in //! [`bevy_input`] to that crate, which is intended to be usable in non-interactive contexts. -use bevy_app::{App, Plugin, PreUpdate}; +use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_input::keyboard::{Key, KeyCode, KeyboardInput}; -use bevy_input::{ButtonInput, InputSystems}; -use bevy_input_focus::{InputFocus, InputFocusSystems}; +use bevy_input::ButtonInput; +use bevy_input_focus::FocusedInput; use bevy_text::{EditableText, TextEdit}; use bevy_ui::{widget::TextNodeFlags, ContentSize, Node}; use smol_str::SmolStr; @@ -24,78 +24,77 @@ use smol_str::SmolStr; /// /// Note that this does not immediately apply the edits; they are queued up in [`EditableText::pending_edits`], /// and then applied later by the [`apply_text_edits`](`bevy_text::apply_text_edits`) system. -pub fn process_text_inputs( - focus: Res, +fn on_focused_keyboard_input( + mut trigger: On>, mut query: Query<&mut EditableText>, - mut keyboard_input: MessageReader, keyboard_button_input: Res>, ) { - // Check if any EditableText is focused - let Some(focused_entity) = focus.get() else { - return; // No focused entity, nothing to do - }; - - let Ok(mut editable_text) = query.get_mut(focused_entity) else { + let Ok(mut editable_text) = query.get_mut(trigger.focused_entity) else { return; // Focused entity is not an EditableText, nothing to do }; let shift = keyboard_button_input.pressed(KeyCode::ShiftLeft) || keyboard_button_input.pressed(KeyCode::ShiftRight); - for keyboard_event in keyboard_input.read() { - match keyboard_event { - KeyboardInput { - logical_key: Key::Character(c), - state: bevy_input::ButtonState::Pressed, - .. - } => { - editable_text.queue_edit(TextEdit::Insert(c.clone())); - } - KeyboardInput { - logical_key: Key::Space, - state: bevy_input::ButtonState::Pressed, - .. - } => { - editable_text.queue_edit(TextEdit::Insert(SmolStr::new_inline(" "))); - } - KeyboardInput { - logical_key: Key::Backspace, - state: bevy_input::ButtonState::Pressed, - .. - } => { - editable_text.queue_edit(TextEdit::Backspace); - } - KeyboardInput { - logical_key: Key::Delete, - state: bevy_input::ButtonState::Pressed, - .. - } => { - editable_text.queue_edit(TextEdit::Delete); - } - KeyboardInput { - logical_key: Key::ArrowRight, - state: bevy_input::ButtonState::Pressed, - .. - } => { - if shift { - editable_text.queue_edit(TextEdit::SelectRight); - } else { - editable_text.queue_edit(TextEdit::MoveCursorRight); - } + let mut consumed = true; + match &trigger.input { + KeyboardInput { + logical_key: Key::Character(c), + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::Insert(c.clone())); + } + KeyboardInput { + logical_key: Key::Space, + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::Insert(SmolStr::new_inline(" "))); + } + KeyboardInput { + logical_key: Key::Backspace, + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::Backspace); + } + KeyboardInput { + logical_key: Key::Delete, + state: bevy_input::ButtonState::Pressed, + .. + } => { + editable_text.queue_edit(TextEdit::Delete); + } + KeyboardInput { + logical_key: Key::ArrowRight, + state: bevy_input::ButtonState::Pressed, + .. + } => { + if shift { + editable_text.queue_edit(TextEdit::SelectRight); + } else { + editable_text.queue_edit(TextEdit::MoveCursorRight); } - KeyboardInput { - logical_key: Key::ArrowLeft, - state: bevy_input::ButtonState::Pressed, - .. - } => { - if shift { - editable_text.queue_edit(TextEdit::SelectLeft); - } else { - editable_text.queue_edit(TextEdit::MoveCursorLeft); - } + } + KeyboardInput { + logical_key: Key::ArrowLeft, + state: bevy_input::ButtonState::Pressed, + .. + } => { + if shift { + editable_text.queue_edit(TextEdit::SelectLeft); + } else { + editable_text.queue_edit(TextEdit::MoveCursorLeft); } - _ => {} } + _ => { + consumed = false; + } + } + + if consumed { + trigger.propagate(false); } } @@ -112,12 +111,7 @@ pub struct EditableTextInputPlugin; impl Plugin for EditableTextInputPlugin { fn build(&self, app: &mut App) { - app.add_systems( - PreUpdate, - process_text_inputs - .after(InputFocusSystems::Dispatch) - .after(InputSystems), - ); + app.add_observer(on_focused_keyboard_input); // These components cannot be registered in `bevy_text` where `EditableText` is defined, // because that would create a circular dependency between `bevy_text` and `bevy_ui`. From 227e757cfbe76e8c6351786312f39e95d77f3d10 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Fri, 20 Mar 2026 07:55:41 +0800 Subject: [PATCH 37/40] Doc --- crates/bevy_ui_widgets/src/editable_text.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_ui_widgets/src/editable_text.rs b/crates/bevy_ui_widgets/src/editable_text.rs index b446c9a733f15..4a105aaee04e8 100644 --- a/crates/bevy_ui_widgets/src/editable_text.rs +++ b/crates/bevy_ui_widgets/src/editable_text.rs @@ -3,8 +3,6 @@ //! This module provides systems to process keyboard input events and apply text edits //! to focused [`EditableText`] widgets. //! -//! Only entities that are focused via the [`InputFocus`] resource will receive keyboard input events. -//! //! Note that this module is distinct from the core `bevy_text` crate to avoid pulling in //! [`bevy_input`] to that crate, which is intended to be usable in non-interactive contexts. From 24a0298231765d5d54a05c1a58949c4f5c373bc8 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Fri, 20 Mar 2026 08:54:34 +0800 Subject: [PATCH 38/40] Rm extract_text_editable from bevy_ui_render --- crates/bevy_ui_render/src/lib.rs | 87 -------------------------------- 1 file changed, 87 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index e6726fd15b2aa..a8bd65b089bc6 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -244,7 +244,6 @@ impl Plugin for UiRenderPlugin { extract_text_decorations.in_set(RenderUiSystems::ExtractTextBackgrounds), extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), extract_text_sections.in_set(RenderUiSystems::ExtractText), - extract_text_editable.in_set(RenderUiSystems::ExtractText), extract_text_cursor.in_set(RenderUiSystems::ExtractCursor), #[cfg(feature = "bevy_ui_debug")] debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), @@ -1302,92 +1301,6 @@ pub fn extract_text_decorations( } } -pub fn extract_text_editable( - mut commands: Commands, - mut extracted_uinodes: ResMut, - uinode_query: Extract< - Query< - ( - Entity, - &ComputedNode, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &TextColor, - &TextLayoutInfo, - ), - With, - >, - >, - camera_map: Extract, -) { - let mut start = extracted_uinodes.glyphs.len(); - let mut end = start + 1; - - let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - transform, - inherited_visibility, - clip, - camera, - text_color, - text_layout_info, - ) in &uinode_query - { - // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) - if !inherited_visibility.get() || uinode.is_empty() { - continue; - } - - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - - let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); - - let color = text_color.0.to_linear(); - - for ( - i, - PositionedGlyph { - position, - atlas_info, - .. - }, - ) in text_layout_info.glyphs.iter().enumerate() - { - extracted_uinodes.glyphs.push(ExtractedGlyph { - color, - translation: *position, - rect: atlas_info.rect, - }); - - if text_layout_info - .glyphs - .get(i + 1) - .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) - { - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: uinode.stack_index as f32 + stack_z_offsets::TEXT, - render_entity: commands.spawn(TemporaryRenderEntity).id(), - image: atlas_info.texture, - clip: clip.map(|clip| clip.clip), - extracted_camera_entity, - item: ExtractedUiItem::Glyphs { range: start..end }, - main_entity: entity.into(), - transform, - }); - start = end; - } - - end += 1; - } - } -} - #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct UiVertex { From fd4872df74e743d554906c6a63e710e77a932656 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Fri, 20 Mar 2026 09:18:09 +0800 Subject: [PATCH 39/40] CI --- crates/bevy_ui_render/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index a8bd65b089bc6..adc6d77efcfe2 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -64,8 +64,8 @@ use gradient::GradientPlugin; use bevy_platform::collections::{HashMap, HashSet}; use bevy_text::{ - ComputedTextBlock, EditableText, PositionedGlyph, Strikethrough, StrikethroughColor, - TextBackgroundColor, TextColor, TextLayoutInfo, Underline, UnderlineColor, + ComputedTextBlock, PositionedGlyph, Strikethrough, StrikethroughColor, TextBackgroundColor, + TextColor, TextLayoutInfo, Underline, UnderlineColor, }; use bevy_transform::components::GlobalTransform; use box_shadow::BoxShadowPlugin; From cd799ffbcc9eba5337d3382785c32d94ffbfded0 Mon Sep 17 00:00:00 2001 From: Daniel Skates Date: Sat, 21 Mar 2026 10:42:37 +0800 Subject: [PATCH 40/40] text_system Without --- crates/bevy_ui/src/widget/text.rs | 29 ++++++++++++---------- crates/bevy_ui/src/widget/text_editable.rs | 6 ++--- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 214e60ff5f1e9..aa39b8ce9347a 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -9,7 +9,7 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - query::With, + query::{With, Without}, reflect::ReflectComponent, system::{Query, Res, ResMut}, world::Ref, @@ -19,10 +19,10 @@ use bevy_log::warn_once; use bevy_math::Vec2; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_text::{ - ComputedTextBlock, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, LetterSpacing, LineBreak, - LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, TextFont, TextLayout, - TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextRoot, TextSpanAccess, - TextWriter, + ComputedTextBlock, EditableText, Font, FontAtlasSet, FontCx, FontHinting, LayoutCx, + LetterSpacing, LineBreak, LineHeight, RemSize, ScaleCx, TextBounds, TextColor, TextError, + TextFont, TextLayout, TextLayoutInfo, TextMeasureInfo, TextPipeline, TextReader, TextRoot, + TextSpanAccess, TextWriter, }; use taffy::style::AvailableSpace; @@ -339,14 +339,17 @@ pub fn text_system( mut textures: ResMut>, mut font_atlas_set: ResMut, mut text_pipeline: ResMut, - mut text_query: Query<( - Ref, - &TextLayout, - &mut TextLayoutInfo, - &mut TextNodeFlags, - &mut ComputedTextBlock, - Ref, - )>, + mut text_query: Query< + ( + Ref, + &TextLayout, + &mut TextLayoutInfo, + &mut TextNodeFlags, + &mut ComputedTextBlock, + Ref, + ), + Without, + >, mut scale_cx: ResMut, ) { for (node, block, mut text_layout_info, mut text_flags, mut computed, hinting) in diff --git a/crates/bevy_ui/src/widget/text_editable.rs b/crates/bevy_ui/src/widget/text_editable.rs index 95a449245e289..26f2e4647c521 100644 --- a/crates/bevy_ui/src/widget/text_editable.rs +++ b/crates/bevy_ui/src/widget/text_editable.rs @@ -11,10 +11,10 @@ use bevy_ecs::{ use bevy_image::prelude::*; use bevy_math::{Rect, Vec2}; use bevy_platform::hash::FixedHasher; -use bevy_text::*; use bevy_text::{ - add_glyph_to_atlas, get_glyph_atlas_info, FontAtlasKey, FontAtlasSet, FontCx, GlyphCacheKey, - LayoutCx, LineHeight, RunGeometry, ScaleCx, TextFont, TextLayoutInfo, + add_glyph_to_atlas, get_glyph_atlas_info, resolve_font_source, EditableText, Font, + FontAtlasKey, FontAtlasSet, FontCx, FontHinting, GlyphCacheKey, LayoutCx, LineHeight, + PositionedGlyph, RemSize, RunGeometry, ScaleCx, TextBrush, TextFont, TextLayoutInfo, }; use parley::{swash::FontRef, BoundingBox}; use parley::{FontFamily, FontStack, PositionedLayoutItem};