use crate::element::{ElementId, ElementProps, ElementWidget};
use crate::events::{ElementClick, ElementSet};
use crate::parser::color::{lighten_color, parse_color};
use crate::parser::values::parse_attribute;
use crate::parser::values::parse_float;
use crate::widgets::Widget;
use bevy::color::Color;
use bevy::input::ButtonState;
use bevy::input::keyboard::{Key, KeyboardInput};
use bevy::prelude::*;
use bevy::ui::FocusPolicy;
use roxmltree::Node as XmlNode;
pub(crate) fn handle_focus(
trigger: On<ElementClick>,
mut all_inputs: Query<(Entity, &mut TextInputState, Option<&ElementId>)>,
) {
let clicked_entity = trigger.entity;
for (entity, mut state, _id) in all_inputs.iter_mut() {
if entity == clicked_entity {
if !state.is_focused {
state.is_focused = true;
}
} else if state.is_focused {
state.is_focused = false;
}
}
}
pub(crate) fn handle_click_outside(
mut commands: Commands,
mouse_button: Res<ButtonInput<MouseButton>>,
touches: Res<Touches>,
interaction_query: Query<&Interaction, With<TextInputState>>,
mut all_inputs: Query<(Entity, &mut TextInputState, Option<&ElementId>)>,
) {
let just_pressed = mouse_button.just_pressed(MouseButton::Left)
|| touches.iter_just_pressed().next().is_some();
if !just_pressed {
return;
}
let clicking_input = interaction_query.iter().any(|i| *i == Interaction::Pressed);
if !clicking_input {
for (entity, mut state, id) in all_inputs.iter_mut() {
if state.is_focused {
state.is_focused = false;
commands.trigger(ElementSet {
entity,
id: id.cloned(),
value: state.value.clone(),
delta: None,
});
}
}
}
}
pub(crate) fn handle_typing(
mut commands: Commands,
mut key_events: MessageReader<KeyboardInput>,
mut inputs: Query<(Entity, &mut TextInputState, Option<&ElementId>)>,
) {
for event in key_events.read() {
if event.state != ButtonState::Pressed {
continue;
}
for (entity, mut state, id) in inputs.iter_mut() {
if !state.is_focused {
continue;
}
match &event.logical_key {
Key::Backspace => {
state.value.pop();
}
Key::Enter => {
state.is_focused = false;
commands.trigger(ElementSet {
entity,
id: id.cloned(),
value: state.value.clone(),
delta: None,
});
}
Key::Character(input_str) => {
if !input_str.chars().any(|c| c.is_control()) {
state.value.push_str(input_str.as_str());
}
}
Key::Space => {
state.value.push(' ');
}
_ => {}
}
}
}
}
pub(crate) fn sync_visuals(
query: Query<
(Entity, &TextInputState, &TextInputWidget, &Children),
Or<(Changed<TextInputState>, Changed<Interaction>)>,
>,
mut border_query: Query<&mut BorderColor>,
mut text_query: Query<(&mut Text, &mut TextColor), With<TextInputText>>,
mut cursor_query: Query<&mut Node, With<TextInputCursor>>,
) {
for (entity, state, widget, children) in &query {
let text_color = widget.text_color.unwrap_or(Color::WHITE);
let placeholder_color = widget
.placeholder_color
.unwrap_or(Color::srgb(0.5, 0.5, 0.5));
if let Ok(mut border) = border_query.get_mut(entity)
&& state.is_focused
{
*border = BorderColor::all(Color::srgb(0.3, 0.6, 1.0));
}
for child in children.iter() {
if let Ok((mut text, mut color)) = text_query.get_mut(child) {
if state.value.is_empty() {
text.0 = state.placeholder.clone();
color.0 = placeholder_color;
} else {
text.0 = state.value.clone();
color.0 = text_color;
}
}
if let Ok(mut cursor_node) = cursor_query.get_mut(child) {
cursor_node.display = if state.is_focused {
Display::Flex
} else {
Display::None
};
}
}
}
}
#[derive(Component, Debug, Clone)]
pub struct TextInputState {
pub value: String,
pub is_focused: bool,
pub placeholder: String,
}
#[derive(Clone, Debug, Component)]
pub struct TextInputWidget {
pub initial_value: String,
pub placeholder: String,
pub text_color: Option<Color>,
pub placeholder_color: Option<Color>,
pub font_size: f32,
}
impl Widget for TextInputWidget {
fn spawn(&self, commands: &mut EntityCommands, _: &AssetServer) -> Entity {
let text_color = self.text_color.unwrap_or(Color::WHITE);
let placeholder_color = self.placeholder_color.unwrap_or(Color::srgb(0.5, 0.5, 0.5));
let display_text = if self.initial_value.is_empty() {
self.placeholder.clone()
} else {
self.initial_value.clone()
};
let active_color = if self.initial_value.is_empty() {
placeholder_color
} else {
text_color
};
commands.insert((
self.clone(),
TextInputState {
value: self.initial_value.clone(),
is_focused: false,
placeholder: self.placeholder.clone(),
},
FocusPolicy::Block,
));
commands.with_children(|parent| {
parent.spawn((
TextInputText,
Text::new(display_text),
TextFont {
font_size: FontSize::Px(self.font_size),
..Default::default()
},
TextColor(active_color),
Node {
align_self: AlignSelf::Center,
..Default::default()
},
FocusPolicy::Pass,
));
parent.spawn((
TextInputCursor,
Node {
width: Val::Px(2.0),
height: Val::Px(self.font_size),
margin: UiRect::left(Val::Px(2.0)),
display: Display::None,
align_self: AlignSelf::Center,
..Default::default()
},
BackgroundColor(text_color),
FocusPolicy::Pass,
));
});
commands.id()
}
fn parse(
node: &XmlNode,
prefix: Option<&str>,
base: Option<&ElementWidget>,
) -> Result<Self, String>
where
Self: Sized,
{
let base_ti = match base {
Some(ElementWidget::TextInput(ti)) => Some(ti),
_ => None,
};
let initial_value = parse_attribute(node, "value", prefix, |s| Ok(s.to_string()))?
.or_else(|| base_ti.map(|b| b.initial_value.clone()))
.unwrap_or_default();
let placeholder = parse_attribute(node, "placeholder", prefix, |s| Ok(s.to_string()))?
.or_else(|| base_ti.map(|b| b.placeholder.clone()))
.unwrap_or_default();
let text_color = parse_attribute(node, "text-color", prefix, parse_color)?
.or_else(|| base_ti.and_then(|b| b.text_color));
let placeholder_color = parse_attribute(node, "placeholder-color", prefix, parse_color)?
.or_else(|| base_ti.and_then(|b| b.placeholder_color));
let font_size = parse_attribute(node, "font-size", prefix, parse_float)?
.or_else(|| base_ti.map(|b| b.font_size))
.unwrap_or(14.0);
Ok(Self {
initial_value,
placeholder,
text_color,
placeholder_color,
font_size,
})
}
fn apply_defaults(
node: &XmlNode,
default: &mut ElementProps,
hover: &mut ElementProps,
click: &mut ElementProps,
) {
if !node.has_attribute("width") {
default.node.width = Val::Px(160.0);
}
if !node.has_attribute("height") {
default.node.height = Val::Px(32.0);
}
default.node.padding = UiRect::axes(Val::Px(8.0), Val::Px(4.0));
default.node.align_items = AlignItems::Center;
default.node.justify_content = JustifyContent::FlexStart;
hover.node.padding = default.node.padding;
click.node.padding = default.node.padding;
let base_bg = default
.bg_color
.unwrap_or_else(|| Color::srgb(0.12, 0.12, 0.14));
if default.bg_color.is_none() {
default.bg_color = Some(base_bg);
}
if !node.has_attribute("hover.bg-color") {
hover.bg_color = Some(lighten_color(base_bg, 0.05));
}
if default.border_color.is_none() {
default.border_color = Some(BorderColor::all(Color::srgb(0.3, 0.3, 0.35)));
}
if !node.has_attribute("hover.border-color") {
hover.border_color = Some(BorderColor::all(Color::srgb(0.5, 0.5, 0.6)));
}
if default.node.border == UiRect::ZERO {
let border_rect = UiRect::all(Val::Px(1.0));
default.node.border = border_rect;
hover.node.border = border_rect;
click.node.border = border_rect;
}
}
}
#[derive(Component, Debug, Clone, Copy)]
pub struct TextInputText;
#[derive(Component, Debug, Clone, Copy)]
pub struct TextInputCursor;