use crate::ecs::EditorWorld;
use crate::systems::retained_ui::{Action, IconBinding, IconLookupFn, UiHandles};
use nightshade::prelude::*;
#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq)]
enum AddMenuTag {
Cube = 1,
Sphere,
Cylinder,
Cone,
Torus,
Plane,
InstancedCube,
InstancedSphere,
InstancedCylinder,
InstancedCone,
InstancedTorus,
InstancedPlane,
Empty,
PlayerSpawn,
SavePoint,
PatrolPoint,
TriggerVolume,
PointLight,
SpotLight,
DirectionalLight,
EmptyChildOfSelection,
DuplicateSelected,
DeleteSelected,
CommandPalette,
}
#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq)]
enum AtmosphereMenuTag {
Hdr = 1,
Sky,
Nebula,
DayNight,
}
#[repr(u32)]
#[derive(Copy, Clone, Eq, PartialEq)]
enum GizmoMenuTag {
LocalTranslation = 100,
GlobalTranslation,
Rotation,
Scale,
CompositeLocal,
CompositeGlobal,
}
#[derive(Default, Clone)]
pub struct TopBarHandles {
pub file_button: Entity,
pub add_button: Entity,
pub assets_button: Entity,
pub view_button: Entity,
pub scene_button: Entity,
pub tests_button: Entity,
pub randomize_button: Entity,
pub file_menu: Entity,
pub add_menu: Entity,
pub assets_menu: Entity,
pub view_menu: Entity,
pub scene_menu: Entity,
pub tests_menu: Entity,
pub icon_entities: Vec<IconBinding>,
}
pub fn build(tree: &mut UiTreeBuilder) -> TopBarHandles {
let panel = tree.add_docked_panel_top("top_bar", "", 36.0);
ui_panel_set_header_visible(tree.world_mut(), panel, false);
if let Some(data) = tree.world_mut().ui.get_ui_panel_mut(panel) {
data.min_size = vec2(0.0, 36.0);
data.resizable = false;
}
let theme = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font = theme.font_size;
let text_color = theme.text_color;
let content = super::panel_content(tree, panel);
if let Some(node) = tree.world_mut().ui.get_ui_layout_node_mut(content) {
node.flow_layout = Some(FlowLayout {
direction: FlowDirection::Horizontal,
padding: 6.0,
spacing: 4.0,
alignment: FlowAlignment::Start,
cross_alignment: FlowAlignment::Center,
wrap: false,
});
}
let item_height = 24.0;
let icon_set = tree.world_mut().resources.retained_ui.icon_set;
let lookup = IconLookup::for_set(icon_set);
let mut icon_entities: Vec<IconBinding> = Vec::new();
let transparent = vec4(0.0, 0.0, 0.0, 0.0);
let mut file_button = Entity::default();
let mut add_button = Entity::default();
let mut assets_button = Entity::default();
let mut view_button = Entity::default();
let mut scene_button = Entity::default();
let mut tests_button = Entity::default();
let mut randomize_button = Entity::default();
tree.in_parent(content, |tree| {
let mut make = |tree: &mut UiTreeBuilder,
icon: IconGlyph,
icon_fn: IconLookupFn,
label: &str|
-> Entity {
let row = tree
.add_node()
.size((0.0).px(), (item_height).px())
.auto_size(AutoSizeMode::Width)
.auto_size_padding(vec2(10.0, 0.0))
.with_rect(4.0, 0.0, transparent)
.color_raw::<UiBase>(transparent)
.fg_hover(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 0.0, 6.0)
.entity();
tree.in_parent(row, |tree| {
let icon_entity = tree
.add_node()
.size((18.0).px(), (item_height).px())
.with_icon(icon, font * 0.95)
.color_raw::<UiBase>(text_color)
.entity();
icon_entities.push((icon_entity, icon_fn));
tree.add_node()
.size((0.0).px(), (item_height).px())
.auto_size(AutoSizeMode::Width)
.with_text(label, font * 0.85)
.text_left()
.color_raw::<UiBase>(text_color)
.entity();
});
row
};
file_button = make(tree, lookup.file, |l| l.file, "File");
add_button = make(tree, lookup.add, |l| l.add, "Add");
assets_button = make(tree, lookup.collections, |l| l.collections, "Assets");
view_button = make(tree, lookup.visibility, |l| l.visibility, "View");
scene_button = make(tree, lookup.account_tree, |l| l.account_tree, "Scene");
tests_button = make(tree, lookup.mesh, |l| l.mesh, "Tests");
randomize_button = make(tree, lookup.shuffle, |l| l.shuffle, "Random");
});
let file_menu_builder = ContextMenuBuilder::new()
.item("New map", "")
.item("Open map...", "")
.separator()
.item("Save map", "Ctrl+S")
.item("Save map as...", "")
.separator()
.item("Load HDR skybox...", "");
let file_menu = tree.add_context_menu_from_builder(file_menu_builder);
let add_menu = tree.add_context_menu_from_builder(
ContextMenuBuilder::new()
.item_tagged("Cube", "", AddMenuTag::Cube as u32)
.item_tagged("Sphere", "", AddMenuTag::Sphere as u32)
.item_tagged("Cylinder", "", AddMenuTag::Cylinder as u32)
.item_tagged("Cone", "", AddMenuTag::Cone as u32)
.item_tagged("Torus", "", AddMenuTag::Torus as u32)
.item_tagged("Plane", "", AddMenuTag::Plane as u32)
.submenu("Instanced Mesh", |builder| {
builder
.item_tagged("Cube", "", AddMenuTag::InstancedCube as u32)
.item_tagged("Sphere", "", AddMenuTag::InstancedSphere as u32)
.item_tagged("Cylinder", "", AddMenuTag::InstancedCylinder as u32)
.item_tagged("Cone", "", AddMenuTag::InstancedCone as u32)
.item_tagged("Torus", "", AddMenuTag::InstancedTorus as u32)
.item_tagged("Plane", "", AddMenuTag::InstancedPlane as u32)
})
.separator()
.item_tagged("Empty", "", AddMenuTag::Empty as u32)
.item_tagged("Player spawn", "", AddMenuTag::PlayerSpawn as u32)
.item_tagged("Save point", "", AddMenuTag::SavePoint as u32)
.item_tagged("Patrol point", "", AddMenuTag::PatrolPoint as u32)
.item_tagged("Trigger volume", "", AddMenuTag::TriggerVolume as u32)
.item_tagged("Point light", "", AddMenuTag::PointLight as u32)
.item_tagged("Spot light", "", AddMenuTag::SpotLight as u32)
.item_tagged("Directional light", "", AddMenuTag::DirectionalLight as u32)
.separator()
.item_tagged(
"Empty (child of selection)",
"",
AddMenuTag::EmptyChildOfSelection as u32,
)
.separator()
.item_tagged(
"Duplicate selected",
"Ctrl+D",
AddMenuTag::DuplicateSelected as u32,
)
.item_tagged("Delete selected", "Del", AddMenuTag::DeleteSelected as u32)
.separator()
.item_tagged(
"Command palette",
"Ctrl+K",
AddMenuTag::CommandPalette as u32,
),
);
let assets_builder = ContextMenuBuilder::new()
.item("Khronos sample assets", "")
.item("Polyhaven", "")
.item("Materials", "");
#[cfg(not(target_arch = "wasm32"))]
let assets_builder = assets_builder.item("Sketchfab", "").item("Kenney", "");
let assets_menu = tree.add_context_menu_from_builder(assets_builder);
let initial_lucide = matches!(icon_set, IconSet::Lucide);
let view_menu = tree.add_context_menu_from_builder(
ContextMenuBuilder::new()
.checkable("Entity tree", "", false)
.checkable("Inspector", "", false)
.checkable("Viewer mode", "", true)
.separator()
.checkable("Ground grid", "", false)
.checkable("Show sky", "", true)
.checkable("Unlit", "", false)
.separator()
.item_tagged("Atmosphere: HDR", "", AtmosphereMenuTag::Hdr as u32)
.item_tagged("Atmosphere: Sky", "", AtmosphereMenuTag::Sky as u32)
.item_tagged("Atmosphere: Nebula", "", AtmosphereMenuTag::Nebula as u32)
.item_tagged(
"Atmosphere: Day/Night",
"",
AtmosphereMenuTag::DayNight as u32,
)
.separator()
.item_tagged(
"Gizmo: Translate (Local)",
"",
GizmoMenuTag::LocalTranslation as u32,
)
.item_tagged(
"Gizmo: Translate (Global)",
"",
GizmoMenuTag::GlobalTranslation as u32,
)
.item_tagged("Gizmo: Rotate", "", GizmoMenuTag::Rotation as u32)
.item_tagged("Gizmo: Scale", "", GizmoMenuTag::Scale as u32)
.item_tagged(
"Gizmo: Composite (Local)",
"",
GizmoMenuTag::CompositeLocal as u32,
)
.item_tagged(
"Gizmo: Composite (Global)",
"",
GizmoMenuTag::CompositeGlobal as u32,
)
.separator()
.checkable("Day/Night cycle", "", true)
.item("Snap settings...", "")
.separator()
.checkable("Lucide icons", "", initial_lucide)
.checkable("Skeleton view", "", false)
.checkable("Normals", "", false)
.item("Normals settings...", ""),
);
let scene_menu_builder = ContextMenuBuilder::new()
.item("Frame scene", "C")
.item("Toggle frame on load", "")
.separator()
.item("Show all cameras", "")
.item("Reset viewport layout", "")
.separator()
.item("Convert similar meshes to instanced", "")
.item("Snap selection to floor", "End")
.separator()
.item("Bake navmesh", "")
.item("Toggle navmesh debug", "")
.separator()
.item("Clear scene", "");
let scene_menu = tree.add_context_menu_from_builder(scene_menu_builder);
let tests_menu = tree.add_context_menu_from_builder(
ContextMenuBuilder::new()
.item("Spawn 2,000 lines", "")
.item("Spawn 10,000 meshes", "")
.separator()
.item("Spawn 3D text", "")
.item("Spawn 5K text lattice", ""),
);
TopBarHandles {
file_button,
add_button,
assets_button,
view_button,
scene_button,
tests_button,
randomize_button,
file_menu,
add_menu,
assets_menu,
view_menu,
scene_menu,
tests_menu,
icon_entities,
}
}
pub fn sync(_world: &mut World, _handles: &UiHandles) {
}
pub fn poll(editor_world: &mut EditorWorld, world: &mut World, handles: &UiHandles) {
let menu_buttons = [
(handles.top_bar.file_button, handles.top_bar.file_menu),
(handles.top_bar.add_button, handles.top_bar.add_menu),
(handles.top_bar.assets_button, handles.top_bar.assets_menu),
(handles.top_bar.view_button, handles.top_bar.view_menu),
(handles.top_bar.scene_button, handles.top_bar.scene_menu),
(handles.top_bar.tests_button, handles.top_bar.tests_menu),
];
let mut to_open: Vec<(Entity, Vec2)> = Vec::new();
let mut randomize = false;
let mut menu_actions: Vec<Action> = Vec::new();
for event in ui_events(world) {
if let UiEvent::ButtonClicked(entity) = event {
if *entity == handles.top_bar.randomize_button {
randomize = true;
continue;
}
for (button, menu) in menu_buttons {
if *entity == button {
let pos = world
.ui
.get_ui_layout_node(button)
.map(|n| vec2(n.computed_rect.min.x, n.computed_rect.max.y))
.unwrap_or_default();
to_open.push((menu, pos));
}
}
}
let (target, index, tag) = match event {
UiEvent::MenuItemClicked { entity, item_index } => (*entity, *item_index, 0),
UiEvent::ContextMenuItemClicked {
entity,
item_index,
tag,
} => (*entity, *item_index, *tag),
_ => continue,
};
let action = if target == handles.top_bar.file_menu {
match index {
0 => Some(Action::NewProject),
1 => Some(Action::OpenProject),
2 => Some(Action::SaveProject),
3 => Some(Action::SaveProjectAs),
4 => Some(Action::LoadHdrSkybox),
_ => None,
}
} else if target == handles.top_bar.add_menu {
add_menu_action(tag, editor_world)
} else if target == handles.top_bar.assets_menu {
match index {
0 => Some(Action::OpenKhronosBrowser),
1 => Some(Action::OpenPolyhavenBrowser),
2 => Some(Action::OpenMaterialsBrowser),
#[cfg(not(target_arch = "wasm32"))]
3 => Some(Action::OpenSketchfabBrowser),
#[cfg(not(target_arch = "wasm32"))]
4 => Some(Action::OpenKenneyBrowser),
_ => None,
}
} else if target == handles.top_bar.view_menu {
if let Some(atmosphere) = atmosphere_menu_action(tag) {
Some(Action::SetAtmosphere(atmosphere))
} else if let Some(mode) = gizmo_menu_action(tag) {
Some(Action::SetGizmoMode(mode))
} else {
match index {
0 => Some(Action::ToggleTreePanel),
1 => Some(Action::ToggleInspectorPanel),
2 => Some(Action::ToggleViewerMode),
3 => Some(Action::ToggleGroundGrid),
4 => Some(Action::ToggleSky),
5 => Some(Action::ToggleLitUnlit),
16 => Some(Action::ToggleDayNightCycle),
17 => Some(Action::OpenSnapSettings),
18 => Some(Action::ToggleIconSet),
19 => Some(Action::ToggleSkeletonView),
20 => Some(Action::ToggleShowNormals),
21 => Some(Action::OpenNormalsSettings),
_ => None,
}
}
} else if target == handles.top_bar.scene_menu {
match index {
0 => Some(Action::FrameScene),
1 => Some(Action::ToggleFrameOnLoad),
2 => Some(Action::ShowAllCameras),
3 => Some(Action::ResetViewportLayout),
4 => Some(Action::ConvertSimilarToInstanced),
5 => Some(Action::SnapSelectionToFloor),
6 => Some(Action::BakeNavmesh),
7 => Some(Action::ToggleNavmeshDebug),
8 => Some(Action::ClearScene),
_ => None,
}
} else if target == handles.top_bar.tests_menu {
match index {
0 => Some(Action::SpawnLines),
1 => Some(Action::SpawnMeshes),
2 => Some(Action::Spawn3DText),
3 => Some(Action::SpawnTextLattice),
_ => None,
}
} else {
None
};
if let Some(action) = action {
menu_actions.push(action);
}
}
for (menu, pos) in to_open {
ui_show_context_menu(world, menu, pos);
}
let actions = &mut editor_world.resources.ui_interaction.actions;
if randomize {
actions.push(Action::RandomizeBoth);
}
actions.extend(menu_actions);
}
fn atmosphere_menu_action(tag: u32) -> Option<Atmosphere> {
match tag {
x if x == AtmosphereMenuTag::Hdr as u32 => Some(Atmosphere::Hdr),
x if x == AtmosphereMenuTag::Sky as u32 => Some(Atmosphere::Sky),
x if x == AtmosphereMenuTag::Nebula as u32 => Some(Atmosphere::Nebula),
x if x == AtmosphereMenuTag::DayNight as u32 => Some(Atmosphere::DayNight),
_ => None,
}
}
fn gizmo_menu_action(tag: u32) -> Option<nightshade::ecs::gizmos::GizmoMode> {
use nightshade::ecs::gizmos::GizmoMode;
match tag {
x if x == GizmoMenuTag::LocalTranslation as u32 => Some(GizmoMode::LocalTranslation),
x if x == GizmoMenuTag::GlobalTranslation as u32 => Some(GizmoMode::GlobalTranslation),
x if x == GizmoMenuTag::Rotation as u32 => Some(GizmoMode::Rotation),
x if x == GizmoMenuTag::Scale as u32 => Some(GizmoMode::Scale),
x if x == GizmoMenuTag::CompositeLocal as u32 => Some(GizmoMode::CompositeLocal),
x if x == GizmoMenuTag::CompositeGlobal as u32 => Some(GizmoMode::CompositeGlobal),
_ => None,
}
}
fn add_menu_action(tag: u32, editor_world: &EditorWorld) -> Option<Action> {
match tag {
x if x == AddMenuTag::Cube as u32 => Some(Action::AddCube),
x if x == AddMenuTag::Sphere as u32 => Some(Action::AddSphere),
x if x == AddMenuTag::Cylinder as u32 => Some(Action::AddCylinder),
x if x == AddMenuTag::Cone as u32 => Some(Action::AddCone),
x if x == AddMenuTag::Torus as u32 => Some(Action::AddTorus),
x if x == AddMenuTag::Plane as u32 => Some(Action::AddPlane),
x if x == AddMenuTag::InstancedCube as u32 => Some(Action::AddInstancedCube),
x if x == AddMenuTag::InstancedSphere as u32 => Some(Action::AddInstancedSphere),
x if x == AddMenuTag::InstancedCylinder as u32 => Some(Action::AddInstancedCylinder),
x if x == AddMenuTag::InstancedCone as u32 => Some(Action::AddInstancedCone),
x if x == AddMenuTag::InstancedTorus as u32 => Some(Action::AddInstancedTorus),
x if x == AddMenuTag::InstancedPlane as u32 => Some(Action::AddInstancedPlane),
x if x == AddMenuTag::Empty as u32 => Some(Action::AddEmpty),
x if x == AddMenuTag::PlayerSpawn as u32 => Some(Action::AddPlayerSpawn),
x if x == AddMenuTag::SavePoint as u32 => Some(Action::AddSavePoint),
x if x == AddMenuTag::PatrolPoint as u32 => Some(Action::AddPatrolPoint),
x if x == AddMenuTag::TriggerVolume as u32 => Some(Action::AddTriggerVolume),
x if x == AddMenuTag::PointLight as u32 => Some(Action::AddPointLight),
x if x == AddMenuTag::SpotLight as u32 => Some(Action::AddSpotLight),
x if x == AddMenuTag::DirectionalLight as u32 => Some(Action::AddDirectionalLight),
x if x == AddMenuTag::EmptyChildOfSelection as u32 => editor_world
.resources
.ui
.selected_entity
.map(Action::AddEntityChild),
x if x == AddMenuTag::DuplicateSelected as u32 => Some(Action::DuplicateSelectedEntity),
x if x == AddMenuTag::DeleteSelected as u32 => Some(Action::DeleteSelectedEntity),
x if x == AddMenuTag::CommandPalette as u32 => Some(Action::OpenCommandPalette),
_ => None,
}
}