use crate::ecs::EditorWorld;
use crate::systems::retained_ui::Action;
use nightshade::ecs::camera::components::{ShadingMode, ViewportShading, ViewportUpdateMode};
use nightshade::ecs::gizmos::GizmoMode;
use nightshade::prelude::*;
const STRIP_INSET_PX: f32 = 8.0;
const STRIP_HEIGHT_PX: f32 = 28.0;
const BUTTON_WIDTH_PX: f32 = 30.0;
const BUTTON_GAP_PX: f32 = 3.0;
const STRIP_PADDING_PX: f32 = 4.0;
const SEPARATOR_WIDTH_PX: f32 = 1.0;
const NUM_BUTTONS: f32 = 12.0;
const NUM_SEPARATORS: f32 = 4.0;
const NUM_CHILDREN: f32 = NUM_BUTTONS + NUM_SEPARATORS;
const STRIP_WIDTH_PX: f32 = STRIP_PADDING_PX * 2.0
+ BUTTON_WIDTH_PX * NUM_BUTTONS
+ SEPARATOR_WIDTH_PX * NUM_SEPARATORS
+ BUTTON_GAP_PX * (NUM_CHILDREN - 1.0);
const STRIP_BG: Vec4 = Vec4::new(0.06, 0.06, 0.08, 0.85);
const BUTTON_INACTIVE: Vec4 = Vec4::new(0.0, 0.0, 0.0, 0.0);
const BUTTON_ACTIVE: Vec4 = Vec4::new(0.20, 0.42, 0.78, 0.95);
const BUTTON_HOVER: Vec4 = Vec4::new(0.25, 0.25, 0.30, 0.85);
const ICON_COLOR: Vec4 = Vec4::new(0.92, 0.92, 0.92, 1.0);
const SEPARATOR_COLOR: Vec4 = Vec4::new(0.25, 0.25, 0.30, 0.85);
#[derive(Clone, Copy)]
pub struct ViewportControlsHandles {
pub camera_entity: Entity,
pub container: Entity,
pub wireframe_button: Entity,
pub solid_button: Entity,
pub flat_button: Entity,
pub rendered_button: Entity,
pub overlays_button: Entity,
pub overlays_icon: Entity,
pub grid_button: Entity,
pub sky_button: Entity,
pub translate_button: Entity,
pub rotate_button: Entity,
pub scale_button: Entity,
pub snap_button: Entity,
pub realtime_button: Entity,
}
pub fn build(tree: &mut UiTreeBuilder, camera_entity: Entity) -> ViewportControlsHandles {
let icon_set = tree.world_mut().resources.retained_ui.icon_set;
let lookup = IconLookup::for_set(icon_set);
let container = tree
.add_node()
.window(
Ab(vec2(STRIP_INSET_PX, STRIP_INSET_PX)),
Ab(vec2(STRIP_WIDTH_PX, STRIP_HEIGHT_PX)),
Anchor::TopLeft,
)
.rect(6.0)
.color_raw::<UiBase>(STRIP_BG)
.with_layer(UiLayer::DockedPanels)
.with_depth(UiDepthMode::Set(14.0))
.flow(FlowDirection::Horizontal, STRIP_PADDING_PX, BUTTON_GAP_PX)
.entity();
let mut wireframe_button = Entity::default();
let mut solid_button = Entity::default();
let mut flat_button = Entity::default();
let mut rendered_button = Entity::default();
let mut overlays_button = Entity::default();
let mut overlays_icon = Entity::default();
let mut grid_button = Entity::default();
let mut sky_button = Entity::default();
let mut translate_button = Entity::default();
let mut rotate_button = Entity::default();
let mut scale_button = Entity::default();
let mut snap_button = Entity::default();
let mut realtime_button = Entity::default();
tree.in_parent(container, |tree| {
wireframe_button = make_icon_button(
tree,
lookup.mesh,
"Viewport Shading: Wireframe\nShow mesh edges (bounding volumes + normal lines).",
);
solid_button = make_icon_button(
tree,
lookup.palette,
"Viewport Shading: Solid\nUnlit shading with post-processing disabled.",
);
flat_button = make_icon_button(
tree,
lookup.crop_free,
"Viewport Shading: Flat\nCAD-style gray with lighting; suppresses material colors and textures.",
);
rendered_button = make_icon_button(
tree,
lookup.auto_awesome,
"Viewport Shading: Rendered\nFull lighting and post-processing pipeline.",
);
add_separator(tree);
let pair = make_overlays_button(
tree,
lookup.visibility,
"Show Overlays\nDisplay overlays like gizmos and grid for this viewport.",
);
overlays_button = pair.0;
overlays_icon = pair.1;
grid_button = make_icon_button(
tree,
lookup.grid,
"Show Ground Grid\nToggle the world floor grid (global).",
);
sky_button = make_icon_button(
tree,
lookup.public,
"Show Sky\nToggle the atmosphere skybox (global). IBL contributions are unaffected.",
);
add_separator(tree);
translate_button = make_icon_button(
tree,
lookup.open_with,
"Translate gizmo\nMove the selected entity along local axes.",
);
rotate_button = make_icon_button(
tree,
lookup.rotate_left,
"Rotate gizmo\nRotate the selected entity around local axes.",
);
scale_button = make_icon_button(
tree,
lookup.zoom_out_map,
"Scale gizmo\nScale the selected entity.",
);
add_separator(tree);
snap_button = make_icon_button(
tree,
lookup.tune,
"Snap\nQuantize gizmo translate, rotate, and scale to step values.\nConfigure in the World inspector.",
);
add_separator(tree);
realtime_button = make_icon_button(
tree,
lookup.refresh,
"Realtime\nRe-render this viewport every frame. When off, the tile only re-renders on scene mutations or shading changes.",
);
});
ViewportControlsHandles {
camera_entity,
container,
wireframe_button,
solid_button,
flat_button,
rendered_button,
overlays_button,
overlays_icon,
grid_button,
sky_button,
translate_button,
rotate_button,
scale_button,
snap_button,
realtime_button,
}
}
fn make_icon_button(tree: &mut UiTreeBuilder, icon: IconGlyph, tooltip: &str) -> Entity {
let button = tree
.add_node()
.size(
BUTTON_WIDTH_PX.px(),
(STRIP_HEIGHT_PX - STRIP_PADDING_PX * 2.0).px(),
)
.rect(4.0)
.color_raw::<UiBase>(BUTTON_INACTIVE)
.color_raw::<UiHover>(BUTTON_HOVER)
.with_interaction()
.with_tooltip(tooltip)
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.entity();
tree.in_parent(button, |tree| {
tree.add_node()
.fill()
.with_icon(icon, 14.0)
.color_raw::<UiBase>(ICON_COLOR)
.entity();
});
button
}
fn add_separator(tree: &mut UiTreeBuilder) {
tree.add_node()
.size(
SEPARATOR_WIDTH_PX.px(),
(STRIP_HEIGHT_PX - STRIP_PADDING_PX * 2.0).px(),
)
.with_rect(0.0, 0.0, SEPARATOR_COLOR)
.color_raw::<UiBase>(SEPARATOR_COLOR)
.entity();
}
fn make_overlays_button(
tree: &mut UiTreeBuilder,
icon: IconGlyph,
tooltip: &str,
) -> (Entity, Entity) {
let button = tree
.add_node()
.size(
BUTTON_WIDTH_PX.px(),
(STRIP_HEIGHT_PX - STRIP_PADDING_PX * 2.0).px(),
)
.rect(4.0)
.color_raw::<UiBase>(BUTTON_INACTIVE)
.color_raw::<UiHover>(BUTTON_HOVER)
.with_interaction()
.with_tooltip(tooltip)
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.entity();
let icon_entity = tree.in_parent(button, |tree| {
tree.add_node()
.fill()
.with_icon(icon, 14.0)
.color_raw::<UiBase>(ICON_COLOR)
.entity()
});
(button, icon_entity)
}
pub fn update(world: &mut World, handles: &ViewportControlsHandles, tile_rect: Option<Rect>) {
let dpi_scale = world.resources.window.cached_scale_factor.max(0.0001);
let min_width_physical = (STRIP_WIDTH_PX + STRIP_INSET_PX) * dpi_scale;
let visible = matches!(tile_rect, Some(rect) if rect.width() > min_width_physical);
ui_set_visible(world, handles.container, visible);
if !visible {
return;
}
if let Some(rect) = tile_rect
&& let Some(node) = world.ui.get_ui_layout_node_mut(handles.container)
&& let Some(nightshade::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.base_layout.as_mut()
{
let position = vec2(rect.min.x + STRIP_INSET_PX, rect.min.y + STRIP_INSET_PX) / dpi_scale;
window.position = nightshade::ecs::ui::units::Ab(position).into();
}
}
pub fn sync(world: &mut World, handles: &ViewportControlsHandles) {
let snap_enabled = world
.resources
.user_interface
.gizmos
.translation_snap
.is_some();
let shading = world
.core
.get_viewport_shading(handles.camera_entity)
.copied()
.unwrap_or_default();
set_button_active(
world,
handles.wireframe_button,
shading.mode == ShadingMode::Wireframe,
);
set_button_active(
world,
handles.solid_button,
shading.mode == ShadingMode::Solid,
);
set_button_active(
world,
handles.flat_button,
shading.mode == ShadingMode::Flat,
);
set_button_active(
world,
handles.rendered_button,
shading.mode == ShadingMode::Rendered,
);
set_button_active(world, handles.overlays_button, shading.show_overlays);
set_button_active(
world,
handles.grid_button,
world.resources.graphics.show_grid,
);
set_button_active(world, handles.sky_button, world.resources.graphics.show_sky);
let gizmo_mode = world.resources.user_interface.gizmos.mode;
set_button_active(
world,
handles.translate_button,
matches!(
gizmo_mode,
GizmoMode::LocalTranslation | GizmoMode::GlobalTranslation
),
);
set_button_active(
world,
handles.rotate_button,
matches!(gizmo_mode, GizmoMode::Rotation),
);
set_button_active(
world,
handles.scale_button,
matches!(gizmo_mode, GizmoMode::Scale),
);
set_button_active(world, handles.snap_button, snap_enabled);
let realtime_active = world
.core
.get_viewport_update_mode(handles.camera_entity)
.copied()
.map(|mode| matches!(mode, ViewportUpdateMode::Always))
.unwrap_or(true);
set_button_active(world, handles.realtime_button, realtime_active);
let icon_set = world.resources.retained_ui.icon_set;
let lookup = IconLookup::for_set(icon_set);
let icon = if shading.show_overlays {
lookup.visibility
} else {
lookup.visibility_off
};
ui_set_icon(world, handles.overlays_icon, icon);
}
fn set_button_active(world: &mut World, button: Entity, active: bool) {
if let Some(color) = world.ui.get_ui_node_color_mut(button) {
color.colors[0] = Some(if active {
BUTTON_ACTIVE
} else {
BUTTON_INACTIVE
});
}
}
pub fn poll(editor_world: &mut EditorWorld, world: &mut World, handles: &ViewportControlsHandles) {
for event in ui_events(world) {
if let UiEvent::ButtonClicked(entity) = event {
let mode = if *entity == handles.wireframe_button {
Some(ShadingMode::Wireframe)
} else if *entity == handles.solid_button {
Some(ShadingMode::Solid)
} else if *entity == handles.flat_button {
Some(ShadingMode::Flat)
} else if *entity == handles.rendered_button {
Some(ShadingMode::Rendered)
} else {
None
};
if let Some(mode) = mode {
editor_world
.resources
.ui_interaction
.actions
.push(Action::SetShadingMode(handles.camera_entity, mode));
} else if *entity == handles.overlays_button {
editor_world
.resources
.ui_interaction
.actions
.push(Action::ToggleViewportOverlays(handles.camera_entity));
} else if *entity == handles.grid_button {
editor_world
.resources
.ui_interaction
.actions
.push(Action::ToggleGroundGrid);
} else if *entity == handles.sky_button {
editor_world
.resources
.ui_interaction
.actions
.push(Action::ToggleSky);
} else if *entity == handles.translate_button {
editor_world
.resources
.ui_interaction
.actions
.push(Action::SetGizmoMode(GizmoMode::LocalTranslation));
} else if *entity == handles.rotate_button {
editor_world
.resources
.ui_interaction
.actions
.push(Action::SetGizmoMode(GizmoMode::Rotation));
} else if *entity == handles.scale_button {
editor_world
.resources
.ui_interaction
.actions
.push(Action::SetGizmoMode(GizmoMode::Scale));
} else if *entity == handles.snap_button {
editor_world
.resources
.ui_interaction
.actions
.push(Action::ToggleSnap);
} else if *entity == handles.realtime_button {
editor_world
.resources
.ui_interaction
.actions
.push(Action::ToggleViewportRealtime(handles.camera_entity));
}
}
}
}
pub fn ensure_shading_with_mode(world: &mut World, camera_entity: Entity, mode: ShadingMode) {
if world.core.get_viewport_shading(camera_entity).is_none() {
world
.core
.add_components(camera_entity, nightshade::ecs::world::VIEWPORT_SHADING);
}
world.core.set_viewport_shading(
camera_entity,
ViewportShading {
mode,
show_overlays: true,
},
);
}
pub fn ensure_shading(world: &mut World, camera_entity: Entity) {
if world.core.get_viewport_shading(camera_entity).is_none() {
world
.core
.add_components(camera_entity, nightshade::ecs::world::VIEWPORT_SHADING);
world
.core
.set_viewport_shading(camera_entity, ViewportShading::default());
}
}