Path: blob/main/crates/bevy_feathers/src/controls/text_input.rs
30636 views
use bevy_app::{Plugin, PreUpdate, PropagateOver};1use bevy_asset::AssetServer;2use bevy_ecs::{3change_detection::DetectChanges,4entity::Entity,5lifecycle::RemovedComponents,6query::{Added, Has, With},7schedule::IntoScheduleConfigs,8system::{Commands, Query, Res},9template::template,10};11use bevy_input_focus::tab_navigation::TabIndex;12use bevy_picking::PickingSystems;13use bevy_scene::prelude::*;14use bevy_text::{15EditableText, FontSource, FontWeight, LineBreak, TextCursorStyle, TextFont, TextLayout,16};17use bevy_ui::{18px, AlignItems, BorderRadius, Display, InteractionDisabled, JustifyContent, Node, UiRect,19};2021use crate::{22constants::{fonts, size},23cursor::EntityCursor,24focus::FocusWithinIndicator,25font_styles::InheritableFont,26theme::{InheritableThemeTextColor, ThemeBackgroundColor, UiTheme},27tokens,28};2930/// Decorative frame around a text input widget. This is a separate entity to allow icons31/// (such as "search" or "clear") to be inserted adjacent to the input.32///33/// This is spawnable by inheriting it as a "scene component".34#[derive(SceneComponent, Default, Clone)]35pub struct FeathersTextInputContainer;3637impl FeathersTextInputContainer {38fn scene() -> impl Scene {39bsn! {40Node {41height: size::ROW_HEIGHT,42display: Display::Flex,43justify_content: JustifyContent::Center,44align_items: AlignItems::Center,45padding: UiRect {46right: px(3.0),47},48border: UiRect {49left: px(3.0)50},51flex_grow: 1.0,52border_radius: {BorderRadius::all(px(4.0))},53column_gap: px(4),54}55FeathersTextInputContainer56FocusWithinIndicator57ThemeBackgroundColor(tokens::TEXT_INPUT_BG)58InheritableThemeTextColor(tokens::TEXT_INPUT_TEXT)59InheritableFont {60font: fonts::REGULAR,61font_size: size::COMPACT_FONT,62weight: FontWeight::NORMAL,63}64}65}66}6768/// Scene function to spawn a text input. For proper styling, this should be enclosed by a [`FeathersTextInputContainer`].69///70/// This is spawnable by inheriting it as a "scene component" with optional [`FeathersTextInputProps`].71///72/// ```ignore73/// :FeathersTextInputContainer74/// Children [75/// :FeathersTextInput76/// ]77/// ```78#[derive(SceneComponent, Default, Clone)]79#[scene(FeathersTextInputProps)]80pub struct FeathersTextInput;8182/// Props used to construct the [`FeathersTextInput`] scene.83#[derive(Default, Clone)]84pub struct FeathersTextInputProps {85/// Visible width86pub visible_width: Option<f32>,87/// Max characters88pub max_characters: Option<usize>,89}9091impl FeathersTextInput {92fn scene(props: FeathersTextInputProps) -> impl Scene {93bsn! {94Node {95flex_grow: {96if props.visible_width.is_some() {970.98} else {991.100}101} ,102}103FeathersTextInput104EditableText {105cursor_width: 0.3,106visible_width: {props.visible_width},107max_characters: {props.max_characters},108}109TextLayout {110linebreak: LineBreak::NoWrap,111}112TabIndex(0)113template(|ctx| {114Ok(TextFont {115font: FontSource::Handle(ctx.resource::<AssetServer>().load(fonts::REGULAR)),116font_size: size::COMPACT_FONT,117weight: FontWeight::NORMAL,118..Default::default()119})120})121PropagateOver<TextFont>122EntityCursor::System(bevy_window::SystemCursorIcon::Text)123TextCursorStyle::default()124}125}126}127128fn update_text_cursor_color(129mut q_text_input: Query<&mut TextCursorStyle, With<FeathersTextInput>>,130theme: Res<UiTheme>,131) {132if theme.is_changed() {133for mut cursor_style in q_text_input.iter_mut() {134cursor_style.color = theme.color(&tokens::TEXT_INPUT_CURSOR);135cursor_style.selection_color = theme.color(&tokens::TEXT_INPUT_SELECTION);136cursor_style.unfocused_selection_color =137theme.color(&tokens::TEXT_INPUT_SELECTION_UNFOCUSED);138}139}140}141142fn update_text_input_styles(143q_inputs: Query<144(Entity, Has<InteractionDisabled>, &InheritableThemeTextColor),145(With<FeathersTextInput>, Added<InteractionDisabled>),146>,147mut commands: Commands,148) {149for (input_ent, disabled, font_color) in q_inputs.iter() {150set_text_input_styles(input_ent, disabled, font_color, &mut commands);151}152}153154fn update_text_input_styles_remove(155q_inputs: Query<156(Entity, Has<InteractionDisabled>, &InheritableThemeTextColor),157With<FeathersTextInput>,158>,159mut removed_disabled: RemovedComponents<InteractionDisabled>,160mut commands: Commands,161) {162removed_disabled.read().for_each(|ent| {163if let Ok((input_ent, disabled, font_color)) = q_inputs.get(ent) {164set_text_input_styles(input_ent, disabled, font_color, &mut commands);165}166});167}168169fn set_text_input_styles(170input_ent: Entity,171disabled: bool,172font_color: &InheritableThemeTextColor,173commands: &mut Commands,174) {175let font_color_token = match disabled {176true => tokens::TEXT_INPUT_TEXT_DISABLED,177false => tokens::TEXT_INPUT_TEXT,178};179180let cursor_shape = match disabled {181true => bevy_window::SystemCursorIcon::NotAllowed,182false => bevy_window::SystemCursorIcon::Text,183};184185// Change font color186if font_color.0 != font_color_token {187commands188.entity(input_ent)189.insert(InheritableThemeTextColor(font_color_token));190}191192// Change cursor shape193commands194.entity(input_ent)195.insert(EntityCursor::System(cursor_shape));196}197198/// Plugin which registers the systems for updating the text input styles.199pub struct TextInputPlugin;200201impl Plugin for TextInputPlugin {202fn build(&self, app: &mut bevy_app::App) {203app.add_systems(204PreUpdate,205(206update_text_cursor_color,207update_text_input_styles,208update_text_input_styles_remove,209)210.in_set(PickingSystems::Last),211);212}213}214215216