use crate::element::{ElementProps, ElementWidget};
use crate::parser::color::parse_color;
use crate::parser::values::parse_attribute;
use crate::widgets::Widget;
use bevy::asset::AssetServer;
use bevy::color::Color;
use bevy::prelude::*;
use roxmltree::Node as XmlNode;
pub(crate) fn trigger_menu(
mut trigger_query: Query<
(&Interaction, &ChildOf),
(With<DropdownTrigger>, Changed<Interaction>),
>,
mut root_query: Query<&mut DropdownState>,
) {
for (interaction, child_of) in &mut trigger_query {
if *interaction == Interaction::Pressed
&& let Ok(mut state) = root_query.get_mut(child_of.0)
{
state.is_open = !state.is_open;
}
}
}
pub(crate) fn option_select(
interaction_query: Query<(Entity, &Interaction), Changed<Interaction>>,
child_of_query: Query<&ChildOf>,
menu_query: Query<&ChildOf, With<DropdownMenu>>,
mut root_query: Query<(&mut DropdownState, &Children)>,
trigger_query: Query<&Children, With<DropdownTrigger>>,
label_query: Query<Entity, With<DropdownLabelText>>,
mut text_query: Query<&mut Text>,
children_query: Query<&Children>,
) {
for (clicked_entity, interaction) in &interaction_query {
if *interaction != Interaction::Pressed {
continue;
}
if let Some(root_entity) = find_dropdown_root(clicked_entity, &child_of_query, &menu_query)
&& let Ok((mut state, root_children)) = root_query.get_mut(root_entity)
{
let extracted_label =
extract_text_from_entity(clicked_entity, &text_query, &children_query)
.unwrap_or_else(|| "Selected Option".to_string());
state.selected_value = extracted_label.clone();
state.selected_label = extracted_label.clone();
state.is_open = false;
for child in root_children.iter() {
if let Ok(trigger_children) = trigger_query.get(child) {
for trigger_child in trigger_children.iter() {
if label_query.contains(trigger_child)
&& let Ok(mut text) = text_query.get_mut(trigger_child)
{
**text = extracted_label.clone();
}
}
}
}
}
}
}
pub(crate) fn visibility(
root_query: Query<(&DropdownState, &Children), Changed<DropdownState>>,
mut menu_query: Query<(&mut Node, &mut ZIndex), With<DropdownMenu>>,
) {
for (state, children) in &root_query {
for child in children.iter() {
if let Ok((mut node, mut z_index)) = menu_query.get_mut(child) {
if state.is_open {
node.display = Display::Flex;
*z_index = ZIndex(100);
} else {
node.display = Display::None;
*z_index = ZIndex(0);
}
}
}
}
}
pub(crate) fn close_on_outside_click(
mouse_button: Res<ButtonInput<MouseButton>>,
mut root_query: Query<(Entity, &mut DropdownState)>,
interaction_query: Query<&Interaction>,
children_query: Query<&Children>,
) {
if !mouse_button.just_pressed(MouseButton::Left) {
return;
}
for (root_entity, mut state) in &mut root_query {
if !state.is_open {
continue;
}
let is_interacting =
is_hierarchy_interacting(root_entity, &interaction_query, &children_query);
if !is_interacting {
state.is_open = false;
}
}
}
fn find_dropdown_root(
start_entity: Entity,
child_of_query: &Query<&ChildOf>,
menu_query: &Query<&ChildOf, With<DropdownMenu>>,
) -> Option<Entity> {
let mut current = start_entity;
loop {
if let Ok(menu_child_of) = menu_query.get(current) {
return Some(menu_child_of.0); }
if let Ok(parent) = child_of_query.get(current) {
current = parent.0;
} else {
return None;
}
}
}
fn extract_text_from_entity(
entity: Entity,
text_query: &Query<&mut Text>,
children_query: &Query<&Children>,
) -> Option<String> {
if let Ok(text) = text_query.get(entity)
&& !text.0.is_empty()
{
return Some(text.0.clone());
}
if let Ok(children) = children_query.get(entity) {
for child in children.iter() {
if let Some(found) = extract_text_from_entity(child, text_query, children_query) {
return Some(found);
}
}
}
None
}
fn is_hierarchy_interacting(
entity: Entity,
interaction_query: &Query<&Interaction>,
children_query: &Query<&Children>,
) -> bool {
if let Ok(interaction) = interaction_query.get(entity)
&& matches!(interaction, Interaction::Hovered | Interaction::Pressed)
{
return true;
}
if let Ok(children) = children_query.get(entity) {
for child in children.iter() {
if is_hierarchy_interacting(child, interaction_query, children_query) {
return true;
}
}
}
false
}
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component)]
pub struct DropdownState {
pub is_open: bool,
pub selected_index: Option<usize>,
pub selected_value: String,
pub selected_label: String,
}
impl Default for DropdownState {
fn default() -> Self {
Self {
is_open: false,
selected_index: None,
selected_value: String::new(),
selected_label: "Select an option...".to_string(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct DropdownWidget {
pub placeholder: String,
pub bg_color: Option<Color>,
pub menu_bg_color: Option<Color>,
}
impl Default for DropdownWidget {
fn default() -> Self {
Self {
placeholder: "Select an option...".to_string(),
bg_color: Some(Color::srgb(0.2, 0.2, 0.2)),
menu_bg_color: Some(Color::srgb(0.15, 0.15, 0.15)),
}
}
}
impl Widget for DropdownWidget {
fn spawn(&self, commands: &mut EntityCommands, _assets: &AssetServer) -> Entity {
let root_entity = commands.id();
let bg_color = self.bg_color.unwrap_or(Color::srgb(0.2, 0.2, 0.2));
let menu_bg = self.menu_bg_color.unwrap_or(Color::srgb(0.15, 0.15, 0.15));
commands.insert(DropdownState {
selected_label: self.placeholder.clone(),
..default()
});
let mut menu_entity = None;
commands.with_children(|parent| {
parent
.spawn((
Transform::default(),
GlobalTransform::default(),
DropdownTrigger,
Interaction::None,
Node {
width: Val::Percent(100.0),
height: Val::Px(36.0),
padding: UiRect::axes(Val::Px(12.0), Val::Px(8.0)),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
border: UiRect::all(Val::Px(1.0)),
border_radius: BorderRadius::all(Val::Px(4.0)),
..default()
},
BackgroundColor(bg_color),
BorderColor::all(Color::srgb(0.4, 0.4, 0.4)),
))
.with_children(|trigger| {
trigger.spawn((
Transform::default(),
GlobalTransform::default(),
DropdownLabelText,
Text::new(&self.placeholder),
TextFont {
font_size: FontSize::Px(14.0),
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
));
trigger.spawn((
Transform::default(),
GlobalTransform::default(),
Text::new(">"),
TextFont {
font_size: FontSize::Px(10.0),
..default()
},
TextColor(Color::srgb(0.6, 0.6, 0.6)),
));
});
let menu_cmd = parent.spawn((
Transform::default(),
GlobalTransform::default(),
DropdownMenu,
Node {
display: Display::None,
position_type: PositionType::Absolute,
top: Val::Percent(100.0),
left: Val::Px(0.0),
width: Val::Percent(100.0),
max_height: Val::Px(200.0),
flex_direction: FlexDirection::Column,
overflow: Overflow::clip_y(),
margin: UiRect::top(Val::Px(4.0)),
border: UiRect::all(Val::Px(1.0)),
border_radius: BorderRadius::all(Val::Px(4.0)),
..default()
},
ZIndex(100),
BackgroundColor(menu_bg),
BorderColor::all(Color::srgb(0.3, 0.3, 0.3)),
));
menu_entity = Some(menu_cmd.id());
});
menu_entity.unwrap_or(root_entity)
}
fn parse(
node: &XmlNode,
prefix: Option<&str>,
_base: Option<&ElementWidget>,
) -> Result<Self, String> {
let placeholder = parse_attribute(node, "placeholder", prefix, |s| Ok(s.to_string()))?
.unwrap_or_else(|| "Select an option...".to_string());
let bg_color = parse_attribute(node, "bg-color", prefix, parse_color)?;
let menu_bg_color = parse_attribute(node, "menu-bg-color", prefix, parse_color)?;
Ok(Self {
placeholder,
bg_color,
menu_bg_color,
})
}
fn apply_defaults(
node: &XmlNode,
default: &mut ElementProps,
hover: &mut ElementProps,
click: &mut ElementProps,
) {
if !node.has_attribute("width") {
default.node.width = Val::Px(200.0);
hover.node.width = Val::Px(200.0);
click.node.width = Val::Px(200.0);
}
}
}
#[derive(Component, Debug, Clone, Copy)]
pub struct DropdownTrigger;
#[derive(Component, Debug, Clone, Copy)]
pub struct DropdownMenu;
#[derive(Component, Debug, Clone, Copy)]
pub struct DropdownLabelText;