diff --git a/_release-content/migration-guides/text_input.md b/_release-content/migration-guides/text_input.md new file mode 100644 index 0000000000000..7a4dfc30f8966 --- /dev/null +++ b/_release-content/migration-guides/text_input.md @@ -0,0 +1,9 @@ +--- +title: "EditableText readonly and display-only modes" +pull_requests: [] +--- + +Previously, the `EditableText` component functioned as both a holder for the state of a editable +text, and as a standalone widget, with observers and keyboard mappings. These two functions have +been separated: to make a complete, working text input widget, you will now need to insert +both an `EditableText` and a `TextInput` component. diff --git a/_release-content/release-notes/text_input.md b/_release-content/release-notes/text_input.md new file mode 100644 index 0000000000000..fbd1a53f35374 --- /dev/null +++ b/_release-content/release-notes/text_input.md @@ -0,0 +1,15 @@ +--- +title: "TextInput" +authors: ["@viridia"] +pull_requests: [] +--- + +The `EditableText` component has been split into two components, which are now `EditableText` +and `TextInput`. The `EditableText` component, which lives in the `bevy::text` crate, holds the +state of a text input field, but no longer has any built-in observers - that is, it does not +behave like a widget (headless or otherwise), but merely a holder of state. + +All of the widget-like behaviors (responding to keystrokes) have been moved to a new `TextInput` +component which lives in the `bevy::ui_widgets` crate. Not only is the arrangement more consistent +with the other widgets, but in addition this new component also has properties which are only +interesting to widgets, like a "read-only" option. diff --git a/crates/bevy_feathers/src/controls/number_input.rs b/crates/bevy_feathers/src/controls/number_input.rs index a44a05e47a64b..d841b3cfd3e89 100644 --- a/crates/bevy_feathers/src/controls/number_input.rs +++ b/crates/bevy_feathers/src/controls/number_input.rs @@ -13,15 +13,13 @@ use bevy_ecs::{ query::{Changed, Has, With}, reflect::ReflectComponent, schedule::IntoScheduleConfigs, - system::{Commands, Query, Res, ResMut}, + system::{Commands, Query, Res}, }; use bevy_input::{ keyboard::{Key, KeyCode, KeyboardInput}, ButtonInput, }; -use bevy_input_focus::{ - AcquireFocus, FocusCause, FocusGained, FocusLost, FocusedInput, InputFocus, -}; +use bevy_input_focus::{FocusGained, FocusLost, FocusedInput, InputFocus}; use bevy_log::{warn, warn_once}; use bevy_math::ops; use bevy_picking::{ @@ -35,7 +33,7 @@ use bevy_reflect::Reflect; use bevy_scene::prelude::*; use bevy_text::{ EditableText, EditableTextFilter, FontSourceTemplate, Justify, LineHeight, TextEdit, TextFont, - TextLayout, + TextLayout, TextReadWriteMode, }; use bevy_ui::{ percent, px, widget::Text, AlignItems, AlignSelf, BackgroundGradient, ColorStop, ComputedNode, @@ -163,6 +161,7 @@ impl FeathersNumberInput { @FeathersTextInput { @max_characters: 20usize, } + DragState Node { flex_grow: 1.0, align_items: AlignItems::Center, @@ -205,7 +204,6 @@ impl FeathersNumberInput { ( // Invisible child on top of input field which intercepts drag // events (conditionally) and handles scrubbing gestures. - ScrubberDragState Node { position_type: PositionType::Absolute, left: px(0), @@ -213,7 +211,6 @@ impl FeathersNumberInput { bottom: px(0), right: px(0), } - on(scrubber_on_acquire_focus) on(scrubber_on_press) on(scrubber_on_release) on(scrubber_on_drag_start) @@ -524,12 +521,37 @@ impl Default for NumberInputStep { } } +/// Manage state transitions between scrubbing and dragging modes +#[derive(Default, Clone, Reflect, PartialEq, Debug)] +enum EditMode { + /// Neither scrubbing nor dragging (unfocused) + #[default] + Idle, + /// Drag value via scrubbing + Scrubbing, + /// Edit value by typing + Editing, +} + /// Component used to manage the state of a number during dragging ("scrubbing"). +/// +/// State transitions: +/// +/// if not editing, then a click puts us into "dragging" mode. Once the drag is finished +/// we check max movement, if it's less than the threshold, we transition to "editing" mode. +/// +/// Loss of focus, or hitting the return key, cancels "editing" mode. +/// +/// Changing [`EditMode`] affects: +/// * Cursor shape +/// * [`TextReadWriteMode`] +/// * How drag events are handled +/// * Background and text color (not yet implemented). #[derive(Component, Default, Clone, Reflect)] #[reflect(Component)] -struct ScrubberDragState { +struct DragState { /// Whether the input is currently being dragged. - dragging: bool, + mode: EditMode, /// Conversion factor from pixels dragged to value drag_speed: f64, @@ -608,6 +630,7 @@ fn number_input_on_insert_disabled( &mut gradient, &mut commands, ); + commands.entity(text_id).insert(TextReadWriteMode::ReadOnly); } } @@ -643,6 +666,7 @@ fn number_input_on_remove_disabled( &mut gradient, &mut commands, ); + commands.entity(text_id).insert(TextReadWriteMode::Editable); } } @@ -688,6 +712,9 @@ fn number_input_init( &mut gradient, &mut commands, ); + if is_disabled { + commands.entity(text_id).insert(TextReadWriteMode::ReadOnly); + } } } @@ -695,46 +722,31 @@ fn number_input_init( fn number_input_hovered( insert: On, q_parent: Query<&ChildOf>, - q_children: Query<&Children>, q_number_input: Query< (Has, Option<&ThemeContext>), With, >, - q_scrubber: Query<&mut ScrubberDragState>, - mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>, + mut q_text_input: Query<(&Hovered, &mut BackgroundGradient, &DragState)>, theme: Res, input_focus: Res, mut commands: Commands, ) { let text_id = insert.event_target(); - if let Ok((&Hovered(hovered), mut gradient)) = q_text_input.get_mut(text_id) + if let Ok((&Hovered(hovered), mut gradient, drag_state)) = q_text_input.get_mut(text_id) && let Ok(&ChildOf(root_id)) = q_parent.get(text_id) && let Ok((is_disabled, theme_context)) = q_number_input.get(root_id) { - let Some(drag_state) = q_children - .iter_descendants(text_id) - .find_map(|e| q_scrubber.get(e).ok()) - else { - return; - }; - set_slidebar_styles( text_id, &theme, theme_context.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base), is_disabled, - drag_state.dragging, + drag_state.mode == EditMode::Scrubbing, hovered, input_focus.get() == Some(text_id), &mut gradient, &mut commands, ); - - if input_focus.get() == Some(text_id) { - commands - .entity(text_id) - .insert(EntityCursor::System(bevy_window::SystemCursorIcon::Text)); - } } } @@ -742,18 +754,26 @@ fn number_input_on_enter_key( key_input: On>, q_parent: Query<&ChildOf>, q_number_input: Query<(&NumberInputValue, Option<&HardLimit>), With>, - q_text_input: Query<&EditableText>, + mut q_text_input: Query<(&EditableText, &mut DragState)>, mut commands: Commands, ) { if key_input.input.key_code != KeyCode::Enter { return; } - if let Ok(&ChildOf(root)) = q_parent.get(key_input.event_target()) + let text_id = key_input.event_target(); + if let Ok(&ChildOf(root)) = q_parent.get(text_id) && let Ok((input_value, hard_limit)) = q_number_input.get(root) - && let Ok(editable_text) = q_text_input.get(key_input.event_target()) + && let Ok((editable_text, mut drag_state)) = q_text_input.get_mut(text_id) { let text_value = editable_text.value().to_string(); + drag_state.mode = EditMode::Idle; // Boot us out of editing mode + commands + .entity(text_id) + .insert(TextReadWriteMode::Static) // Hide selection + .insert(EntityCursor::System( + bevy_window::SystemCursorIcon::ColResize, + )); emit_value_change( text_value, input_value.format(), @@ -776,15 +796,22 @@ fn number_input_on_focus_gained(focus_gained: On, mut commands: Com fn number_input_on_focus_lost( focus_lost: On, q_parent: Query<&ChildOf>, - q_number_input: Query<(&NumberInputValue, Option<&HardLimit>), With>, - mut q_text_input: Query<&mut EditableText>, + q_number_input: Query< + ( + &NumberInputValue, + Has, + Option<&HardLimit>, + ), + With, + >, + mut q_text_input: Query<(&EditableText, &mut DragState)>, mut commands: Commands, ) { let editable_text_id = focus_lost.event_target(); if let Ok(&ChildOf(root)) = q_parent.get(editable_text_id) - && let Ok((input_value, hard_limit)) = q_number_input.get(root) - && let Ok(editable_text) = q_text_input.get_mut(editable_text_id) + && let Ok((input_value, disabled, hard_limit)) = q_number_input.get(root) + && let Ok((editable_text, mut drag_state)) = q_text_input.get_mut(editable_text_id) { let text_value = editable_text.value().to_string(); emit_value_change( @@ -796,35 +823,45 @@ fn number_input_on_focus_lost( true, ); - // Restore cursor back to normal. + drag_state.mode = EditMode::Idle; + + // Restore cursor and rwmode back to normal. commands .entity(editable_text_id) .insert(EntityCursor::System( bevy_window::SystemCursorIcon::ColResize, - )); + )) + .insert(if disabled { + TextReadWriteMode::ReadOnly + } else { + TextReadWriteMode::Static + }); } } -/// Suppress the standard "click to focus" behavior, we want to handle this ourselves (focus -/// happens on release rather than press so we can detect drags). -fn scrubber_on_acquire_focus(mut acquire_focus: On) { - acquire_focus.propagate(false); -} - fn scrubber_on_press( mut press: On>, - mut q_scrubber: Query<&mut ScrubberDragState>, + mut q_text_input: Query<&mut DragState>, + q_number_input: Query, With>, q_parent: Query<&ChildOf>, - mut focus: ResMut, + mut commands: Commands, ) { if let Ok(&ChildOf(text_id)) = q_parent.get(press.event_target()) - && let Ok(mut drag_state) = q_scrubber.get_mut(press.entity) + && let Ok(&ChildOf(root_id)) = q_parent.get(text_id) + && let Ok(disabled) = q_number_input.get(root_id) + && let Ok(mut drag_state) = q_text_input.get_mut(text_id) { drag_state.max_distance = 0.0; - // If the text input has focus, then allow the press event to go through - if focus.get() != Some(text_id) { - // If some other widget has focus, clear it. - focus.clear(); + if disabled { + drag_state.mode = EditMode::Idle; + commands.entity(text_id).insert(TextReadWriteMode::ReadOnly); + press.propagate(false); + } else if drag_state.mode == EditMode::Editing { + // Let events propagate to text edit if editing mode. + commands.entity(text_id).insert(TextReadWriteMode::Editable); + } else { + drag_state.mode = EditMode::Scrubbing; + commands.entity(text_id).insert(TextReadWriteMode::Static); press.propagate(false); } } @@ -834,31 +871,28 @@ fn scrubber_on_release( mut release: On>, mut q_text: Query<( &mut EditableText, + &mut DragState, &ComputedNode, &ComputedUiRenderTargetInfo, &UiGlobalTransform, )>, - q_scrubber: Query<(&ComputedNode, &UiGlobalTransform, &mut ScrubberDragState)>, - q_root: Query>, q_parent: Query<&ChildOf>, - mut input_focus: ResMut, ui_scale: Res, + mut commands: Commands, ) { if let Ok(&ChildOf(text_id)) = q_parent.get(release.event_target()) - && let Ok(&ChildOf(root_id)) = q_parent.get(text_id) - && let Ok((mut editable_text, node, target, transform)) = q_text.get_mut(text_id) - && let Ok((_, _, drag_state)) = q_scrubber.get(release.entity) - && let Ok(disabled) = q_root.get(root_id) + && let Ok((mut editable_text, mut drag_state, node, target, transform)) = + q_text.get_mut(text_id) { - // If editable text has focus, then pass the event through. - if input_focus.get() == Some(text_id) { + // If we're in editing mode, let event propagate so that text input can handle it. + if drag_state.mode == EditMode::Editing { return; } release.propagate(false); // Copy of logic from EditableText / text_input, but done on pointer up instead of down. - if !disabled && drag_state.max_distance <= DRAG_THRESHOLD_DISTANCE { + if drag_state.max_distance <= DRAG_THRESHOLD_DISTANCE { if release.button != PointerButton::Primary { return; } @@ -877,8 +911,12 @@ fn scrubber_on_release( return; }; + drag_state.mode = EditMode::Editing; + commands + .entity(text_id) + .insert(TextReadWriteMode::Editable) + .insert(EntityCursor::System(bevy_window::SystemCursorIcon::Text)); editable_text.queue_edit(TextEdit::MoveToPoint(local_pos)); - input_focus.set(text_id, FocusCause::Pressed); } } } @@ -893,8 +931,8 @@ fn scrubber_on_drag_start( Has, Option<&ThemeContext>, )>, - mut q_text_input: Query<&mut BackgroundGradient>, - mut q_scrubber: Query<(&ComputedNode, &mut ScrubberDragState)>, + mut q_text_input: Query<(&mut BackgroundGradient, &mut DragState)>, + mut q_scrubber: Query<&ComputedNode>, q_parent: Query<&ChildOf>, input_focus: Res, theme: Res, @@ -904,14 +942,13 @@ fn scrubber_on_drag_start( && let Ok(&ChildOf(root_id)) = q_parent.get(text_id) && let Ok((input_value, soft_limit, precision, step, disabled, theme_context)) = q_root.get(root_id) - && let Ok(mut gradient) = q_text_input.get_mut(text_id) + && let Ok((mut gradient, mut drag)) = q_text_input.get_mut(text_id) && !disabled - && input_focus.get() != Some(text_id) - && let Ok((node, mut drag)) = q_scrubber.get_mut(drag_start.event_target()) + && let Ok(node) = q_scrubber.get_mut(drag_start.event_target()) + && drag.mode == EditMode::Scrubbing { let slider_size = (node.size().x * node.inverse_scale_factor).max(1.0) as f64; drag_start.propagate(false); - drag.dragging = true; drag.base_value = *input_value; drag.max_distance = 0.0; drag.value_offset = 0.0f64; @@ -961,22 +998,23 @@ fn scrubber_on_drag( Option<&NumberInputPrecision>, Has, )>, - mut q_scrubber: Query<(&UiGlobalTransform, &mut ScrubberDragState)>, + mut q_text_input: Query<&mut DragState>, + mut q_scrubber: Query<&UiGlobalTransform>, q_parent: Query<&ChildOf>, - focus: Res, mut commands: Commands, ui_scale: Res, keys: Res>, ) { if let Ok(&ChildOf(text_id)) = q_parent.get(drag.event_target()) - && focus.get() != Some(text_id) && let Ok(&ChildOf(root_id)) = q_parent.get(text_id) && let Ok((soft_limit, hard_limit, precision, disabled)) = q_root.get(root_id) - && let Ok((transform, mut drag_state)) = q_scrubber.get_mut(drag.entity) + && let Ok(mut drag_state) = q_text_input.get_mut(text_id) + && let Ok(transform) = q_scrubber.get_mut(drag.entity) + && drag_state.mode == EditMode::Scrubbing { drag_state.max_distance = drag_state.max_distance.max(drag.distance.length()); drag.propagate(false); - if drag_state.dragging && !disabled && drag_state.max_distance > DRAG_THRESHOLD_DISTANCE { + if !disabled && drag_state.max_distance > DRAG_THRESHOLD_DISTANCE { let drag_delta = transform.transform_vector2(drag.delta / ui_scale.0).x; let mut delta = drag_delta as f64 * drag_state.drag_speed; if keys.pressed(Key::Shift) { @@ -1005,62 +1043,60 @@ fn scrubber_on_drag_end( Has, Option<&ThemeContext>, )>, - mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>, - mut q_scrubber: Query<&mut ScrubberDragState>, + mut q_text_input: Query<(&Hovered, &mut BackgroundGradient, &mut DragState)>, q_parent: Query<&ChildOf>, - input_focus: Res, mut commands: Commands, theme: Res, ) { if let Ok(&ChildOf(text_id)) = q_parent.get(drag_end.event_target()) - && input_focus.get() != Some(text_id) && let Ok(&ChildOf(root_id)) = q_parent.get(text_id) && let Ok((soft_limit, hard_limit, precision, disabled, theme_context)) = q_root.get(root_id) - && let Ok(mut drag_state) = q_scrubber.get_mut(drag_end.entity) - && let Ok((&Hovered(hovered), mut gradient)) = q_text_input.get_mut(text_id) + && let Ok((&Hovered(hovered), mut gradient, mut drag_state)) = q_text_input.get_mut(text_id) + && drag_state.mode == EditMode::Scrubbing { drag_end.propagate(false); - if drag_state.dragging { - if !disabled { - emit_drag_value_change( - &mut commands, - root_id, - soft_limit, - hard_limit, - precision, - &mut drag_state, - true, - ); - } - set_slidebar_styles( - text_id, - &theme, - theme_context.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base), - disabled, - false, - hovered, - false, - &mut gradient, + if !disabled { + emit_drag_value_change( &mut commands, + root_id, + soft_limit, + hard_limit, + precision, + &mut drag_state, + true, ); - drag_state.dragging = false; } + set_slidebar_styles( + text_id, + &theme, + theme_context.map(|tc| tc.0).unwrap_or(SurfaceLevel::Base), + disabled, + false, + hovered, + false, + &mut gradient, + &mut commands, + ); } } fn scrubber_on_drag_cancel( mut drag_cancel: On>, q_parent: Query<&ChildOf>, - mut q_text_input: Query<(&Hovered, &mut BackgroundGradient, Option<&ThemeContext>)>, - mut q_scrubber: Query<&mut ScrubberDragState>, + mut q_text_input: Query<( + &Hovered, + &mut BackgroundGradient, + &mut DragState, + Option<&ThemeContext>, + )>, theme: Res, input_focus: Res, mut commands: Commands, ) { if let Ok(&ChildOf(text_id)) = q_parent.get(drag_cancel.event_target()) - && let Ok(mut drag_state) = q_scrubber.get_mut(drag_cancel.entity) - && let Ok((&Hovered(hovered), mut gradient, theme_context)) = q_text_input.get_mut(text_id) + && let Ok((&Hovered(hovered), mut gradient, mut drag_state, theme_context)) = + q_text_input.get_mut(text_id) { set_slidebar_styles( text_id, @@ -1074,7 +1110,7 @@ fn scrubber_on_drag_cancel( &mut commands, ); drag_cancel.propagate(false); - drag_state.dragging = false; + drag_state.mode = EditMode::Idle; } } @@ -1141,7 +1177,7 @@ fn set_slidebar_styles( let cursor_shape = match disabled { true => bevy_window::SystemCursorIcon::NotAllowed, - false => bevy_window::SystemCursorIcon::EwResize, + false => bevy_window::SystemCursorIcon::ColResize, }; if let [Gradient::Linear(linear_gradient)] = &mut gradient.0[..] { @@ -1164,7 +1200,7 @@ fn emit_drag_value_change( soft_limit: Option<&SoftLimit>, hard_limit: Option<&HardLimit>, precision: Option<&NumberInputPrecision>, - drag_state: &mut ScrubberDragState, + drag_state: &mut DragState, is_final: bool, ) { // Relative scrub: always measured from the value at drag start. diff --git a/crates/bevy_feathers/src/controls/text_input.rs b/crates/bevy_feathers/src/controls/text_input.rs index ef54a852ec7e7..34589291dad52 100644 --- a/crates/bevy_feathers/src/controls/text_input.rs +++ b/crates/bevy_feathers/src/controls/text_input.rs @@ -17,10 +17,12 @@ use bevy_reflect::Reflect; use bevy_scene::prelude::*; use bevy_text::{ EditableText, FontSource, FontWeight, LineBreak, TextCursorStyle, TextFont, TextLayout, + TextReadWriteMode, }; use bevy_ui::{ px, AlignItems, BorderRadius, Display, InteractionDisabled, JustifyContent, Node, UiRect, }; +use bevy_ui_widgets::TextInput; use crate::{ constants::{fonts, size}, @@ -109,6 +111,7 @@ impl FeathersTextInput { } , } FeathersTextInput + TextInput EditableText { cursor_width: 0.3, visible_width: {props.visible_width}, @@ -159,6 +162,9 @@ fn update_text_input_styles( ) { for (input_ent, disabled, font_color) in q_inputs.iter() { set_text_input_styles(input_ent, disabled, font_color, &mut commands); + commands + .entity(input_ent) + .insert(TextReadWriteMode::ReadOnly); } } @@ -173,6 +179,9 @@ fn update_text_input_styles_remove( removed_disabled.read().for_each(|ent| { if let Ok((input_ent, disabled, font_color)) = q_inputs.get(ent) { set_text_input_styles(input_ent, disabled, font_color, &mut commands); + commands + .entity(input_ent) + .insert(TextReadWriteMode::Editable); } }); } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 26df25b475326..554aa2b0f7f94 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -127,7 +127,7 @@ impl Plugin for IgnoreAmbiguitiesPlugin { )] fn build(&self, app: &mut bevy_app::App) { #[cfg(all(feature = "bevy_ui_widgets", feature = "bevy_sprite"))] - if app.is_plugin_added::() + if app.is_plugin_added::() && app.is_plugin_added::() { // update_ime_position reads Window to reposition the IME cursor, while diff --git a/crates/bevy_text/src/editing.rs b/crates/bevy_text/src/editing.rs index e0425df584287..6bf54ee6ec19a 100644 --- a/crates/bevy_text/src/editing.rs +++ b/crates/bevy_text/src/editing.rs @@ -103,7 +103,8 @@ use parley::{FontContext, LayoutContext, PlainEditor, SplitString}; TextColor, LineHeight, FontHinting, - EditableTextGeneration + EditableTextGeneration, + TextReadWriteMode )] pub struct EditableText { /// A [`parley::PlainEditor`], tracking both the text content and cursor position. @@ -311,6 +312,20 @@ pub struct EditableTextGeneration(parley::Generation); #[derive(Component, Clone, Default)] pub struct EditableTextFilter(Option bool + Send + Sync + 'static>>); +/// Indicates whether the text is editable, or is in "readonly" mode. A special "static" mode is +/// also available, which is used by the feathers number input widget. +#[derive(Component, Clone, Copy, Default, PartialEq, Debug)] +pub enum TextReadWriteMode { + /// Text input functions normally + #[default] + Editable, + /// Cursor movement, selection, and copy to clipboard is still enabled, but no mutations are allowed + ReadOnly, + /// Display only, all interactions disabled - this is used by number input widget when dragging. + /// This disallows cursor movement and selection as well. + Static, +} + impl EditableTextFilter { /// Create a new `EditableTextFilter` from the given filter function. pub fn new(filter: impl Fn(char) -> bool + Send + Sync + 'static) -> Self { diff --git a/crates/bevy_text/src/text_edit.rs b/crates/bevy_text/src/text_edit.rs index 84c236a451f6b..20c44abf17a17 100644 --- a/crates/bevy_text/src/text_edit.rs +++ b/crates/bevy_text/src/text_edit.rs @@ -222,6 +222,52 @@ impl TextEdit { } } + /// Returns whether this actually affects the text, or is just cursor movement + pub fn is_destructive(&self) -> bool { + match self { + // Destructive actions + TextEdit::Cut + | TextEdit::Paste + | TextEdit::Insert(_) + | TextEdit::Backspace + | TextEdit::BackspaceWord + | TextEdit::Delete + | TextEdit::DeleteWord + | TextEdit::ImeSetCompose { + value: _, + cursor: _, + } + | TextEdit::ImeCommit { value: _ } => true, + + // Nondestructive actions + TextEdit::Copy + | TextEdit::Left(_) + | TextEdit::Right(_) + | TextEdit::WordLeft(_) + | TextEdit::WordRight(_) + | TextEdit::Up(_) + | TextEdit::Down(_) + | TextEdit::TextStart(_) + | TextEdit::TextEnd(_) + | TextEdit::HardLineStart(_) + | TextEdit::HardLineEnd(_) + | TextEdit::LineStart(_) + | TextEdit::LineEnd(_) + | TextEdit::CollapseSelection + | TextEdit::SelectAll + | TextEdit::SelectAllIfCollapsed + | TextEdit::MoveToPoint(_) + | TextEdit::SelectWordAtPoint(_) + | TextEdit::SelectLineAtPoint(_) + | TextEdit::SelectedHardLineAtPoint(_) + | TextEdit::ExtendSelectionToPoint(_) + | TextEdit::ShiftClickExtension(_) + | TextEdit::ScrollByLines(_) + | TextEdit::ScrollTo(_) + | TextEdit::ScrollBy(_) => false, + } + } + /// Apply the [`TextEdit`] to the text editor driver and viewport. /// /// Note that some edits, such as [`TextEdit::Paste`], may need to be deferred across frames due to asynchronous clipboard I/O. diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs index 0af2e772f4679..e78c560c58dd8 100644 --- a/crates/bevy_ui/src/interaction_states.rs +++ b/crates/bevy_ui/src/interaction_states.rs @@ -18,6 +18,14 @@ use bevy_reflect::Reflect; /// the `InteractionDisabled` component should be added to the root entity of the widget - the /// same entity that contains the `AccessibilityNode` component. This will ensure that /// the a11y tree is updated correctly. +/// +/// For developers familiar with the web and HTML, the word "disabled" as used here should be +/// interpreted as working more like the `aria-disabled` (which doesn't prevent input focus) +/// than the `disabled` attribute (which does). However, unlike `aria-disabled`, which is purely +/// advisory, this does make the widget non-operable. +/// +/// In particular, this marker currently has no effect on the "click-to-focus" observer which +/// lives in [`bevy_input_focus`], as that is a lower-level crate that this one depends on. #[derive(Component, Debug, Clone, Copy, Default, Reflect)] #[reflect(Component, Default, Clone)] pub struct InteractionDisabled; diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index bb77b09008950..b5a41c6163edb 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -6,7 +6,7 @@ use bevy_input_focus::InputFocus; use bevy_math::{Affine2, Rect, Vec2}; use bevy_render::Extract; use bevy_sprite::BorderRect; -use bevy_text::{EditableText, TextColor, TextCursorStyle, TextLayoutInfo}; +use bevy_text::{EditableText, TextColor, TextCursorStyle, TextLayoutInfo, TextReadWriteMode}; use bevy_ui::{ CalculatedClip, ComputedNode, ComputedStackIndex, ComputedUiTargetCamera, ResolvedBorderRadius, UiGlobalTransform, @@ -30,6 +30,7 @@ pub fn extract_text_cursor( &ComputedUiTargetCamera, &TextLayoutInfo, &TextCursorStyle, + &TextReadWriteMode, Option<&EditableText>, )>, >, @@ -49,6 +50,7 @@ pub fn extract_text_cursor( target_camera, text_layout_info, cursor_style, + rwmode, editable_text, ) in extracted_uinodes .changed @@ -89,13 +91,16 @@ pub fn extract_text_cursor( focused = true; } - let sc = if focused { + let sc = if focused && *rwmode == TextReadWriteMode::Editable { cursor_style.selection_color } else { cursor_style.unfocused_selection_color }; - if !text_layout_info.selection_rects.is_empty() && !sc.is_fully_transparent() { + if !text_layout_info.selection_rects.is_empty() + && !sc.is_fully_transparent() + && *rwmode != TextReadWriteMode::Static + { let selection_color = sc.to_linear(); let selection_radius = cursor_style.selection_radius.clamp(0.0, 0.5); @@ -175,6 +180,7 @@ pub fn extract_text_cursor( if let Some((true, cursor_rect)) = text_layout_info.cursor && !cursor_rect.is_empty() && !cursor_style.color.is_fully_transparent() + && *rwmode != TextReadWriteMode::Static { extracted_uinodes .uinodes diff --git a/crates/bevy_ui_widgets/src/lib.rs b/crates/bevy_ui_widgets/src/lib.rs index 92d17d051ae6f..046ee2c6c3ec2 100644 --- a/crates/bevy_ui_widgets/src/lib.rs +++ b/crates/bevy_ui_widgets/src/lib.rs @@ -91,7 +91,7 @@ impl PluginGroup for UiWidgetsPlugins { PluginGroupBuilder::start::() .add(ButtonPlugin) .add(CheckboxPlugin) - .add(EditableTextInputPlugin) + .add(TextInputPlugin) .add(ListBoxPlugin) .add(MenuPlugin) .add(DialogPlugin) diff --git a/crates/bevy_ui_widgets/src/text_input.rs b/crates/bevy_ui_widgets/src/text_input.rs index 82e0feb504477..83e3754c46a95 100644 --- a/crates/bevy_ui_widgets/src/text_input.rs +++ b/crates/bevy_ui_widgets/src/text_input.rs @@ -1,14 +1,15 @@ -//! Input handling for [`EditableText`] widgets. +//! Input handling for [`TextInput`] widgets. //! //! This module provides systems to process keyboard input events and apply text edits -//! to focused [`EditableText`] widgets. +//! to focused [`TextInput`] widgets. //! //! 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_a11y::AccessibilitySystems; +use accesskit::Role; +use bevy_a11y::{AccessibilityNode, AccessibilitySystems}; use bevy_app::{App, Plugin, PostUpdate, PreUpdate}; -use bevy_ecs::prelude::*; +use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_input::keyboard::{Key, KeyCode, KeyboardInput}; use bevy_input::{ButtonInput, InputSystems}; use bevy_input_focus::{ @@ -20,15 +21,15 @@ use bevy_picking::pointer::PointerButton; use bevy_reflect::Reflect; use bevy_text::{ scrollable_text_layout_width, EditableText, EditableTextSystems, PreeditCursor, TextEdit, - TextLayout, TextLayoutInfo, + TextLayout, TextLayoutInfo, TextReadWriteMode, }; use bevy_time::{Real, Time}; use bevy_ui::widget::{sync_editable_text_viewports, update_editable_text_layout}; -use bevy_ui::UiSystems; use bevy_ui::{ - widget::TextNodeFlags, ComputedNode, ComputedUiRenderTargetInfo, ContentSize, - InteractionDisabled, Node, UiGlobalTransform, UiScale, + widget::TextNodeFlags, ComputedNode, ComputedUiRenderTargetInfo, ContentSize, Node, + UiGlobalTransform, UiScale, }; +use bevy_ui::{InteractionDisabled, UiSystems}; use bevy_window::{Ime, PrimaryWindow, Window}; const NONE: u8 = 0; @@ -50,6 +51,12 @@ const SHIFT_COMMAND: u8 = SHIFT | COMMAND; #[cfg(not(target_os = "macos"))] const SHIFT_ALT: u8 = SHIFT | ALT; +/// Editable text widget. +#[derive(Component, Clone, Default, Reflect)] +#[require(EditableText, AccessibilityNode(accesskit::Node::new(Role::TextInput)))] +#[reflect(Component)] +pub struct TextInput; + /// Autoscroll speed is proportional to the input size const AUTOSCROLL_BASE_SPEED: f32 = 0.75; const AUTOSCROLL_MAX_SPEED: f32 = 2.0; @@ -91,11 +98,19 @@ fn matches_edit_shortcut(input: &KeyboardInput, character: &str, key_code: KeyCo /// and then applied later by the [`apply_text_edits`](`bevy_text::apply_text_edits`) system. fn on_focused_keyboard_input( mut keyboard_input: On>, + mut query: Query< + ( + &mut EditableText, + &TextReadWriteMode, + Has, + ), + With, + >, mut input_focus: ResMut, - mut query: Query<&mut EditableText, Without>, keys: Res>, ) { - let Ok(mut editable_text) = query.get_mut(keyboard_input.focused_entity) else { + let Ok((mut editable_text, rwmode, disabled)) = query.get_mut(keyboard_input.focused_entity) + else { return; // Focused entity is not an EditableText, nothing to do }; @@ -121,8 +136,11 @@ fn on_focused_keyboard_input( let mut should_propagate = true; - let mut queue_edit = |edit| { - if keyboard_input.input.state.is_pressed() { + let mut queue_edit = |edit: TextEdit| { + if (*rwmode == TextReadWriteMode::Editable + || (!edit.is_destructive() && (!disabled || *rwmode == TextReadWriteMode::ReadOnly))) + && keyboard_input.input.state.is_pressed() + { editable_text.queue_edit(edit); } should_propagate = false; @@ -222,21 +240,22 @@ fn on_pointer_press( mut text_input_query: Query< ( &mut EditableText, + &TextReadWriteMode, &ComputedNode, &ComputedUiRenderTargetInfo, &UiGlobalTransform, ), - Without, + With, >, keys: Res>, - mut input_focus: ResMut, ui_scale: Res, ) { if press.button != PointerButton::Primary { return; } - let Ok((mut editable_text, node, target, transform)) = text_input_query.get_mut(press.entity) + let Ok((mut editable_text, rwmode, node, target, transform)) = + text_input_query.get_mut(press.entity) else { // The press landed on something that isn't an `EditableText`. Clicking away to blur a // focused text input is handled by `PointerFocusPlugin` (in `bevy_input_focus`), which @@ -245,7 +264,9 @@ fn on_pointer_press( return; }; - input_focus.set(press.entity, FocusCause::Pressed); + if *rwmode == TextReadWriteMode::Static { + return; + } press.propagate(false); @@ -253,6 +274,7 @@ fn on_pointer_press( // The IME is active; all input needs to be routed there, including pointer presses. return; } + let Some(local_pos) = transform.try_inverse().and_then(|inverse| { let local_pos = inverse .transform_point2(press.pointer_location.position * target.scale_factor() / ui_scale.0); @@ -284,28 +306,32 @@ fn on_pointer_press( /// and then applied later by the [`apply_text_edits`](`bevy_text::apply_text_edits`) system. fn on_pointer_drag( mut drag: On>, - mut text_input_query: Query<( - &mut EditableText, - &ComputedNode, - &ComputedUiRenderTargetInfo, - &UiGlobalTransform, - )>, + mut text_input_query: Query< + ( + &mut EditableText, + &TextReadWriteMode, + &ComputedNode, + &ComputedUiRenderTargetInfo, + &UiGlobalTransform, + ), + With, + >, ui_scale: Res, - input_focus: Res, ) { if drag.button != PointerButton::Primary { return; } - if input_focus.get() != Some(drag.entity) { - return; - } - - let Ok((mut editable_text, node, target, transform)) = text_input_query.get_mut(drag.entity) + let Ok((mut editable_text, rwmode, node, target, transform)) = + text_input_query.get_mut(drag.entity) else { return; }; + if *rwmode == TextReadWriteMode::Static { + return; + } + drag.propagate(false); if editable_text.is_composing() { @@ -449,7 +475,14 @@ fn autoscroll_axis(overflow: f32, view_size: f32, time_delta: f32) -> f32 { fn on_ime_input( mut ime_reader: MessageReader, input_focus: Res, - mut editable_text_query: Query<&mut EditableText>, + mut editable_text_query: Query< + ( + &mut EditableText, + &TextReadWriteMode, + Has, + ), + With, + >, ) { let Some(focused_entity) = input_focus.get() else { // No focused entity, nothing to do. @@ -458,13 +491,20 @@ fn on_ime_input( return; }; - let Ok(mut editable_text) = editable_text_query.get_mut(focused_entity) else { + let Ok((mut editable_text, rwmode, disabled)) = editable_text_query.get_mut(focused_entity) + else { // Focused entity is not an EditableText, nothing to do. // Still need to drain the reader to prevent stale events on next focus. ime_reader.read().for_each(drop); return; }; + if *rwmode != TextReadWriteMode::Editable || disabled { + // Still need to drain the reader to prevent stale events on next focus. + ime_reader.read().for_each(drop); + return; + } + for ime in ime_reader.read() { match ime { Ime::Preedit { value, cursor, .. } => { @@ -499,12 +539,17 @@ fn on_ime_input( /// composing text rather than overlapping it. fn update_ime_position( input_focus: Res, - editable_text_query: Query<( - &EditableText, - &ComputedNode, - &UiGlobalTransform, - &ComputedUiRenderTargetInfo, - )>, + editable_text_query: Query< + ( + &EditableText, + &TextReadWriteMode, + &ComputedNode, + &UiGlobalTransform, + &ComputedUiRenderTargetInfo, + Has, + ), + With, + >, // TODO: support multiple windows and track which one has focus mut windows: Query<&mut Window, With>, ui_scale: Res, @@ -512,10 +557,17 @@ fn update_ime_position( let Some(focused) = input_focus.get() else { return; }; - let Ok((editable_text, node, transform, target)) = editable_text_query.get(focused) else { + + let Ok((editable_text, rwmode, node, transform, target, disabled)) = + editable_text_query.get(focused) + else { return; }; + if *rwmode != TextReadWriteMode::Editable || disabled { + return; + } + let Ok(mut window) = windows.single_mut() else { return; }; @@ -531,25 +583,36 @@ fn update_ime_position( } /// System that enables or disables IME on the primary window based on whether the focused entity -/// is an [`EditableText`]. +/// is an editable text field. /// -/// IME is enabled when an `EditableText` gains focus and disabled when focus moves elsewhere. +/// IME is enabled when an editable `EditableText` gains focus and disabled when focus moves +/// elsewhere. The IME state is also re-evaluated whenever the focused entity's read/write mode +/// changes. Because [`TextReadWriteMode`] is an optional component, such a change can happen when the +/// component is overwritten, inserted, or removed; removal reverts the effective mode to the +/// [`Editable`](TextReadWriteMode::Editable) default and is detected via [`RemovedComponents`]. fn listen_for_ime_input_when_text_input_focused( input_focus: Res, - editable_text_query: Query<(), With>, + mut editable_text_query: Query<(&mut EditableText, Ref), With>, mut windows: Query<&mut Window, With>, ) { - if !input_focus.is_changed() { - return; - } + let focused_entity = input_focus.get(); + let Ok(mut window) = windows.single_mut() else { return; }; - let editable_text_focused = input_focus - .get() - .is_some_and(|e| editable_text_query.contains(e)); - // The IME should be enabled whenever an EditableText is focused, + // Fetch the focused editable text field (if any). + let focused = focused_entity.and_then(|e| editable_text_query.get_mut(e).ok()); + + let editable_text_focused = focused.is_some_and(|(mut editable_text, rwmode)| { + let rwmode_changed = rwmode.is_changed(); + if rwmode_changed && *rwmode != TextReadWriteMode::Editable { + editable_text.queue_edit(TextEdit::clear_ime_compose()); + } + *rwmode == TextReadWriteMode::Editable + }); + + // The IME should be enabled whenever an EditableText is focused and editable, // even if the IME isn't currently composing (i.e. there's no preedit text). // The field here is about "should" we accept IME input, not "are we currently" accepting IME input window.ime_enabled = editable_text_focused; @@ -570,7 +633,10 @@ fn listen_for_ime_input_when_text_input_focused( /// A [`TextEdit::CollapseSelection`] is also fired to collapse any active highlighted text /// selection back to the caret cursor position, preventing text styles from getting stuck in a /// focused color state. -fn on_focus_lost(trigger: On, mut editable_text_query: Query<&mut EditableText>) { +fn on_focus_lost( + trigger: On, + mut editable_text_query: Query<&mut EditableText, With>, +) { if let Ok(mut editable_text) = editable_text_query.get_mut(trigger.entity) { editable_text.queue_edit(TextEdit::clear_ime_compose()); editable_text.queue_edit(TextEdit::CollapseSelection); @@ -595,7 +661,7 @@ struct QueuedSelectAll(Option); fn on_focus_select_all( focus_gained: On, - mut q_text_input: Query<(&mut EditableText, Has)>, + mut q_text_input: Query<(&mut EditableText, Has), With>, mut queued_select_all: ResMut, ) { let target = focus_gained.event_target(); @@ -637,7 +703,7 @@ fn apply_queued_select_all( } } -/// System sets for IME-related systems used by [`EditableTextInputPlugin`]. +/// System sets for IME-related systems used by [`TextInputPlugin`]. #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] pub enum ImeSystems { /// Processes [`Ime`] events into [`TextEdit`] actions for the focused [`EditableText`]. @@ -663,9 +729,9 @@ pub enum ImeSystems { /// /// Note that [`TextEdit`]s are applied during [`PostUpdate`] /// in the [`EditableTextSystems`] system set. -pub struct EditableTextInputPlugin; +pub struct TextInputPlugin; -impl Plugin for EditableTextInputPlugin { +impl Plugin for TextInputPlugin { fn build(&self, app: &mut App) { app.init_resource::() .add_observer(on_focused_keyboard_input) @@ -673,6 +739,15 @@ impl Plugin for EditableTextInputPlugin { .add_observer(on_pointer_press) .add_observer(on_focus_lost) .add_observer(on_focus_select_all) + .configure_sets( + PreUpdate, + ( + ImeSystems::ToggleWindowIMEInput, + ImeSystems::HandleEvents, + ImeSystems::UpdatePosition, + ) + .chain(), + ) .add_systems( PreUpdate, ( @@ -740,7 +815,10 @@ mod tests { .world_mut() .spawn((Window::default(), PrimaryWindow)) .id(); - let editable_text = app.world_mut().spawn(EditableText::default()).id(); + let editable_text = app + .world_mut() + .spawn((EditableText::default(), TextInput)) + .id(); app.insert_resource(InputFocus::from_entity(editable_text)); for (text, repeat) in [("J", false), ("#", true)] { @@ -1032,7 +1110,10 @@ mod tests { .spawn((Window::default(), PrimaryWindow)) .id(); - let editable_text = app.world_mut().spawn(EditableText::default()).id(); + let editable_text = app + .world_mut() + .spawn((TextInput, EditableText::default())) + .id(); app.world_mut().entity_mut(window).observe( move |input: On>, mut saw: ResMut| { if matches!(input.input.logical_key, Key::Escape) && input.input.state.is_pressed() diff --git a/examples/3d/clustered_decals.rs b/examples/3d/clustered_decals.rs index da673f6df4f2a..f1d7bf04ed65a 100644 --- a/examples/3d/clustered_decals.rs +++ b/examples/3d/clustered_decals.rs @@ -418,7 +418,6 @@ fn handle_drag_as_movement( { return; } - for (mut transform, selection) in &mut selections { if app_status.selection != *selection { continue; diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 2bb33561c70d0..3e5cadc0e212b 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -2813,6 +2813,7 @@ mod editable_text { use bevy::text::EditableText; use bevy::text::TextCursorStyle; use bevy::text::TextEdit; + use bevy::ui_widgets::TextInput; const DUMMY_TEXT: &str = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten"; const LOREM_TEXT: &str = concat!( @@ -2821,14 +2822,14 @@ mod editable_text { "Cum sociis natoque penatibus et magnis dis parturient montes, nascetur reprehenderit mus. ", "Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. ", "Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. ", - "In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. ", - "Nullam dictum felis eu pede mollis pretium. Integer tincidunt. ", + "In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. ", + "Nullam dictum felis eu pede mollis pretium. Integer tincidunt. ", "Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. ", "Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. ", "Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. ", "Phasellus viverra nulla ut metus officia laoreet. Quisque rutrum. ", - "Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi.", - " Qui eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, ", + "Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi.", + " Qui eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, ", "sem quam semper libero, sit amet adipiscing sem neque sed ipsum. ", "Qui quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. ", "Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. ", @@ -2861,6 +2862,7 @@ mod editable_text { children![ Text::new("Single line"), ( + TextInput, EditableText { pending_edits: vec![TextEdit::Insert( "Single line EditableText".into(), @@ -2898,6 +2900,7 @@ mod editable_text { ), Text::new("Insert end"), ( + TextInput, EditableText { pending_edits: vec![TextEdit::Insert(LOREM_TEXT.into())], ..default() @@ -2917,6 +2920,7 @@ mod editable_text { ), Text::new("Select line start"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(LOREM_TEXT.into()), @@ -2948,6 +2952,7 @@ mod editable_text { children![ Text::new("Wrapped start"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(LOREM_TEXT.into()), @@ -2979,6 +2984,7 @@ mod editable_text { children![ Text::new("Wrapped selection"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(LOREM_TEXT.into()), @@ -3012,6 +3018,7 @@ mod editable_text { children![ Text::new("Clamp top"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), @@ -3043,6 +3050,7 @@ mod editable_text { children![ Text::new("Home, Scroll 1"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), @@ -3075,6 +3083,7 @@ mod editable_text { children![ Text::new("Home, Scroll 2"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), @@ -3107,6 +3116,7 @@ mod editable_text { children![ Text::new("Clamp bottom"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), @@ -3139,6 +3149,7 @@ mod editable_text { children![ Text::new("Bottom -1"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), @@ -3172,6 +3183,7 @@ mod editable_text { children![ Text::new("Top +3"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), @@ -3204,6 +3216,7 @@ mod editable_text { children![ Text::new("Select down 3"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), @@ -3239,6 +3252,7 @@ mod editable_text { children![ Text::new("End, Scroll 1"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), @@ -3270,6 +3284,7 @@ mod editable_text { children![ Text::new("End, Scroll -0.5"), ( + TextInput, EditableText { pending_edits: vec![ TextEdit::Insert(DUMMY_TEXT.into()), diff --git a/examples/ui/text/editable_text_filter.rs b/examples/ui/text/editable_text_filter.rs index aee98af372df6..b70b70b47c3cc 100644 --- a/examples/ui/text/editable_text_filter.rs +++ b/examples/ui/text/editable_text_filter.rs @@ -5,6 +5,7 @@ use bevy::color::palettes::tailwind::SLATE_300; use bevy::input_focus::AutoFocus; use bevy::prelude::*; use bevy::text::{EditableText, EditableTextFilter, TextCursorStyle}; +use bevy::ui_widgets::TextInput; fn main() { App::new() @@ -32,6 +33,7 @@ fn setup(mut commands: Commands) { padding: px(8.).all(), ..default() }, + TextInput, EditableText { max_characters: Some(8), ..default() diff --git a/examples/ui/text/ime_support.rs b/examples/ui/text/ime_support.rs index 6d0d12e3bbdea..af08d8fff0b1a 100644 --- a/examples/ui/text/ime_support.rs +++ b/examples/ui/text/ime_support.rs @@ -14,6 +14,7 @@ use bevy::input_focus::{ }; use bevy::prelude::*; use bevy::text::{EditableText, TextCursorStyle}; +use bevy::ui_widgets::TextInput; fn main() { App::new() @@ -58,6 +59,7 @@ fn setup(mut commands: Commands) { ..default() }, BorderColor::from(Color::from(SLATE_300)), + TextInput, EditableText { allow_newlines: true, ..default() diff --git a/examples/ui/text/multiline_text_input.rs b/examples/ui/text/multiline_text_input.rs index e002520158226..c58b5e88ced26 100644 --- a/examples/ui/text/multiline_text_input.rs +++ b/examples/ui/text/multiline_text_input.rs @@ -7,6 +7,7 @@ use bevy::input_focus::tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin} use bevy::input_focus::{AutoFocus, FocusCause, FocusedInput, InputFocus}; use bevy::prelude::*; use bevy::text::{EditableText, EditableTextFilter, TextCursorStyle, TextLayoutInfo}; +use bevy::ui_widgets::TextInput; use bevy::ui_widgets::{ popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide}, Activate, Button, MenuAction, MenuButton, MenuEvent, MenuFocusState, MenuItem, MenuPopup, @@ -93,6 +94,7 @@ fn setup(mut commands: Commands, asset_server: Res) { padding: px(8.).all(), ..default() }, + TextInput, EditableText { visible_lines: Some(8.), allow_newlines: true, @@ -208,6 +210,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, BackgroundColor(DARK_SLATE_GRAY.into()), BorderColor::all(SLATE_300), + TextInput, EditableText::new("8"), EditableTextFilter::new(|c| c.is_ascii_digit() || c == '.'), TextCursorStyle { @@ -292,6 +295,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, BackgroundColor(DARK_SLATE_GRAY.into()), BorderColor::all(SLATE_300), + TextInput, EditableText::new("30"), EditableTextFilter::new(|c| c.is_ascii_digit()), TextCursorStyle { @@ -373,6 +377,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, BackgroundColor(DARK_SLATE_GRAY.into()), BorderColor::all(SLATE_300), + TextInput, EditableText::new("0"), EditableTextFilter::new(|c| c.is_ascii_digit() || c == '.'), TextCursorStyle { diff --git a/examples/ui/text/multiple_text_inputs.rs b/examples/ui/text/multiple_text_inputs.rs index 6462959e68d24..6ec152ffcfd5d 100644 --- a/examples/ui/text/multiple_text_inputs.rs +++ b/examples/ui/text/multiple_text_inputs.rs @@ -1,9 +1,9 @@ //! Demonstrates multiple text inputs //! //! This example arranges text inputs in a four-column grid layout. The first column shows the text justification, the second column is an -//! [`EditableText`] text input node, the third column is a `Text` node that is kept synchronized with the [`EditableText`]'s contents by the +//! [`TextInput`] text input node, the third column is a `Text` node that is kept synchronized with the [`TextInput`]'s contents by the //! [`synchronize_output_text`] system, and the fourth column is updated by the [`submit_text`] system when the user submits the -//! [`EditableText`]'s text by pressing `Enter`. +//! [`TextInput`]'s text by pressing `Enter`. use bevy::color::palettes::tailwind::SLATE_300; use bevy::input::keyboard::Key; @@ -14,11 +14,12 @@ use bevy::input_focus::{ InputFocus, }; use bevy::prelude::*; -use bevy::text::{EditableText, TextCursorStyle, TextEditChange}; +use bevy::text::{EditableText, TextCursorStyle, TextEditChange, TextReadWriteMode}; +use bevy::ui_widgets::TextInput; fn main() { App::new() - // `EditableTextInputPlugin` is part of `DefaultPlugins` + // `TextInputPlugin` is part of `DefaultPlugins` .add_plugins((DefaultPlugins, TabNavigationPlugin)) .add_systems(Startup, setup) .add_systems(Update, (submit_text, update_row_border_colors)) @@ -113,6 +114,7 @@ fn setup(mut commands: Commands, asset_server: Res) { padding: px(4.).all(), ..default() }, + TextInput, EditableText::new(format!("Initial text {row}")), TextCursorStyle::default(), font.clone(), @@ -174,6 +176,106 @@ fn setup(mut commands: Commands, asset_server: Res) { )); } + let label_font = font.clone().with_font_size(14.); + for label in ["ReadWrite", "EditableText", "value", "submission"] { + parent.spawn(( + Text::new(label), + label_font.clone(), + Node { + justify_self: JustifySelf::Center, + margin: px(-4).bottom(), + ..default() + }, + )); + } + + for (row, rwmode) in [ + TextReadWriteMode::Editable, + TextReadWriteMode::ReadOnly, + TextReadWriteMode::Static, + ] + .into_iter() + .enumerate() + { + parent.spawn(( + Node { + border: px(4).all(), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BorderColor::all(Color::WHITE), + children![(Text::new(format!("{rwmode:?}")), font.clone(),)], + )); + + let mut input = parent.spawn(( + Node { + border: px(4.).all(), + padding: px(4.).all(), + ..default() + }, + TextInput, + EditableText::new(format!("Initial text {row}")), + TextCursorStyle::default(), + font.clone(), + BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()), + TextInputRow(row + 100), + rwmode, + TabIndex(row as i32 + 100), + BorderColor::all(SLATE_300), + )); + if row == 0 { + input.insert(AutoFocus); + } + + parent.spawn(( + Node { + border: px(4.).all(), + padding: px(4.).all(), + overflow: Overflow::clip_x(), + overflow_clip_margin: OverflowClipMargin { + visual_box: VisualBox::ContentBox, + ..default() + }, + ..default() + }, + BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()), + BorderColor::all(Color::WHITE), + children![( + Text::default(), + TextLayout::no_wrap(), + font.clone(), + BackgroundColor(bevy::color::palettes::css::DARK_SLATE_GRAY.into()), + BorderColor::all(Color::WHITE), + TextInputRow(row + 100), + TextOutput, + )], + )); + + parent.spawn(( + Node { + border: px(4.).all(), + padding: px(4.).all(), + overflow: Overflow::clip_x(), + overflow_clip_margin: OverflowClipMargin { + visual_box: VisualBox::ContentBox, + ..default() + }, + + ..default() + }, + BackgroundColor(bevy::color::palettes::css::DARK_SLATE_BLUE.into()), + BorderColor::all(Color::WHITE), + children![( + Text::default(), + TextLayout::no_wrap(), + font.clone(), + TextInputRow(row + 100), + SubmitOutput, + )], + )); + } + parent.spawn(( Text::new("Press Enter to submit"), Node { diff --git a/examples/ui/text/text_input.rs b/examples/ui/text/text_input.rs index 7431762a1fc06..9fbf9b2b4e18b 100644 --- a/examples/ui/text/text_input.rs +++ b/examples/ui/text/text_input.rs @@ -1,6 +1,6 @@ -//! Demonstrates a simple, unstyled [`EditableText`] widget. +//! Demonstrates a simple, unstyled [`TextInput`] widget. //! -//! [`EditableText`] is a basic primitive for text input in Bevy UI. +//! [`TextInput`] 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. //! @@ -31,6 +31,7 @@ use bevy::input_focus::{ }; use bevy::prelude::*; use bevy::text::{EditableText, TextCursorStyle}; +use bevy::ui_widgets::TextInput; fn main() { App::new() @@ -124,6 +125,7 @@ fn build_input_text(commands: &mut Commands, is_left: bool, font_size: f32) -> E }, BorderColor::from(Color::from(SLATE_300)), Name::new(if is_left { "Left" } else { "Right" }), + TextInput, EditableText { visible_width: Some(10.), allow_newlines: false,