Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
edc879c
Added `input` module to `bevy_text` with systems and components to su…
ickshonpe Jul 30, 2025
f36f9d0
use `is_empty()` instead of checking lengeth is 0
ickshonpe Jul 30, 2025
771477c
Renamings:
ickshonpe Aug 1, 2025
11074c0
Renamed `TextInputPasswordMask` to `PasswordMask`
ickshonpe Aug 1, 2025
f991426
Updated comments including suggestions from review
ickshonpe Aug 1, 2025
62ca2f8
Added a `TextEdit::InsertString` variant. Use to insert a string at …
ickshonpe Aug 1, 2025
308d0c9
Renamed `appy_text_input_action` to `apply_text_edit`
ickshonpe Aug 1, 2025
158f329
Updated `TextInputValue`'s comments to explain that it is synchronise…
ickshonpe Aug 1, 2025
a7d5e45
Expanded the doc comments for `TextInputAttributes`.
ickshonpe Aug 1, 2025
785a1d5
Renamed `TextInputUndoHistory` to `UndoHistory`
ickshonpe Aug 1, 2025
f637123
Merge branch 'main' into bevy-text-input
ickshonpe Aug 1, 2025
b4589b8
Updated buffer's docs and added a `needs_redraw` function.
ickshonpe Aug 1, 2025
8e61105
Updated the doc comments for `apply_text_edits`
ickshonpe Aug 1, 2025
ede9d60
updated another doc comment
ickshonpe Aug 1, 2025
a80a5dc
More doc comments
ickshonpe Aug 1, 2025
0b2c268
Edited the doc comment for apply_text_edits
ickshonpe Aug 1, 2025
93149bc
More comment edits
ickshonpe Aug 1, 2025
049e350
Renamed `Prompt` to `Placeholder` and also updated the comments and t…
ickshonpe Aug 1, 2025
3e9c860
Fixed renaming.
ickshonpe Aug 1, 2025
5c956c6
Rephrased doc comment.
ickshonpe Aug 1, 2025
6396a11
Spellings.
ickshonpe Aug 1, 2025
5d34e6d
Renamed the `InvalidInput` and `ValueChanged` `TextInputEvent` varian…
ickshonpe Aug 1, 2025
57a70cd
Fixed event dispatch.
ickshonpe Aug 1, 2025
85bed33
Added release note
ickshonpe Aug 1, 2025
f43c461
Added migration note for `load_font_to_fontdb` function changes.
ickshonpe Aug 1, 2025
0b6b1d2
Update release note
ickshonpe Aug 1, 2025
897d741
Merge branch 'main' into bevy-text-input
ickshonpe Aug 2, 2025
2bd0216
Update crates/bevy_text/src/input.rs
ickshonpe Aug 2, 2025
255d2c1
Update crates/bevy_text/src/input.rs
ickshonpe Aug 2, 2025
419bcaf
Update crates/bevy_text/src/input.rs
ickshonpe Aug 2, 2025
fe38cf2
Update release-content/release-notes/bevy_text_input_module.md
ickshonpe Aug 2, 2025
dea6616
Update crates/bevy_text/src/input.rs
ickshonpe Aug 2, 2025
48475ff
FIxed comment
ickshonpe Aug 2, 2025
893f9c4
Merge branch 'bevy-text-input' of https://github.com/ickshonpe/bevy i…
ickshonpe Aug 2, 2025
96bacc1
Fix comments
ickshonpe Aug 2, 2025
2035726
Added text input 2d example
ickshonpe Aug 4, 2025
a692fd8
cargo run -p build-templated-pages -- build-example-page
ickshonpe Aug 4, 2025
9371313
Merge branch 'main' into bevy-text-input
alice-i-cecile Aug 5, 2025
27dc3df
Merge branch 'bevy-text-input' of https://github.com/ickshonpe/bevy i…
ickshonpe Aug 5, 2025
622bbe6
Added example skeleton.
ickshonpe Aug 5, 2025
e20e670
Merge branch 'main' into bevy-text-input
ickshonpe Aug 8, 2025
307590a
Added `TextInputStyle` component.
ickshonpe Aug 8, 2025
6611bde
Updated example
ickshonpe Aug 8, 2025
811f0c8
Added `CursorBlinkInterval` resource
ickshonpe Aug 8, 2025
2f43052
Renamed CursorBlinkInterval to TextCursorBlinkInterval.
ickshonpe Aug 8, 2025
acc0706
Added doc comment for resource
ickshonpe Aug 8, 2025
c72f7ce
Deleted style component.
ickshonpe Aug 8, 2025
eb15165
Fixed target size scaling.
ickshonpe Aug 8, 2025
171de2f
Updated comments
ickshonpe Aug 8, 2025
96e3c6e
Clean up
ickshonpe Aug 8, 2025
0ba0b68
Merge branch 'main' into bevy-text-input
ickshonpe Aug 11, 2025
295de7f
Fixed example imports
ickshonpe Aug 11, 2025
6b3f3d8
Anchor output below input in example
ickshonpe Aug 11, 2025
f794658
Removed unnecessary casts
ickshonpe Aug 11, 2025
00a0c13
Fixed wrong casts
ickshonpe Aug 11, 2025
f03a7cf
New `InvalidTextEditError`, returned by `apply_text_edit`.
ickshonpe Aug 11, 2025
fb73aa3
Only collect buffer text if `TextInputValue` is present.
ickshonpe Aug 11, 2025
288ea3b
take just Editor'_> with is_cursor_at_end_of_line
ickshonpe Aug 11, 2025
6817f5c
Added release note suggestion
ickshonpe Aug 11, 2025
7ab3c19
Update crates/bevy_text/src/input.rs
ickshonpe Aug 11, 2025
caa38b1
Drop undo/redo
Zeophlite Aug 19, 2025
d6fb883
Merge remote-tracking branch 'origin/main' into no-undo
Zeophlite Aug 26, 2025
3146a97
Backticks in docs
Zeophlite Aug 6, 2025
3e1621d
Fix ambiguity_detection
Zeophlite Aug 6, 2025
7f2a1ce
Merge remote-tracking branch 'origin/main' into no-undo
Zeophlite Aug 26, 2025
ae9b9dc
CI
Zeophlite Aug 26, 2025
430f85e
Apply suggestions from code review
Zeophlite Aug 27, 2025
acaff98
Reposition anchor
Zeophlite Aug 27, 2025
9584a78
Merge remote-tracking branch 'origin/main' into g
Zeophlite Aug 27, 2025
e981c7b
Clarify docs
Zeophlite Aug 27, 2025
60a6349
Feedback
Zeophlite Aug 28, 2025
2f1d6d4
Merge remote-tracking branch 'origin/main' into no-undo
Zeophlite Aug 28, 2025
1b1bfcc
Lint
Zeophlite Aug 28, 2025
3e14a7d
Copyedits from Vero
alice-i-cecile Aug 28, 2025
9787de6
Fix PR number for font migration guide
alice-i-cecile Aug 28, 2025
91ff4a5
Remove already redundant components when spawning
alice-i-cecile Aug 28, 2025
cb9aa00
Notes from playing around with the example
alice-i-cecile Aug 28, 2025
66048df
Initial module docs
alice-i-cecile Aug 28, 2025
32af1dc
TextInputBuffer config
alice-i-cecile Aug 28, 2025
44a4101
Better docs for `Placeholder`
alice-i-cecile Aug 28, 2025
4089f8a
More breadcrumbs
alice-i-cecile Aug 28, 2025
9a39b80
Docs for `TextEdit`
alice-i-cecile Aug 28, 2025
76b5835
don't need textfont and react to font asset events
mockersf Aug 28, 2025
83956c7
Make cursor blink optional component
Zeophlite Aug 29, 2025
f018a7b
Merge remote-tracking branch 'origin/main' into no-undo
Zeophlite Aug 29, 2025
4b79c15
Nits
Zeophlite Aug 29, 2025
b9e6198
CI
Zeophlite Aug 29, 2025
053103b
Less magic
Zeophlite Aug 30, 2025
c7e4a0f
Remove TextEdit::Submit
Zeophlite Aug 30, 2025
1c24810
Merge remote-tracking branch 'origin/main' into no-undo
Zeophlite Aug 30, 2025
e321a9f
reposition -> calculate_bounds
Zeophlite Aug 30, 2025
29b29db
Merge remote-tracking branch 'origin/main' into no-undo
Zeophlite Sep 2, 2025
9f7b1af
Split bevy_text input.rs into input/
Zeophlite Sep 2, 2025
e49c58f
Merge remote-tracking branch 'origin/main' into no-undo
Zeophlite Sep 2, 2025
0b10def
CI
Zeophlite Sep 2, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,17 @@ description = "Renders text to multiple windows with different scale factors usi
category = "2D Rendering"
wasm = true

[[example]]
name = "text_input_2d"
path = "examples/2d/text_input_2d.rs"
doc-scrape-examples = true

[package.metadata.example.text_input_2d]
name = "Text Input 2D"
description = "Text input in 2D"
category = "2D Rendering"
wasm = true

[[example]]
name = "texture_atlas"
path = "examples/2d/texture_atlas.rs"
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ impl Plugin for SpritePlugin {
bevy_text::detect_text_needs_rerender::<Text2d>,
update_text2d_layout
.after(bevy_camera::CameraUpdateSystems)
.after(bevy_text::remove_dropped_font_atlas_sets),
.after(bevy_text::remove_dropped_font_atlas_sets)
.ambiguous_with(bevy_text::update_placeholder_layouts),
calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
)
.chain()
Expand Down
17 changes: 15 additions & 2 deletions crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use bevy_asset::{Assets, Handle};
use bevy_camera::visibility::{self, Visibility, VisibilityClass};
use bevy_camera::{
primitives::Aabb,
visibility::{self, Visibility, VisibilityClass},
};
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_math::{Rect, UVec2, Vec2, Vec3};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_transform::components::Transform;

Expand Down Expand Up @@ -260,6 +263,16 @@ impl Anchor {
pub fn as_vec(&self) -> Vec2 {
self.0
}

/// Determine the bounds at the anchor
pub fn calculate_bounds(&self, size: Vec2) -> Aabb {
let x1 = (Anchor::TOP_LEFT.0.x - self.as_vec().x) * size.x;
let x2 = (Anchor::TOP_LEFT.0.x - self.as_vec().x + 1.) * size.x;
let y1 = (Anchor::TOP_LEFT.0.y - self.as_vec().y - 1.) * size.y;
let y2 = (Anchor::TOP_LEFT.0.y - self.as_vec().y) * size.y;

Aabb::from_min_max(Vec3::new(x1, y1, 0.), Vec3::new(x2, y2, 0.))
}
}

impl Default for Anchor {
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_text/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bevy_image = { path = "../bevy_image", version = "0.17.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std",
Expand Down
306 changes: 306 additions & 0 deletions crates/bevy_text/src/input/buffer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
use bevy_asset::{AssetEvent, Assets, Handle};
use bevy_derive::Deref;
use bevy_ecs::{
change_detection::DetectChanges,
component::Component,
event::EventReader,
lifecycle::HookContext,
system::{Query, Res, ResMut},
world::{DeferredWorld, Ref},
};
use bevy_time::Time;
use cosmic_text::{Buffer, BufferLine, Edit, Editor, Metrics};

use crate::{
load_font_to_fontdb, CosmicFontSystem, CursorBlink, Font, FontSmoothing, Justify, LineBreak,
LineHeight, TextCursorBlinkInterval, TextEdit, TextEdits, TextError, TextInputTarget,
TextLayoutInfo, TextPipeline,
};

/// Common text input properties set by the user.
/// On changes, the text input systems will automatically update the buffer, layout and fonts as required.
#[derive(Component, Debug, PartialEq)]
pub struct TextInputAttributes {
/// The text input's font, which also applies to any [`crate::Placeholder`] text or password mask.
/// A text input's glyphs must all be from the same font.
pub font: Handle<Font>,
/// The size of the font.
/// A text input's glyphs must all be the same size.
pub font_size: f32,
/// The height of each line.
/// A text input's lines must all be the same height.
pub line_height: LineHeight,
/// Determines how lines will be broken
pub line_break: LineBreak,
/// The horizontal alignment for all the text in the text input buffer.
pub justify: Justify,
/// Controls text antialiasing
pub font_smoothing: FontSmoothing,
/// Maximum number of glyphs the text input buffer can contain.
/// Any edits that extend the length above `max_chars` are ignored.
/// If set on a buffer longer than `max_chars` the buffer will be truncated.
pub max_chars: Option<usize>,
/// The maximum number of lines the buffer will display without scrolling.
/// * Clamped between zero and target height divided by line height.
/// * If None or equal or less than 0, will fill the target space.
/// * Only restricts the maximum number of visible lines, places no constraint on the text buffer's length.
/// * Supports fractional values, `visible_lines: Some(2.5)` will display two and a half lines of text.
pub visible_lines: Option<f32>,
}

/// Default font size
pub const DEFAULT_FONT_SIZE: f32 = 20.;
/// Default line height factor (relative to font size)
///
/// `1.2` corresponds to `normal` in `<https://developer.mozilla.org/en-US/docs/Web/CSS/line-height>`
pub const DEFAULT_LINE_HEIGHT_FACTOR: f32 = 1.2;
/// Default line height
pub const DEFAULT_LINE_HEIGHT: f32 = DEFAULT_FONT_SIZE * DEFAULT_LINE_HEIGHT_FACTOR;
/// Default space advance
pub const DEFAULT_SPACE_ADVANCE: f32 = 20.;

impl Default for TextInputAttributes {
fn default() -> Self {
Self {
font: Default::default(),
font_size: DEFAULT_FONT_SIZE,
line_height: LineHeight::RelativeToFont(DEFAULT_LINE_HEIGHT_FACTOR),
font_smoothing: Default::default(),
justify: Default::default(),
line_break: Default::default(),
max_chars: None,
visible_lines: None,
}
}
}

/// Contains the current text in the text input buffer.
/// Automatically synchronized with the buffer by [`crate::apply_text_edits`] after any edits are applied.
/// On insertion, replaces the current text in the text buffer.
#[derive(Component, PartialEq, Debug, Default, Deref)]
#[component(
on_insert = on_insert_text_input_value,
)]
pub struct TextInputValue(pub String);

impl TextInputValue {
/// New text, when inserted replaces the current text in the text buffer
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}

/// Get the current text
pub fn get(&self) -> &str {
&self.0
}
}

/// Set the text input with the text from the `TextInputValue` when inserted.
fn on_insert_text_input_value(mut world: DeferredWorld, context: HookContext) {
if let Some(value) = world.get::<TextInputValue>(context.entity) {
let value = value.0.clone();
if let Some(mut actions) = world.entity_mut(context.entity).get_mut::<TextEdits>() {
actions.queue(TextEdit::SetText(value));
}
}
}

/// Get the text from a cosmic text buffer
pub fn get_cosmic_text_buffer_contents(buffer: &Buffer) -> String {
buffer
.lines
.iter()
.map(BufferLine::text)
.fold(String::new(), |mut out, line| {
if !out.is_empty() {
out.push('\n');
}
out.push_str(line);
out
})
}

/// The text input buffer.
/// Primary component that contains the text layout.
///
/// The `needs_redraw` method can be used to check if the buffer's contents have changed and need redrawing.
/// Component change detection is not reliable as the editor buffer needs to be borrowed mutably during updates.
#[derive(Component, Debug)]
#[require(TextInputAttributes, TextInputTarget, TextEdits, TextLayoutInfo)]
pub struct TextInputBuffer {
/// The cosmic text editor buffer.
pub editor: Editor<'static>,
/// Space advance width for the current font, used to determine the width of the cursor when it is at the end of a line
/// or when the buffer is empty.
pub space_advance: f32,
}

impl Default for TextInputBuffer {
fn default() -> Self {
Self {
editor: Editor::new(Buffer::new_empty(Metrics::new(
DEFAULT_FONT_SIZE,
DEFAULT_LINE_HEIGHT,
))),
space_advance: DEFAULT_SPACE_ADVANCE,
}
}
}

impl TextInputBuffer {
/// Use the cosmic text buffer mutably
pub fn with_buffer_mut<F, T>(&mut self, f: F) -> T
where
F: FnOnce(&mut Buffer) -> T,
{
self.editor.with_buffer_mut(f)
}

/// Use the cosmic text buffer
pub fn with_buffer<F, T>(&self, f: F) -> T
where
F: FnOnce(&Buffer) -> T,
{
self.editor.with_buffer(f)
}

/// True if the buffer is empty
pub fn is_empty(&self) -> bool {
self.with_buffer(|buffer| {
buffer.lines.is_empty()
|| (buffer.lines.len() == 1 && buffer.lines[0].text().is_empty())
})
}

/// Get the text contained in the text buffer
pub fn get_text(&self) -> String {
self.editor.with_buffer(get_cosmic_text_buffer_contents)
}

/// Returns true if the buffer's contents have changed and need to be redrawn.
pub fn needs_redraw(&self) -> bool {
self.editor.redraw()
}
}

/// Updates the text input buffer in response to changes
/// that require regeneration of the the buffer's
/// metrics and attributes.
pub fn update_text_input_buffers(
mut text_input_query: Query<(
&mut TextInputBuffer,
Ref<TextInputTarget>,
&TextEdits,
Ref<TextInputAttributes>,
Option<&mut CursorBlink>,
)>,
time: Res<Time>,
cursor_blink_interval: Res<TextCursorBlinkInterval>,
mut font_system: ResMut<CosmicFontSystem>,
mut text_pipeline: ResMut<TextPipeline>,
fonts: Res<Assets<Font>>,
mut font_events: EventReader<AssetEvent<Font>>,
) {
let font_system = &mut font_system.0;
let font_id_map = &mut text_pipeline.map_handle_to_font_id;
for (mut input_buffer, target, edits, attributes, maybe_cursor_blink) in
text_input_query.iter_mut()
{
let TextInputBuffer {
editor,
space_advance,
} = input_buffer.as_mut();

if let Some(mut cursor_blink) = maybe_cursor_blink {
cursor_blink.cursor_blink_timer = if edits.queue.is_empty() {
(cursor_blink.cursor_blink_timer + time.delta_secs())
.rem_euclid(cursor_blink_interval.0.as_secs_f32() * 2.)
} else {
0.
};
}

let _ = editor.with_buffer_mut(|buffer| {
if target.is_changed()
|| attributes.is_changed()
|| font_events.read().any(|event| match event {
AssetEvent::Added { id } | AssetEvent::Modified { id } => {
*id == attributes.font.id()
}
_ => false,
})
{
let line_height = attributes.line_height.eval(attributes.font_size);
let metrics =
Metrics::new(attributes.font_size, line_height).scale(target.scale_factor);
buffer.set_metrics(font_system, metrics);

buffer.set_wrap(font_system, attributes.line_break.into());

if !fonts.contains(attributes.font.id()) {
return Err(TextError::NoSuchFont);
}

let face_info =
load_font_to_fontdb(attributes.font.clone(), font_system, font_id_map, &fonts);

let attrs = cosmic_text::Attrs::new()
.metadata(0)
.family(cosmic_text::Family::Name(&face_info.family_name))
.stretch(face_info.stretch)
.style(face_info.style)
.weight(face_info.weight);

let mut text = buffer.lines.iter().map(BufferLine::text).fold(
String::new(),
|mut out, line| {
if !out.is_empty() {
out.push('\n');
}
out.push_str(line);
out
},
);

if let Some(max_chars) = attributes.max_chars {
text.truncate(max_chars);
}

buffer.set_text(font_system, &text, &attrs, cosmic_text::Shaping::Advanced);
let align = Some(attributes.justify.into());
for buffer_line in buffer.lines.iter_mut() {
buffer_line.set_align(align);
}

*space_advance = font_id_map
.get(&attributes.font.id())
.and_then(|(id, ..)| font_system.get_font(*id))
.and_then(|font| {
let face = font.rustybuzz();
face.glyph_index(' ')
.and_then(|gid| face.glyph_hor_advance(gid))
.map(|advance| advance as f32 / face.units_per_em() as f32)
})
.unwrap_or(0.0)
* buffer.metrics().font_size;

let height =
if let Some(lines) = attributes.visible_lines.filter(|lines| 0. < *lines) {
(metrics.line_height * lines).max(target.size.y)
} else {
target.size.y
};

buffer.set_size(
font_system,
Some(target.size.x - *space_advance),
Some(height),
);

buffer.set_redraw(true);
}

Ok(())
});
}
}
8 changes: 8 additions & 0 deletions crates/bevy_text/src/input/clipboard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use bevy_ecs::resource::Resource;

/// Basic clipboard implementation that only works within the bevy app.
///
/// This is written to in the [`crate::apply_text_edits`] system when
/// [`crate::TextEdit::Copy`], [`crate::TextEdit::Cut`] or [`crate::TextEdit::Paste`] edits are applied.
#[derive(Resource, Default)]
pub struct Clipboard(pub String);
Loading
Loading