Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/bevy_feathers/src/controls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod color_slider;
mod color_swatch;
mod radio;
mod slider;
mod text_input;
mod toggle_switch;
mod virtual_keyboard;

Expand All @@ -23,6 +24,7 @@ pub use color_swatch::{
};
pub use radio::{radio, radio_bundle, RadioPlugin};
pub use slider::{slider, slider_bundle, SliderPlugin, SliderProps};
pub use text_input::{text_input, text_input_container, TextInputPlugin, TextInputProps};
pub use toggle_switch::{toggle_switch, toggle_switch_bundle, ToggleSwitchPlugin};
pub use virtual_keyboard::{virtual_keyboard, virtual_keyboard_bundle, VirtualKeyPressed};

Expand All @@ -46,6 +48,7 @@ impl Plugin for ControlsPlugin {
ColorSwatchPlugin,
RadioPlugin,
SliderPlugin,
TextInputPlugin,
ToggleSwitchPlugin,
));
}
Expand Down
214 changes: 214 additions & 0 deletions crates/bevy_feathers/src/controls/text_input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
use bevy_app::{Plugin, PreUpdate};
use bevy_ecs::{
change_detection::{DetectChanges, DetectChangesMut},
component::Component,
entity::Entity,
hierarchy::ChildOf,
lifecycle::RemovedComponents,
query::{Added, Has, With},
schedule::IntoScheduleConfigs,
system::{Commands, Query, Res},
};
use bevy_input_focus::{tab_navigation::TabIndex, InputFocus, InputFocusVisible};
use bevy_picking::PickingSystems;
use bevy_scene::prelude::*;
use bevy_text::{EditableText, FontSize, FontWeight, LineBreak, TextCursorStyle, TextLayout};
use bevy_ui::{
px, AlignItems, BorderColor, BorderRadius, Display, InteractionDisabled, JustifyContent, Node,
UiRect, Val,
};

use crate::{
constants::{fonts, size},
cursor::EntityCursor,
font_styles::InheritableFont,
theme::{ThemeBackgroundColor, ThemeBorderColor, ThemeFontColor, ThemedText, UiTheme},
tokens,
};

/// Marker to indicate a text input widget with feathers styling.
#[derive(Component, Default, Clone)]
struct FeathersTextInputContainer;

/// Marker to indicate a the inner part of the text input widget.
#[derive(Component, Default, Clone)]
struct FeathersTextInput;

/// Parameters for the text input template, passed to [`text_input`] function.
pub struct TextInputProps {
/// Max characters
pub max_characters: Option<usize>,
}

/// Decorative frame around a text input widget. This is a separate entity to allow icons
/// (such as "search" or "clear") to be inserted adjacent to the input.
pub fn text_input_container() -> impl Scene {
bsn! {
Node {
height: size::ROW_HEIGHT,
display: Display::Flex,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: UiRect::axes(Val::Px(4.0), Val::Px(0.)),
border: UiRect::all(Val::Px(2.0)),
flex_grow: 1.0,
border_radius: {BorderRadius::all(px(4.0))},
column_gap: px(4),
}
FeathersTextInputContainer
ThemeBorderColor(tokens::TEXT_INPUT_BG)
ThemeBackgroundColor(tokens::TEXT_INPUT_BG)
ThemeFontColor(tokens::TEXT_INPUT_TEXT)
InheritableFont {
font: fonts::REGULAR,
font_size: FontSize::Px(13.0),
weight: FontWeight::NORMAL,
}
}
}

/// Scene function to spawn a text input. For proper styling, this should be enclosed by a
/// `text_input_container`.
///
/// ```ignore
/// :text_input_container
/// Children [
/// text_input(props)
/// ]
/// ```
///
/// # Arguments
/// * `props` - construction properties for the text input.
pub fn text_input(props: TextInputProps) -> impl Scene {
bsn! {
Node {
flex_grow: 1.0,
}
FeathersTextInput
EditableText {
cursor_width: 0.3,
max_characters: {props.max_characters},
}
TextLayout {
linebreak: LineBreak::NoWrap,
}
TabIndex(0)
ThemedText
EntityCursor::System(bevy_window::SystemCursorIcon::Text)
TextCursorStyle::default()
}
}

fn update_text_cursor_color(
mut q_text_input: Query<&mut TextCursorStyle, With<FeathersTextInput>>,
theme: Res<UiTheme>,
) {
if theme.is_changed() {
for mut cursor_style in q_text_input.iter_mut() {
cursor_style.color = theme.color(&tokens::TEXT_INPUT_CURSOR);
cursor_style.selection_color = theme.color(&tokens::TEXT_INPUT_SELECTION);
}
}
}

fn update_text_input_styles(
q_inputs: Query<
(Entity, Has<InteractionDisabled>, &ThemeFontColor),
(With<FeathersTextInput>, Added<InteractionDisabled>),
>,
mut commands: Commands,
) {
for (input_ent, disabled, font_color) in q_inputs.iter() {
set_text_input_styles(input_ent, disabled, font_color, &mut commands);
}
}

fn update_text_input_styles_remove(
q_inputs: Query<(Entity, Has<InteractionDisabled>, &ThemeFontColor), With<FeathersTextInput>>,
mut removed_disabled: RemovedComponents<InteractionDisabled>,
mut commands: Commands,
) {
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);
}
});
}

fn update_text_input_focus(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we hook into the existing FocusIndicator infrastructure here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't, for two reasons:

  • This detects a focused descendant rather than a focused ancestor
  • The highlight uses an inset rect rather than an outline

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kk, that's fine, but leave a comment to this effect please :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment added; I suppose it's possible to generalize FocusIndicator in some way, but it seems like it would be complicated. In CSS this would be handled by the :focus-within pseudo-class.

q_inputs: Query<(), With<FeathersTextInput>>,
q_input_containers: Query<(Entity, &mut BorderColor), With<FeathersTextInputContainer>>,
parents: Query<&ChildOf>,
focus: Res<InputFocus>,
focus_visible: Res<InputFocusVisible>,
theme: Res<UiTheme>,
) {
if focus.is_changed() {
let focus_parent = focus.0.and_then(|focus_ent| {
if focus_visible.0 && q_inputs.contains(focus_ent) {
parents
.iter_ancestors(focus_ent)
.find(|ent| q_input_containers.contains(*ent))
} else {
None
}
});

for (container, mut border_color) in q_input_containers {
let new_border_color = if Some(container) == focus_parent {
theme.color(&tokens::FOCUS_RING)
} else {
theme.color(&tokens::TEXT_INPUT_BG)
};

border_color.set_if_neq(BorderColor::all(new_border_color));
}
}
}

fn set_text_input_styles(
input_ent: Entity,
disabled: bool,
font_color: &ThemeFontColor,
commands: &mut Commands,
) {
let font_color_token = match disabled {
true => tokens::TEXT_INPUT_TEXT_DISABLED,
false => tokens::TEXT_INPUT_TEXT,
};

let cursor_shape = match disabled {
true => bevy_window::SystemCursorIcon::NotAllowed,
false => bevy_window::SystemCursorIcon::Text,
};

// Change font color
if font_color.0 != font_color_token {
commands
.entity(input_ent)
.insert(ThemeFontColor(font_color_token));
}

// Change cursor shape
commands
.entity(input_ent)
.insert(EntityCursor::System(cursor_shape));
}

/// Plugin which registers the systems for updating the text input styles.
pub struct TextInputPlugin;

impl Plugin for TextInputPlugin {
fn build(&self, app: &mut bevy_app::App) {
app.add_systems(
PreUpdate,
(
update_text_cursor_color,
update_text_input_styles,
update_text_input_styles_remove,
update_text_input_focus,
)
.in_set(PickingSystems::Last),
);
}
}
9 changes: 9 additions & 0 deletions crates/bevy_feathers/src/dark_theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,15 @@ pub fn create_dark_theme() -> ThemeProps {
palette::LIGHT_GRAY_2.with_alpha(0.3),
),
(tokens::COLOR_PLANE_BG, palette::GRAY_1),
// Text Input
(tokens::TEXT_INPUT_BG, palette::GRAY_1),
(tokens::TEXT_INPUT_TEXT, palette::LIGHT_GRAY_1),
(
tokens::TEXT_INPUT_TEXT_DISABLED,
palette::WHITE.with_alpha(0.5),
),
(tokens::TEXT_INPUT_CURSOR, palette::ACCENT),
(tokens::TEXT_INPUT_SELECTION, palette::GRAY_2),
]),
}
}
14 changes: 14 additions & 0 deletions crates/bevy_feathers/src/tokens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,17 @@ pub const SWITCH_SLIDE_DISABLED: ThemeToken =

/// Color plane frame background
pub const COLOR_PLANE_BG: ThemeToken = ThemeToken::new_static("feathers.colorplane.bg");

// Text Input

/// Background for text input
pub const TEXT_INPUT_BG: ThemeToken = ThemeToken::new_static("feathers.textinput.bg");
/// Text color for text input
pub const TEXT_INPUT_TEXT: ThemeToken = ThemeToken::new_static("feathers.textinput.text");
/// Text color for text input (disabled)
pub const TEXT_INPUT_TEXT_DISABLED: ThemeToken =
ThemeToken::new_static("feathers.textinput.text.disabled");
/// Cursor color for text input
pub const TEXT_INPUT_CURSOR: ThemeToken = ThemeToken::new_static("feathers.textinput.cursor");
/// Selection color for text input
pub const TEXT_INPUT_SELECTION: ThemeToken = ThemeToken::new_static("feathers.textinput.selection");
4 changes: 2 additions & 2 deletions crates/bevy_text/src/text_editable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub struct Clipboard(pub String);
/// which manages both the text content and the cursor position,
/// and provides methods for applying text edits and cursor movements correctly
/// according to Unicode rules.
#[derive(Component)]
#[derive(Component, Clone)]
#[require(TextLayout, TextFont, TextColor, LineHeight, FontHinting)]
pub struct EditableText {
/// A [`parley::PlainEditor`], tracking both the text content and cursor position.
Expand Down Expand Up @@ -232,6 +232,6 @@ pub fn apply_text_edits(
///
/// As [`TextEdit`] includes cursor motions, this will be emitted even if [`EditableText::value`] is unchanged.
#[derive(EntityEvent)]
struct TextEditChange {
pub struct TextEditChange {
entity: Entity,
}
Loading
Loading