diff --git a/Cargo.toml b/Cargo.toml index 62ad1cd93a1d6..d50561f1dd289 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 81874f37af24f..cf68b9a1077a1 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -87,7 +87,8 @@ impl Plugin for SpritePlugin { bevy_text::detect_text_needs_rerender::, 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() diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 065effa8d494b..7f3dac07f2a2d 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -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; @@ -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 { diff --git a/crates/bevy_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 1d46bf56511d5..cecbbf46fc2fa 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -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", diff --git a/crates/bevy_text/src/input/buffer.rs b/crates/bevy_text/src/input/buffer.rs new file mode 100644 index 0000000000000..08803591849bf --- /dev/null +++ b/crates/bevy_text/src/input/buffer.rs @@ -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, + /// 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, + /// 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, +} + +/// Default font size +pub const DEFAULT_FONT_SIZE: f32 = 20.; +/// Default line height factor (relative to font size) +/// +/// `1.2` corresponds to `normal` in `` +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) -> 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::(context.entity) { + let value = value.0.clone(); + if let Some(mut actions) = world.entity_mut(context.entity).get_mut::() { + 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(&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(&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, + &TextEdits, + Ref, + Option<&mut CursorBlink>, + )>, + time: Res