You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
718 lines
25 KiB
Rust
718 lines
25 KiB
Rust
/// TODO:
|
|
/// * Text box w/ "reset" button
|
|
/// * Textbox (ReadWrite)
|
|
/// * Move code to submodules
|
|
///
|
|
use bevy::{
|
|
asset::Asset,
|
|
input::mouse::{MouseScrollUnit, MouseWheel},
|
|
prelude::*,
|
|
window::PrimaryWindow,
|
|
};
|
|
use std::cmp::PartialEq;
|
|
|
|
#[derive(Debug, Component, PartialEq)]
|
|
pub struct TargetAsset<T: Asset> {
|
|
pub handle: Handle<T>,
|
|
}
|
|
|
|
#[derive(Debug, Component, PartialEq)]
|
|
pub struct TargetEntity {
|
|
pub entity: Entity,
|
|
}
|
|
|
|
/// UI Plugin
|
|
#[derive(Default)]
|
|
pub struct GameUiPlugin {
|
|
pub enable_alerts: bool,
|
|
}
|
|
|
|
impl Plugin for GameUiPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_systems(PreUpdate, create_titles)
|
|
.add_systems(
|
|
Update,
|
|
(
|
|
manage_titles,
|
|
manage_button_interaction,
|
|
manage_select_active,
|
|
manage_cursor,
|
|
manage_scroll,
|
|
manage_collapse_hiding,
|
|
manage_collapse_active,
|
|
show_child_number,
|
|
manage_sort,
|
|
),
|
|
)
|
|
.add_systems(PostUpdate, close_window);
|
|
|
|
if self.enable_alerts {
|
|
app.add_event::<Alert>()
|
|
.add_systems(Startup, init_alerts)
|
|
.add_systems(Update, spawn_alert);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub use title::*;
|
|
mod title {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component, Default)]
|
|
pub struct Title {
|
|
pub text: String,
|
|
pub font: Option<Handle<Font>>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct TitleBarBase {
|
|
bg_color: Color,
|
|
}
|
|
|
|
impl TitleBarBase {
|
|
pub fn new(bg_color: Color) -> Self {
|
|
Self { bg_color }
|
|
}
|
|
|
|
pub fn bundle(&self) -> NodeBundle {
|
|
NodeBundle {
|
|
style: Style {
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(1.0)),
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
flex_direction: FlexDirection::Row,
|
|
align_items: AlignItems::Center,
|
|
align_content: AlignContent::Center,
|
|
justify_content: JustifyContent::SpaceBetween,
|
|
..default()
|
|
},
|
|
background_color: self.bg_color.into(),
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct Note {
|
|
pub text: String,
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct TitleText;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct Minimize {
|
|
pub target: Entity,
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct Close {
|
|
pub target: Entity,
|
|
}
|
|
|
|
pub fn create_titles(
|
|
events: Query<
|
|
(
|
|
Entity,
|
|
&Title,
|
|
Option<&Note>,
|
|
Option<&Minimize>,
|
|
Option<&Close>,
|
|
),
|
|
Added<Title>,
|
|
>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.for_each(|(entity, title, note, minimize, close)| {
|
|
commands.entity(entity).with_children(|parent| {
|
|
let style = match &title.font {
|
|
Some(handle) => TextStyle {
|
|
color: Color::BLACK,
|
|
font: handle.clone(),
|
|
..default()
|
|
},
|
|
None => TextStyle {
|
|
color: Color::BLACK,
|
|
..default()
|
|
},
|
|
};
|
|
parent.spawn((
|
|
TextBundle {
|
|
text: Text {
|
|
sections: vec![
|
|
TextSection {
|
|
value: title.text.clone(),
|
|
style,
|
|
},
|
|
TextSection {
|
|
value: note
|
|
.unwrap_or(&Note {
|
|
text: String::new(),
|
|
})
|
|
.text
|
|
.clone(),
|
|
style: TextStyle {
|
|
color: Color::BLACK,
|
|
..default()
|
|
},
|
|
},
|
|
],
|
|
..default()
|
|
},
|
|
style: Style {
|
|
margin: UiRect::all(Val::Px(3.0)),
|
|
padding: UiRect::all(Val::Px(3.0)),
|
|
left: Val::Px(0.0),
|
|
..default()
|
|
},
|
|
..default()
|
|
},
|
|
Sorting(0),
|
|
TitleText,
|
|
));
|
|
if minimize.is_some() || close.is_some() {
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
// border: UiRect::all(Val::Px(1.0)),
|
|
// margin: UiRect::all(Val::Px(1.0)),
|
|
// padding: UiRect::all(Val::Px(1.0)),
|
|
right: Val::Px(0.0),
|
|
..default()
|
|
},
|
|
background_color: Color::WHITE.into(),
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
if let Some(Minimize { target }) = minimize {
|
|
parent
|
|
.spawn((
|
|
ButtonBundle {
|
|
style: Style {
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
// margin: UiRect::all(Val::Px(1.0)),
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
..default()
|
|
},
|
|
background_color: Color::WHITE.into(),
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
},
|
|
Collapse { target: *target },
|
|
Active,
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn(TextBundle::from_section(
|
|
"-",
|
|
TextStyle {
|
|
color: Color::BLACK,
|
|
..default()
|
|
},
|
|
));
|
|
});
|
|
}
|
|
if let Some(Close { target }) = close {
|
|
parent
|
|
.spawn((
|
|
ButtonBundle {
|
|
style: Style {
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
// margin: UiRect::all(Val::Px(1.0)),
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
..default()
|
|
},
|
|
background_color: Color::WHITE.into(),
|
|
border_color: Color::BLACK.into(),
|
|
..default()
|
|
},
|
|
Close { target: *target },
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn(TextBundle::from_section(
|
|
"x",
|
|
TextStyle {
|
|
color: Color::BLACK,
|
|
..default()
|
|
},
|
|
));
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn manage_titles(
|
|
events: Query<(&Title, Option<&Note>, &Children), Or<(Changed<Title>, Changed<Note>)>>,
|
|
mut texts: Query<&mut Text, With<TitleText>>,
|
|
) {
|
|
events.iter().for_each(|(title, note, children)| {
|
|
children.iter().for_each(|child| {
|
|
if let Ok(mut text) = texts.get_mut(*child) {
|
|
let style = match &title.font {
|
|
Some(handle) => TextStyle {
|
|
color: Color::BLACK,
|
|
font: handle.clone(),
|
|
..default()
|
|
},
|
|
None => TextStyle {
|
|
color: Color::BLACK,
|
|
..default()
|
|
},
|
|
};
|
|
*text = Text {
|
|
sections: vec![
|
|
TextSection {
|
|
value: title.text.clone(),
|
|
style: style,
|
|
},
|
|
TextSection {
|
|
value: note
|
|
.unwrap_or(&Note {
|
|
text: String::new(),
|
|
})
|
|
.text
|
|
.clone(),
|
|
style: TextStyle {
|
|
color: Color::BLACK,
|
|
..default()
|
|
},
|
|
},
|
|
],
|
|
..default()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn close_window(
|
|
events: Query<(&Interaction, &Close), (With<Button>, Changed<Interaction>)>,
|
|
mut commands: Commands,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|(&interaction, _)| interaction == Interaction::Pressed)
|
|
.for_each(|(_, Close { target })| {
|
|
commands.entity(*target).despawn_recursive();
|
|
});
|
|
}
|
|
}
|
|
|
|
pub use collapse::*;
|
|
mod collapse {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct Collapse {
|
|
pub target: Entity,
|
|
}
|
|
|
|
pub fn manage_collapse_active(
|
|
events: Query<
|
|
(Entity, Option<&Active>, &Interaction),
|
|
(Changed<Interaction>, With<Button>, With<Collapse>),
|
|
>,
|
|
mut commands: Commands,
|
|
) {
|
|
events
|
|
.iter()
|
|
.filter(|(_, _, &interaction)| interaction == Interaction::Pressed)
|
|
.for_each(|(entity, active, _)| {
|
|
match active {
|
|
Some(_) => {
|
|
commands.entity(entity).remove::<Active>();
|
|
}
|
|
None => {
|
|
// Set this butotn to active
|
|
commands.entity(entity).insert(Active);
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
pub fn manage_collapse_hiding(
|
|
added: Query<Entity, (Added<Active>, With<Collapse>)>,
|
|
mut removed: RemovedComponents<Active>,
|
|
collapses: Query<&Collapse, With<Button>>,
|
|
mut styles: Query<&mut Style>,
|
|
) {
|
|
// Removed collapse, hide the target entity
|
|
removed.iter().for_each(|e| {
|
|
if let Ok(Collapse { target }) = collapses.get(e) {
|
|
if let Ok(mut style) = styles.get_mut(*target) {
|
|
style.display = Display::None;
|
|
}
|
|
}
|
|
});
|
|
// Added collapse, display the target entity
|
|
added.iter().for_each(|e| {
|
|
if let Ok(Collapse { target }) = collapses.get(e) {
|
|
if let Ok(mut style) = styles.get_mut(*target) {
|
|
style.display = Display::Flex;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
pub fn show_child_number(
|
|
events: Query<(Entity, &Children), (Changed<Children>, With<Node>)>,
|
|
mut tabs: Query<(Entity, &Collapse)>,
|
|
mut commands: Commands,
|
|
) {
|
|
// Any time a widget changes
|
|
events.iter().for_each(|(entity, children)| {
|
|
// Find any tabs which have this as a target
|
|
tabs.iter_mut()
|
|
.find_map(|(button, collapse)| (entity == collapse.target).then_some(button))
|
|
.into_iter()
|
|
.for_each(|button| {
|
|
let num_children = children.len();
|
|
commands.entity(button).insert(Note {
|
|
text: format!("({})", num_children),
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
pub use buttons::*;
|
|
mod buttons {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct Active;
|
|
|
|
pub fn manage_button_interaction(
|
|
events: Query<
|
|
(Entity, &Interaction, Option<&Active>),
|
|
(Changed<Interaction>, With<Button>),
|
|
>,
|
|
added_active: Query<Entity, Added<Active>>,
|
|
mut removed_active: RemovedComponents<Active>,
|
|
mut bg_colors: Query<&mut BackgroundColor>,
|
|
) {
|
|
removed_active.iter().for_each(|e| {
|
|
if let Ok(mut bg_color) = bg_colors.get_mut(e) {
|
|
*bg_color = Color::WHITE.into();
|
|
}
|
|
});
|
|
added_active.for_each(|e| {
|
|
if let Ok(mut bg_color) = bg_colors.get_mut(e) {
|
|
*bg_color = Color::ORANGE.into();
|
|
}
|
|
});
|
|
events.for_each(|(entity, interaction, active)| {
|
|
if let Ok(mut bg_color) = bg_colors.get_mut(entity) {
|
|
match active {
|
|
Some(_) => match interaction {
|
|
Interaction::None => *bg_color = Color::ORANGE.into(),
|
|
Interaction::Pressed => *bg_color = Color::YELLOW.into(),
|
|
Interaction::Hovered => *bg_color = Color::RED.into(),
|
|
},
|
|
None => match interaction {
|
|
Interaction::None => *bg_color = Color::WHITE.into(),
|
|
Interaction::Pressed => *bg_color = Color::WHITE.into(),
|
|
Interaction::Hovered => *bg_color = Color::GRAY.into(),
|
|
},
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/// Marks a container node as having single- or multi-active components
|
|
#[derive(Debug, Component)]
|
|
pub enum Select {
|
|
Multi,
|
|
Single,
|
|
Action,
|
|
}
|
|
|
|
pub fn manage_select_active(
|
|
events: Query<
|
|
(Entity, &Parent, Option<&Active>, &Interaction),
|
|
(With<Button>, Changed<Interaction>),
|
|
>,
|
|
selects: Query<(&Select, &Children)>,
|
|
mut commands: Commands,
|
|
) {
|
|
events
|
|
.iter()
|
|
.for_each(|(entity, parent, active, interaction)| {
|
|
selects
|
|
.get(parent.get())
|
|
.iter()
|
|
.for_each(|(select, children)| match interaction {
|
|
Interaction::Pressed => match select {
|
|
Select::Multi | Select::Action => {
|
|
if active.is_some() {
|
|
commands.entity(entity).remove::<Active>();
|
|
} else {
|
|
commands.entity(entity).insert(Active);
|
|
}
|
|
}
|
|
Select::Single => {
|
|
if active.is_some() {
|
|
commands.entity(entity).remove::<Active>();
|
|
} else {
|
|
commands.entity(entity).insert(Active);
|
|
}
|
|
children.iter().filter(|&child| *child != entity).for_each(
|
|
|&child| {
|
|
commands.entity(child).remove::<Active>();
|
|
},
|
|
);
|
|
}
|
|
},
|
|
// A silly hack to get actions to maintain the active tag for 1+ frames
|
|
Interaction::None => match select {
|
|
Select::Action => {
|
|
commands.entity(entity).remove::<Active>();
|
|
}
|
|
_ => (),
|
|
},
|
|
_ => (),
|
|
});
|
|
});
|
|
}
|
|
|
|
/// run_if helper for simple button->trigger action
|
|
pub fn activated<T: Component>(events: Query<Entity, (With<T>, Added<Active>)>) -> bool {
|
|
events.iter().any(|_| true)
|
|
}
|
|
|
|
pub fn event<T: Event>(mut events: EventReader<T>) -> bool {
|
|
events.iter().len() > 0
|
|
}
|
|
}
|
|
|
|
pub use scroll::*;
|
|
mod scroll {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct Scroll;
|
|
|
|
pub fn manage_scroll(
|
|
mut events: EventReader<MouseWheel>,
|
|
mut query: Query<(&Interaction, &Parent, &Children, &mut Style), With<Scroll>>,
|
|
interactions: Query<&Interaction>,
|
|
) {
|
|
events.iter().for_each(|MouseWheel { unit, y, .. }| {
|
|
query
|
|
.iter_mut()
|
|
.filter(|(&interaction, parent, children, _)| {
|
|
let self_hover = interaction == Interaction::Hovered;
|
|
let child_hover = children.iter().any(|&child| {
|
|
if let Ok(&interaction) = interactions.get(child) {
|
|
interaction == Interaction::Hovered
|
|
} else {
|
|
false
|
|
}
|
|
});
|
|
let parent_hover = interactions
|
|
.get(parent.get())
|
|
.map_or(false, |&interaction| interaction == Interaction::Hovered);
|
|
self_hover || child_hover || parent_hover
|
|
})
|
|
.for_each(|(_, _, _, mut style)| {
|
|
let factor = 8.0;
|
|
let val = match unit {
|
|
MouseScrollUnit::Line => match style.top {
|
|
Val::Px(v) => v + (y * factor),
|
|
_ => (*y) * factor,
|
|
},
|
|
MouseScrollUnit::Pixel => match style.top {
|
|
Val::Px(v) => v + (y * factor),
|
|
_ => (*y) * factor,
|
|
},
|
|
};
|
|
style.top = Val::Px(val.min(0.0));
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
use cursor::*;
|
|
mod cursor {
|
|
use super::*;
|
|
|
|
pub fn manage_cursor(
|
|
events: Query<&Interaction, Changed<Interaction>>,
|
|
mut window: Query<&mut Window, With<PrimaryWindow>>,
|
|
) {
|
|
events.for_each(|interaction| {
|
|
let mut win = window.single_mut();
|
|
|
|
win.cursor.icon = match interaction {
|
|
Interaction::None => CursorIcon::Default,
|
|
Interaction::Hovered => CursorIcon::Hand,
|
|
Interaction::Pressed => CursorIcon::Grabbing,
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
pub use sort::*;
|
|
mod sort {
|
|
use std::cmp::Ordering;
|
|
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct Sorting(pub u8);
|
|
|
|
pub fn manage_sort(
|
|
mut events: Query<&mut Children, Changed<Children>>,
|
|
sorting: Query<Option<&Sorting>>,
|
|
) {
|
|
events.iter_mut().for_each(|mut children| {
|
|
children.sort_by(|&a, &b| match (sorting.get(a), sorting.get(b)) {
|
|
(Ok(Some(Sorting(ord_a))), Ok(Some(Sorting(ord_b)))) => {
|
|
if ord_a > ord_b {
|
|
Ordering::Greater
|
|
} else if ord_a < ord_b {
|
|
Ordering::Less
|
|
} else {
|
|
Ordering::Equal
|
|
}
|
|
}
|
|
(Ok(Some(_)), Err(_)) | (Ok(Some(_)), Ok(None)) => Ordering::Less,
|
|
(Err(_), Ok(Some(_))) | (Ok(None), Ok(Some(_))) => Ordering::Greater,
|
|
_ => Ordering::Equal,
|
|
});
|
|
})
|
|
}
|
|
}
|
|
|
|
pub use alert::*;
|
|
pub mod alert {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Event)]
|
|
pub enum Alert {
|
|
Info(String),
|
|
Warn(String),
|
|
Danger(String),
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
pub struct AlertsWidget;
|
|
|
|
pub fn init_alerts(mut commands: Commands) {
|
|
commands
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
top: Val::Px(0.0),
|
|
right: Val::Px(0.0),
|
|
position_type: PositionType::Absolute,
|
|
width: Val::Percent(33.0),
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(1.0)),
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
flex_direction: FlexDirection::Column,
|
|
justify_items: JustifyItems::Center,
|
|
..default()
|
|
},
|
|
border_color: Color::WHITE.into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
let container = parent
|
|
.spawn((
|
|
NodeBundle {
|
|
style: Style {
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(1.0)),
|
|
border: UiRect::all(Val::Px(1.0)),
|
|
flex_direction: FlexDirection::Column,
|
|
justify_items: JustifyItems::Center,
|
|
..default()
|
|
},
|
|
border_color: Color::WHITE.into(),
|
|
..default()
|
|
},
|
|
AlertsWidget,
|
|
))
|
|
.id();
|
|
parent.spawn((
|
|
TitleBarBase::new(Color::WHITE).bundle(),
|
|
Title {
|
|
text: "Alerts".into(),
|
|
..default()
|
|
},
|
|
Minimize { target: container },
|
|
Sorting(0),
|
|
));
|
|
});
|
|
}
|
|
|
|
pub fn spawn_alert(
|
|
mut events: EventReader<Alert>,
|
|
root: Query<Entity, With<AlertsWidget>>,
|
|
mut commands: Commands,
|
|
) {
|
|
events.iter().for_each(|alert| {
|
|
info!("Processing alert {:?}", alert);
|
|
|
|
let (color, text) = match alert {
|
|
Alert::Info(text) => (Color::BLUE, text),
|
|
Alert::Warn(text) => (Color::ORANGE, text),
|
|
Alert::Danger(text) => (Color::RED, text),
|
|
};
|
|
commands.entity(root.single()).with_children(|parent| {
|
|
parent
|
|
.spawn(NodeBundle {
|
|
style: Style {
|
|
width: Val::Percent(100.0),
|
|
padding: UiRect::all(Val::Px(1.0)),
|
|
margin: UiRect::all(Val::Px(1.0)),
|
|
border: UiRect::all(Val::Px(2.0)),
|
|
flex_direction: FlexDirection::Column,
|
|
justify_self: JustifySelf::Center,
|
|
..default()
|
|
},
|
|
background_color: Color::WHITE.into(),
|
|
border_color: color.into(),
|
|
..default()
|
|
})
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
TitleBarBase::new(color).bundle(),
|
|
Title {
|
|
text: "Heads up!".into(),
|
|
..default()
|
|
},
|
|
Close {
|
|
target: parent.parent_entity(),
|
|
},
|
|
Sorting(0),
|
|
));
|
|
parent.spawn(TextBundle::from_section(
|
|
text,
|
|
TextStyle {
|
|
color: Color::BLACK.into(),
|
|
..default()
|
|
},
|
|
));
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
use dragndrop::*;
|
|
pub mod dragndrop {
|
|
use super::*;
|
|
|
|
#[derive(Debug, Component)]
|
|
struct DragNDrop;
|
|
|
|
pub fn dragndrop() {
|
|
todo!()
|
|
}
|
|
}
|