UI (User Interface) / Multiline Text Input

Back to examples View in GitHub

Demonstrates a single, minimal multiline [EditableText] widget.

use bevy::color::palettes::css::DARK_SLATE_GRAY;
use bevy::color::palettes::tailwind::SLATE_300;
use bevy::input::keyboard::{Key, KeyboardInput};
use bevy::input_focus::tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin};
use bevy::input_focus::{AutoFocus, FocusedInput};
use bevy::prelude::*;
use bevy::text::{EditableText, EditableTextFilter, TextCursorStyle};
use bevy::ui_widgets::SelectAllOnFocus;

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, TabNavigationPlugin))
        .add_systems(Startup, setup)
        .run();
}

#[derive(Component)]
struct MultilineInput;

#[derive(Component)]
struct VisibleLinesInput;

#[derive(Component)]
struct FontSizeInput;

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);

    commands
        .spawn(Node {
            width: percent(100.),
            height: percent(100.),
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
            ..default()
        })
        .with_children(|parent| {
            parent
                .spawn((
                    Node {
                        flex_direction: FlexDirection::Column,
                        align_items: AlignItems::End,
                        row_gap: px(10.),
                        ..default()
                    },
                    TabGroup::default(),
                ))
                .with_children(|parent| {
                    parent
                        .spawn((
                            Node {
                                width: px(450.),
                                border: px(2.).all(),
                                padding: px(8.).all(),
                                ..default()
                            },
                            EditableText {
                                visible_lines: Some(8.),
                                allow_newlines: true,
                                ..default()
                            },
                            TextLayout {
                                linebreak: LineBreak::WordOrCharacter,
                                ..default()
                            },
                            TextCursorStyle {
                                color: Color::WHITE,
                                selected_text_color: Some(Color::BLACK),
                                ..default()
                            },
                            TextFont {
                                font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
                                font_size: FontSize::Px(30.),
                                ..default()
                            },
                            BackgroundColor(DARK_SLATE_GRAY.into()),
                            BorderColor::all(SLATE_300),
                            MultilineInput,
                            TabIndex(0),
                            AutoFocus,
                        ))
                        .observe(
                            |on: On<FocusedInput<KeyboardInput>>,
                             keys: Res<ButtonInput<Key>>,
                             input_query: Query<&EditableText, With<MultilineInput>>| {
                                if !(on.input.state.is_pressed()
                                    && on.input.logical_key == Key::Enter
                                    && keys.pressed(Key::Control))
                                {
                                    return;
                                }
                                let Ok(input) = input_query.get(on.focused_entity) else {
                                    return;
                                };

                                let mut output = String::new();
                                output.reserve(input.value().into_iter().map(str::len).sum());
                                for sub_str in input.value() {
                                    output.push_str(sub_str);
                                }

                                info!("{output}"                                    );
                            },
                        );

                    parent
                        .spawn((
                            Node {
                                flex_direction: FlexDirection::Row,
                                column_gap: px(10.),
                                ..default()
                            },
                            children![
                                (
                                    Text::new("visible lines:"),
                                    TextFont {
                                        font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
                                        font_size: FontSize::Px(30.),
                                        ..default()
                                    },
                                ),
                                (
                                    Node {
                                        width: px(100.),
                                        border: px(2.).all(),
                                        ..default()
                                    },
                                    TextFont {
                                        font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
                                        font_size: FontSize::Px(30.),
                                        ..default()
                                    },
                                    TextLayout {
                                        justify: Justify::End,
                                        ..default()
                                    },
                                    BackgroundColor(DARK_SLATE_GRAY.into()),
                                    BorderColor::all(SLATE_300),
                                    EditableText::new("8"),
                                    EditableTextFilter::new(|c| c.is_ascii_digit() || c == '.'),
                                    TextCursorStyle {
                                        color: Color::WHITE,
                                        selected_text_color: Some(Color::BLACK),
                                        unfocused_selection_color: Color::NONE,
                                        ..default()
                                    },
                                    SelectAllOnFocus,
                                    VisibleLinesInput,
                                    TabIndex(1),
                                )
                            ],
                        ))
                        .observe(
                            |on: On<FocusedInput<KeyboardInput>>,
                             mut query_set: ParamSet<(
                                Query<&EditableText, With<VisibleLinesInput>>,
                                Query<&mut EditableText, With<MultilineInput>>,
                            )>| {
                                if !(on.input.state.is_pressed()
                                    && on.input.logical_key == Key::Enter)
                                {
                                    return;
                                }

                                let visible_lines_query = query_set.p0();
                                let Ok(input) = visible_lines_query.get(on.original_event_target())
                                else {
                                    return;
                                };

                                let mut output = String::new();
                                output.reserve(input.value().into_iter().map(str::len).sum());
                                for sub_str in input.value() {
                                    output.push_str(sub_str);
                                }

                                let Ok(lines) = output.parse::<f32>() else {
                                    return;
                                };

                                let mut multiline_query = query_set.p1();
                                let Ok(mut multiline_input) = multiline_query.single_mut() else {
                                    return;
                                };

                                multiline_input.visible_lines = Some(lines.clamp(1., 10.));
                            },
                        );

                    parent
                        .spawn((
                            Node {
                                flex_direction: FlexDirection::Row,
                                column_gap: px(10.),
                                ..default()
                            },
                            children![
                                (
                                    Text::new("font size:"),
                                    TextFont {
                                        font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
                                        font_size: FontSize::Px(30.),
                                        ..default()
                                    },
                                ),
                                (
                                    Node {
                                        width: px(100.),
                                        border: px(2.).all(),
                                        ..default()
                                    },
                                    TextFont {
                                        font: asset_server.load("fonts/FiraMono-Medium.ttf").into(),
                                        font_size: FontSize::Px(30.),
                                        ..default()
                                    },
                                    TextLayout {
                                        justify: Justify::End,
                                        ..default()
                                    },
                                    BackgroundColor(DARK_SLATE_GRAY.into()),
                                    BorderColor::all(SLATE_300),
                                    EditableText::new("30"),
                                    EditableTextFilter::new(|c| c.is_ascii_digit()),
                                    TextCursorStyle {
                                        color: Color::WHITE,
                                        selected_text_color: Some(Color::BLACK),
                                        unfocused_selection_color: Color::NONE,
                                        ..default()
                                    },
                                    SelectAllOnFocus,
                                    FontSizeInput,
                                    TabIndex(2),
                                )
                            ],
                        ))
                        .observe(
                            |on: On<FocusedInput<KeyboardInput>>,
                             font_size_input_query: Query<&EditableText, With<FontSizeInput>>,
                             mut multiline_input_font: Single<
                                &mut TextFont,
                                With<MultilineInput>,
                            >| {
                                if !(on.input.state.is_pressed()
                                    && on.input.logical_key == Key::Enter)
                                {
                                    return;
                                }

                                let Ok(input) =
                                    font_size_input_query.get(on.original_event_target())
                                else {
                                    return;
                                };

                                let mut output = String::new();
                                output.reserve(input.value().into_iter().map(str::len).sum());
                                for sub_str in input.value() {
                                    output.push_str(sub_str);
                                }

                                let Ok(font_size) = output.parse::<f32>() else {
                                    return;
                                };

                                multiline_input_font.font_size =
                                    FontSize::Px(font_size.clamp(5., 50.));
                            },
                        );
                });
        });
}