use bevy::prelude::*;
use crate::ui::icons::{ICON_ADD, ICON_ARROW_DOWN};
use crate::ui::tokens::{BORDER_COLOR, FONT_PATH, TEXT_DISPLAY_COLOR, TEXT_SIZE};
use crate::ui::widgets::button::{ButtonClickEvent, ButtonVariant, IconButtonProps, icon_button};
pub fn plugin(app: &mut App) {
app.add_systems(Update, setup_panel_section_buttons);
}
#[derive(Component)]
pub struct EditorPanelSection;
#[derive(Component)]
struct PanelSectionHeader;
#[derive(Component)]
struct PanelSectionButtonsContainer;
#[derive(Component)]
pub struct PanelSectionAddButton(pub Entity);
#[derive(Component)]
struct PanelSectionCollapseButton(Entity);
#[derive(Component, Default)]
struct Collapsed(bool);
#[derive(Component)]
struct PanelSectionState {
has_add_button: bool,
collapsible: bool,
}
#[derive(Default, Clone, Copy)]
pub enum PanelSectionSize {
#[default]
MD,
XL,
}
impl PanelSectionSize {
fn padding(&self) -> UiRect {
match self {
Self::MD => UiRect::all(px(12)),
Self::XL => UiRect::axes(px(24), px(14)),
}
}
}
#[derive(Default)]
pub struct PanelSectionProps {
pub title: String,
pub size: PanelSectionSize,
pub has_add_button: bool,
pub collapsible: bool,
}
impl PanelSectionProps {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
..default()
}
}
pub fn with_size(mut self, size: PanelSectionSize) -> Self {
self.size = size;
self
}
pub fn with_add_button(mut self) -> Self {
self.has_add_button = true;
self
}
pub fn collapsible(mut self) -> Self {
self.collapsible = true;
self
}
}
pub fn panel_section(props: PanelSectionProps, asset_server: &AssetServer) -> impl Bundle {
let PanelSectionProps {
title,
size,
has_add_button,
collapsible,
} = props;
let font: Handle<Font> = asset_server.load(FONT_PATH);
(
EditorPanelSection,
Collapsed::default(),
Node {
width: percent(100),
flex_direction: FlexDirection::Column,
row_gap: px(12),
padding: size.padding(),
border: UiRect::bottom(px(1)),
..default()
},
BorderColor::all(BORDER_COLOR),
PanelSectionState {
has_add_button,
collapsible,
},
children![(
PanelSectionHeader,
Node {
width: percent(100),
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
..default()
},
children![
(
Text::new(title),
TextFont {
font: font.into(),
font_size: TEXT_SIZE,
weight: FontWeight::SEMIBOLD,
..default()
},
TextColor(TEXT_DISPLAY_COLOR.into()),
),
(
PanelSectionButtonsContainer,
Node {
align_items: AlignItems::Center,
..default()
},
),
],
)],
)
}
fn setup_panel_section_buttons(
mut commands: Commands,
asset_server: Res<AssetServer>,
new_sections: Query<(Entity, &PanelSectionState, &Children), Added<EditorPanelSection>>,
headers: Query<&Children, With<PanelSectionHeader>>,
containers: Query<Entity, With<PanelSectionButtonsContainer>>,
) {
for (section_entity, state, section_children) in &new_sections {
let Some(&header_entity) = section_children.first() else {
continue;
};
let Ok(header_children) = headers.get(header_entity) else {
continue;
};
let Some(&container_entity) = header_children.get(1) else {
continue;
};
if containers.get(container_entity).is_err() {
continue;
}
if state.has_add_button {
let add_entity = commands
.spawn((
PanelSectionAddButton(section_entity),
icon_button(
IconButtonProps::new(ICON_ADD).variant(ButtonVariant::Ghost),
&asset_server,
),
))
.observe(on_add_click)
.id();
commands.entity(container_entity).add_child(add_entity);
}
if state.collapsible {
let collapse_entity = commands
.spawn((
PanelSectionCollapseButton(section_entity),
UiTransform {
rotation: Rot2::degrees(180.0),
..default()
},
icon_button(
IconButtonProps::new(ICON_ARROW_DOWN).variant(ButtonVariant::Ghost),
&asset_server,
),
))
.observe(on_collapse_click)
.id();
commands.entity(container_entity).add_child(collapse_entity);
}
}
}
fn on_add_click(
event: On<ButtonClickEvent>,
add_buttons: Query<&PanelSectionAddButton>,
mut commands: Commands,
) {
let Ok(add_button) = add_buttons.get(event.entity) else {
return;
};
commands.trigger(ButtonClickEvent {
entity: add_button.0,
});
}
fn on_collapse_click(
event: On<ButtonClickEvent>,
collapse_buttons: Query<&PanelSectionCollapseButton>,
mut sections: Query<(&mut Collapsed, &Children), With<EditorPanelSection>>,
mut nodes: Query<&mut Node, Without<PanelSectionHeader>>,
headers: Query<Entity, With<PanelSectionHeader>>,
mut button_transforms: Query<&mut UiTransform>,
) {
let button_entity = event.entity;
let Ok(collapse_button) = collapse_buttons.get(button_entity) else {
return;
};
let Ok((mut collapsed, section_children)) = sections.get_mut(collapse_button.0) else {
return;
};
collapsed.0 = !collapsed.0;
for child in section_children.iter() {
if headers.get(child).is_ok() {
continue;
}
if let Ok(mut node) = nodes.get_mut(child) {
node.display = if collapsed.0 {
Display::None
} else {
Display::Flex
};
}
}
if let Ok(mut transform) = button_transforms.get_mut(button_entity) {
transform.rotation = if collapsed.0 {
Rot2::degrees(0.0)
} else {
Rot2::degrees(180.0)
};
}
}