use bevy_ecs::observer::Observer;
use bevy_ecs::prelude::*;
use bevy_ecs::resource::IsResource;
use bevy_ecs::system::SystemIdMarker;
use bevy_ecs::world::EntityRef;
use yakui::CrossAxisAlignment;
use yakui::geometry::{Constraints, Vec2};
use yakui::widgets::List;
use super::color::yakui_color;
use super::resources::ScreenSize;
use super::widgets::{self, SCREEN_MARGIN, icon, text};
use crate::sceneobjects::{Category, SceneObject};
const WIDTH: f32 = 200.0;
const TOP: f32 = 64.0;
const ICON: f32 = 13.0;
const PX: f32 = 14.0;
#[derive(Resource, Default)]
pub struct Outliner {
pub open: bool,
}
impl Outliner {
pub fn is_open(&self) -> bool {
self.open
}
pub fn toggle(&mut self) {
self.open = !self.open;
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Row {
pub entity: Entity,
pub name: String,
pub icon: &'static str,
pub category: Category,
pub labelled: bool,
}
impl Row {
fn of(entity: EntityRef) -> Self {
match entity.get::<SceneObject>() {
Some(object) => Self {
entity: entity.id(),
name: object.name.clone(),
icon: object.icon,
category: object.category,
labelled: true,
},
None => Self {
entity: entity.id(),
name: format!("Entity {}", entity.id()),
icon: super::icons::path::CIRCLE,
category: Category::Scene,
labelled: false,
},
}
}
}
pub fn rows(world: &World) -> Vec<Row> {
let mut rows: Vec<Row> = world
.iter_entities()
.filter(|entity| {
!(entity.contains::<IsResource>()
|| entity.contains::<Observer>()
|| entity.contains::<SystemIdMarker>())
})
.map(Row::of)
.collect();
rows.sort_by(|a, b| {
(!a.labelled, a.category as u8, a.name.as_str()).cmp(&(
!b.labelled,
b.category as u8,
b.name.as_str(),
))
});
rows
}
pub fn show(world: &mut World) {
if !world.resource::<Outliner>().open {
return;
}
let screen = *world.resource::<ScreenSize>();
let rows = rows(world);
let response = widgets::window(
"OUTLINER",
Vec2::new(SCREEN_MARGIN, TOP),
WIDTH + widgets::PANEL_PADDING * 2.0,
true,
|| {
let room = (screen.height - TOP - SCREEN_MARGIN - widgets::TITLE_HEIGHT).max(50.0);
yakui::constrained(
Constraints {
min: Vec2::new(WIDTH, 0.0),
max: Vec2::new(WIDTH, room),
},
|| {
yakui::scroll_vertical(|| {
let mut column = List::column();
column.main_axis_size = yakui::MainAxisSize::Min;
column.show(|| {
for row in &rows {
let mut line = List::row();
line.item_spacing = 5.0;
line.cross_axis_alignment = CrossAxisAlignment::Center;
line.show(|| {
icon(row.icon, ICON, yakui_color(row.category.color()));
text(PX, row.name.clone());
});
}
});
});
},
);
},
);
if response.closed {
world.resource_mut::<Outliner>().open = false;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ecs::Application;
use crate::ui::icons::path;
use crate::ui::plugin::UiPlugin;
fn lamp(name: impl Into<String>) -> SceneObject {
SceneObject::new(name, path::LAMP, Category::Light)
}
fn block(name: impl Into<String>) -> SceneObject {
SceneObject::new(name, path::CUBE, Category::Mesh)
}
fn names(world: &World) -> Vec<String> {
rows(world).into_iter().map(|row| row.name).collect()
}
#[test]
fn rows_are_grouped_by_category_then_named_in_order() {
let mut world = World::new();
world.spawn(block("Zebra"));
world.spawn(lamp("Key"));
world.spawn(block("Apple"));
world.spawn(lamp("Fill"));
assert_eq!(names(&world), ["Fill", "Key", "Apple", "Zebra"]);
}
#[test]
fn a_row_carries_its_object_s_icon_tinted_by_its_category() {
let mut world = World::new();
world.spawn(lamp("Lamp 1"));
let rows = rows(&world);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].icon, path::LAMP);
assert_eq!(rows[0].category, Category::Light);
}
#[test]
fn an_entity_with_nothing_to_say_for_itself_is_still_listed() {
let mut world = World::new();
let bare = world.spawn(()).id();
world.spawn(block("Block 1"));
assert_eq!(names(&world), ["Block 1", &format!("Entity {bare}")]);
}
#[test]
fn the_list_does_not_list_the_world_s_bookkeeping() {
let mut app = Application::new();
app.add_plugin(UiPlugin);
app.world.spawn(lamp("Lamp 1"));
assert_eq!(names(&app.world), ["Lamp 1"]);
}
#[test]
fn it_draws_while_open_and_the_x_closes_it() {
use crate::ui::resources::{CursorPosition, MouseInput};
use crate::ui::state::Ui;
let mut app = Application::new();
app.add_plugin(UiPlugin);
app.world.insert_resource(ScreenSize {
width: 1280.0,
height: 800.0,
});
for i in 0..50 {
app.world.spawn(block(format!("Block {i:02}")));
}
app.world.resource_mut::<Outliner>().toggle();
let frame = |app: &mut Application| {
Ui::begin_frame(&mut app.world);
widgets::screen(|| show(&mut app.world));
app.update();
Ui::end_frame(&mut app.world);
};
frame(&mut app);
frame(&mut app);
assert!(app.world.resource::<Outliner>().is_open());
let x = (
SCREEN_MARGIN + WIDTH + widgets::PANEL_PADDING * 2.0 - widgets::TITLE_HEIGHT * 0.5,
TOP + widgets::TITLE_HEIGHT * 0.5,
);
app.world.insert_resource(CursorPosition { x: x.0, y: x.1 });
app.world.insert_resource(MouseInput {
left_down: true,
just_pressed: true,
..MouseInput::default()
});
frame(&mut app);
app.world.insert_resource(MouseInput::default());
frame(&mut app);
assert!(
!app.world.resource::<Outliner>().is_open(),
"the X closed it"
);
}
}