mirror of
https://github.com/bevyengine/bevy.git
synced 2026-06-29 23:35:35 -04:00
e52d2c5c53
# Objective Make the `multiple_text_inputs` example a bit more useful by giving each text input a different justification, which should help with debugging cursor behaviour. ## Solution Add an input for each `Justify` variant along with a column on the left labelling the justification it is using. ## Showcase <img width="1798" height="750" alt="inputs-justified" src="https://github.com/user-attachments/assets/324c1cd4-b6a1-4c0f-ad1a-38cd28867704" />
280 lines
9.9 KiB
Rust
280 lines
9.9 KiB
Rust
//! 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
|
||
//! [`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`.
|
||
|
||
use bevy::color::palettes::tailwind::SLATE_300;
|
||
use bevy::input::keyboard::Key;
|
||
use bevy::input_focus::tab_navigation::NavAction;
|
||
use bevy::input_focus::{tab_navigation::TabNavigation, AutoFocus, FocusCause};
|
||
use bevy::input_focus::{
|
||
tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin},
|
||
InputFocus,
|
||
};
|
||
use bevy::prelude::*;
|
||
use bevy::text::{EditableText, TextCursorStyle};
|
||
|
||
fn main() {
|
||
App::new()
|
||
// `EditableTextInputPlugin` is part of `DefaultPlugins`
|
||
.add_plugins((DefaultPlugins, TabNavigationPlugin))
|
||
.add_systems(Startup, setup)
|
||
.add_systems(
|
||
Update,
|
||
(
|
||
synchronize_output_text,
|
||
submit_text,
|
||
update_row_border_colors,
|
||
),
|
||
)
|
||
.run();
|
||
}
|
||
|
||
#[derive(Component)]
|
||
struct TextOutput;
|
||
|
||
#[derive(Component)]
|
||
struct SubmitOutput;
|
||
|
||
#[derive(Component)]
|
||
struct TextInputRow(usize);
|
||
|
||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||
commands.spawn(Camera2d);
|
||
|
||
let font = TextFont {
|
||
font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
|
||
font_size: FontSize::Px(24.),
|
||
..default()
|
||
};
|
||
|
||
commands
|
||
.spawn((
|
||
Node {
|
||
width: percent(100.),
|
||
height: percent(100.),
|
||
display: Display::Grid,
|
||
justify_content: JustifyContent::Center,
|
||
align_content: AlignContent::Center,
|
||
grid_template_columns: vec![GridTrack::px(160.), RepeatedGridTrack::px(3, 320.)],
|
||
row_gap: px(8.),
|
||
column_gap: px(8.),
|
||
..default()
|
||
},
|
||
TabGroup::default(),
|
||
))
|
||
.with_children(|parent| {
|
||
parent.spawn((
|
||
Text::new("Multiple Text Inputs Example"),
|
||
Node {
|
||
grid_column: GridPlacement::span(4),
|
||
justify_self: JustifySelf::Center,
|
||
margin: px(16).bottom(),
|
||
..default()
|
||
},
|
||
TextColor::WHITE,
|
||
font.clone(),
|
||
));
|
||
|
||
let label_font = font.clone().with_font_size(14.);
|
||
for label in ["Justify", "EditableText", "value", "submission"] {
|
||
parent.spawn((
|
||
Text::new(label),
|
||
label_font.clone(),
|
||
Node {
|
||
justify_self: JustifySelf::Center,
|
||
margin: px(-4).bottom(),
|
||
..default()
|
||
},
|
||
));
|
||
}
|
||
|
||
for (row, justify) in [
|
||
Justify::Left,
|
||
Justify::Center,
|
||
Justify::Right,
|
||
Justify::Justified,
|
||
Justify::Start,
|
||
Justify::End,
|
||
]
|
||
.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!("{justify:?}")), font.clone(),)],
|
||
));
|
||
|
||
let mut input = parent.spawn((
|
||
Node {
|
||
border: px(4.).all(),
|
||
padding: px(4.).all(),
|
||
..default()
|
||
},
|
||
EditableText::new(format!("Initial text {row}")),
|
||
TextCursorStyle::default(),
|
||
font.clone(),
|
||
BackgroundColor(bevy::color::palettes::css::DARK_GREY.into()),
|
||
TextInputRow(row),
|
||
TextLayout::no_wrap().with_justify(justify),
|
||
TabIndex(row as i32),
|
||
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),
|
||
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),
|
||
SubmitOutput,
|
||
)],
|
||
));
|
||
}
|
||
|
||
parent.spawn((
|
||
Text::new("Press Enter to submit"),
|
||
Node {
|
||
grid_column: GridPlacement::span(4),
|
||
justify_self: JustifySelf::Center,
|
||
margin: px(16).top(),
|
||
..default()
|
||
},
|
||
font.clone(),
|
||
));
|
||
});
|
||
}
|
||
|
||
/// This system keeps the text of the [`TextOutput`] [`Text`] nodes synchronized with the text
|
||
/// of the [`EditableText`] node on the same row.
|
||
fn synchronize_output_text(
|
||
changed_inputs: Query<(&EditableText, &TextInputRow), Changed<EditableText>>,
|
||
mut outputs: Query<(&mut Text, &TextInputRow), With<TextOutput>>,
|
||
) {
|
||
for (editable_text, input_row) in &changed_inputs {
|
||
for (mut text, output_row) in &mut outputs {
|
||
if output_row.0 == input_row.0 {
|
||
// `EditableText::value()` returns a `SplitString` because Parley may keep IME preedit text
|
||
// in a contiguous range of the editor’s internal `String` buffer during composition.
|
||
// The returned `SplitString` omits that preedit range, exposing only the text before and after it.
|
||
//
|
||
// To avoid allocating a new `String`, we reserve the total length of the `SplitString`'s slices,
|
||
// then append them to the output `Text`.
|
||
text.0.clear();
|
||
text.0
|
||
.reserve(editable_text.value().into_iter().map(str::len).sum());
|
||
for sub_str in editable_text.value() {
|
||
text.0.push_str(sub_str);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Submit the focused input's text when Enter is pressed.
|
||
fn submit_text(
|
||
mut input_focus: ResMut<InputFocus>,
|
||
keyboard_input: Res<ButtonInput<Key>>,
|
||
mut text_input: Query<(&mut EditableText, &TextInputRow)>,
|
||
mut text_output: Query<(&mut Text, &TextInputRow), With<SubmitOutput>>,
|
||
tab_navigation: TabNavigation,
|
||
) {
|
||
if keyboard_input.just_pressed(Key::Enter)
|
||
&& let Some(focused_entity) = input_focus.get()
|
||
&& let Ok((mut editable_text, input_row)) = text_input.get_mut(focused_entity)
|
||
{
|
||
for (mut text, output_row) in &mut text_output {
|
||
if input_row.0 == output_row.0 {
|
||
text.0.clear();
|
||
text.0
|
||
.reserve(editable_text.value().into_iter().map(str::len).sum());
|
||
for sub_str in editable_text.value() {
|
||
text.0.push_str(sub_str);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
editable_text.clear();
|
||
|
||
if let Ok(next) = tab_navigation.navigate(&input_focus, NavAction::Next) {
|
||
input_focus.set(next, FocusCause::Navigated);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Dim a row's border colors when its [`EditableText`] does not have input focus.
|
||
fn update_row_border_colors(
|
||
input_focus: Res<InputFocus>,
|
||
input_rows: Query<&TextInputRow, With<EditableText>>,
|
||
mut row_borders: Query<(&TextInputRow, &mut BorderColor, Has<EditableText>)>,
|
||
) {
|
||
if !input_focus.is_changed() {
|
||
return;
|
||
}
|
||
|
||
let focused_row = input_focus
|
||
.get()
|
||
.and_then(|focused_entity| input_rows.get(focused_entity).ok())
|
||
.map(|row| row.0);
|
||
|
||
for (row, mut border_color, is_input) in &mut row_borders {
|
||
let mut color = if is_input {
|
||
SLATE_300.into()
|
||
} else {
|
||
Color::WHITE
|
||
};
|
||
if Some(row.0) != focused_row {
|
||
color = color.darker(0.75);
|
||
}
|
||
border_color.set_all(color);
|
||
}
|
||
}
|