use bevy::{feathers::theme::ThemedText, picking::hover::Hovered, prelude::*, ui_widgets::observe};
use jackdaw_feathers::{
icons::{Icon, IconFont},
menu_bar, panel_header, popover, separator, split_panel, status_bar,
text_edit::{self, TextEditProps},
tokens,
tree_view::tree_container_drop_observers,
};
use crate::{
EditorEntity,
asset_browser::{self, ActiveTooltip},
brush::{BrushEditMode, BrushSelection, EditMode},
draw_brush::DrawBrushState,
gizmos::{GizmoMode, GizmoSpace},
hierarchy::{HierarchyPanel, HierarchyShowAllButton, HierarchyTreeContainer},
inspector::Inspector,
material_browser,
remote::ConnectionManager,
selection::Selection,
viewport::SceneViewport,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum TabKind {
#[default]
Scene,
ScheduleExplorer,
}
impl TabKind {
pub fn label(self) -> &'static str {
match self {
TabKind::Scene => "Main scene",
TabKind::ScheduleExplorer => "Schedule Explorer",
}
}
pub fn accent(self) -> Color {
match self {
TabKind::Scene => tokens::DOC_TAB_SCENE_ACCENT,
TabKind::ScheduleExplorer => tokens::DOC_TAB_TOOL_ACCENT,
}
}
pub fn icon(self) -> Icon {
match self {
TabKind::Scene => Icon::File,
TabKind::ScheduleExplorer => Icon::CalendarSearch,
}
}
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum SceneViewPreset {
#[default]
Scene,
}
#[derive(Resource, Default, Clone, Copy)]
pub struct ActiveDocument {
pub kind: TabKind,
}
#[derive(Component)]
pub struct DocumentTabStrip;
#[derive(Component)]
pub struct DocumentTabButton(pub TabKind);
#[derive(Component)]
pub struct DocumentRoot(pub TabKind);
#[derive(Component)]
pub struct SceneCenter;
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum DockWindowKind {
Terminal,
#[default]
Assets,
Timeline,
}
#[derive(Resource, Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct ActiveDockWindow(pub DockWindowKind);
#[derive(Component, Clone, Copy, PartialEq, Eq)]
pub struct DockSidebarIcon(pub DockWindowKind);
#[derive(Component, Clone, Copy, PartialEq, Eq)]
pub struct DockBody(pub DockWindowKind);
#[derive(Component)]
pub struct HierarchyFilter;
#[derive(Component)]
pub struct Toolbar;
#[derive(Component)]
pub struct GizmoModeButton(pub GizmoMode);
#[derive(Component)]
pub struct GizmoSpaceButton;
#[derive(Component, Clone, Copy, PartialEq, Eq)]
pub enum EditToolButton {
Object,
Draw,
Vertex,
Edge,
Face,
Clip,
Physics,
}
#[derive(Component)]
pub struct ToolbarTooltip(pub String);
#[derive(Component)]
pub struct KeybindHelpButton;
#[derive(Resource, Default)]
pub struct KeybindHelpPopover {
pub entity: Option<Entity>,
}
pub fn editor_layout(icon_font: &IconFont) -> impl Bundle {
let font = icon_font.0.clone();
(
EditorEntity,
BackgroundColor(tokens::WINDOW_BG),
Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
padding: UiRect::all(px(tokens::PANEL_GAP)),
..Default::default()
},
children![(
EditorEntity,
Node {
width: percent(100),
flex_grow: 1.0,
min_height: px(0.0),
flex_direction: FlexDirection::Column,
border: UiRect::all(px(1.0)),
border_radius: BorderRadius::all(px(8.0)),
overflow: Overflow::clip(),
..Default::default()
},
BackgroundColor(tokens::WINDOW_BG),
BorderColor::all(tokens::BORDER_SUBTLE),
children![
window_header(),
(
EditorEntity,
Node {
width: percent(100),
flex_grow: 1.0,
min_height: px(0.0),
flex_direction: FlexDirection::Column,
padding: UiRect::horizontal(px(tokens::PANEL_GAP)),
row_gap: px(tokens::PANEL_GAP),
..Default::default()
},
children![
(
DocumentRoot(TabKind::Scene),
EditorEntity,
Node {
width: percent(100),
flex_grow: 1.0,
min_height: px(0.0),
display: Display::Flex,
..Default::default()
},
children![(
Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Row,
..Default::default()
},
split_panel::panel_group(
0.1,
(
Spawn((split_panel::panel(1), left_column(font.clone()))),
Spawn(split_panel::panel_handle()),
Spawn((
split_panel::panel(4),
center_column(font.clone()),
)),
Spawn(split_panel::panel_handle()),
Spawn((split_panel::panel(1), entity_inspector(font.clone()))),
),
),
)],
),
(
DocumentRoot(TabKind::ScheduleExplorer),
EditorEntity,
Node {
width: percent(100),
flex_grow: 1.0,
min_height: px(0.0),
flex_direction: FlexDirection::Column,
display: Display::None,
..Default::default()
},
split_panel::panel_group(
0.2,
(
Spawn((
split_panel::panel(1),
crate::remote::entity_browser::remote_debug_workspace_content(),
)),
Spawn(split_panel::panel_handle()),
Spawn((
split_panel::panel(1),
crate::remote::remote_inspector::remote_inspector(),
)),
),
),
)
],
),
editor_status_bar()
],
)],
)
}
fn window_header() -> impl Bundle {
(
EditorEntity,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
width: percent(100),
height: px(34.0),
flex_shrink: 0.0,
border_radius: BorderRadius::top(Val::Px(7.0)),
..Default::default()
},
BackgroundColor(tokens::WINDOW_BG),
children![
(
EditorEntity,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
height: percent(100),
column_gap: px(tokens::SPACING_LG),
..Default::default()
},
children![
menu_bar::menu_bar_shell(),
(
DocumentTabStrip,
EditorEntity,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
height: percent(100),
column_gap: px(4.0),
..Default::default()
},
children![
document_tab(TabKind::Scene, true),
document_tab(TabKind::ScheduleExplorer, false),
],
),
],
),
(
EditorEntity,
Node {
flex_grow: 1.0,
..Default::default()
},
),
(
EditorEntity,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
padding: UiRect::horizontal(px(tokens::SPACING_MD)),
column_gap: px(6.0),
..Default::default()
},
children![play_pause_controls(),],
),
],
)
}
fn document_tab(kind: TabKind, active: bool) -> impl Bundle {
let bg = if active {
tokens::DOC_TAB_ACTIVE_BG
} else {
Color::NONE
};
let border = if active {
tokens::DOC_TAB_ACTIVE_BORDER
} else {
Color::NONE
};
let label_color = if active {
tokens::DOC_TAB_ACTIVE_LABEL
} else {
tokens::DOC_TAB_INACTIVE_LABEL
};
(
DocumentTabButton(kind),
Hovered::default(),
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
padding: UiRect::axes(px(7.0), px(4.0)),
column_gap: px(5.0),
border: UiRect::all(px(1.0)),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_MD)),
..Default::default()
},
BackgroundColor(bg),
BorderColor::all(border),
children![
(
Node {
width: px(2.5),
height: px(12.0),
border_radius: BorderRadius::all(px(5.0)),
..Default::default()
},
BackgroundColor(kind.accent()),
),
(
Text::new(String::from(kind.icon().unicode())),
TextFont {
font_size: 12.0,
..Default::default()
},
TextColor(label_color),
),
(
Text::new(kind.label().to_string()),
TextFont {
font_size: tokens::FONT_MD,
..Default::default()
},
TextColor(label_color),
),
],
observe(
move |_: On<Pointer<Click>>,
mut active: ResMut<ActiveDocument>,
manager: Res<ConnectionManager>| {
if kind == TabKind::ScheduleExplorer && !manager.is_connected() {
return;
}
active.kind = kind;
},
),
)
}
fn play_pause_controls() -> impl Bundle {
(
EditorEntity,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
height: px(22.0),
padding: UiRect::horizontal(px(6.5)),
column_gap: px(9.0),
border: UiRect::all(px(1.0)),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_LG)),
..Default::default()
},
BackgroundColor(tokens::HEADER_CONTROL_BG),
BorderColor::all(tokens::HEADER_CONTROL_BORDER),
children![
(
Text::new(String::from(Icon::Play.unicode())),
TextFont {
font_size: 13.0,
..Default::default()
},
TextColor(tokens::HEADER_CONTROL_LABEL),
),
(
Text::new(String::from(Icon::Pause.unicode())),
TextFont {
font_size: 13.0,
..Default::default()
},
TextColor(tokens::HEADER_CONTROL_LABEL),
),
],
)
}
fn left_column(icon_font: Handle<Font>) -> impl Bundle {
(
EditorEntity,
Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Column,
..Default::default()
},
split_panel::panel_group(
0.15,
(
Spawn((split_panel::panel(3), entity_heiarchy(icon_font))),
Spawn(split_panel::panel_handle()),
Spawn((split_panel::panel(1), project_files_panel())),
),
),
)
}
fn project_files_panel() -> impl Bundle {
(
EditorEntity,
Node {
flex_direction: FlexDirection::Column,
width: percent(100),
height: percent(100),
overflow: Overflow::clip(),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_LG)),
..Default::default()
},
BackgroundColor(tokens::PANEL_BG),
children![
panel_header::panel_tab_bar(&[panel_header::TabDef::new("Project Files", true)], true,),
(
Node {
flex_direction: FlexDirection::Column,
width: percent(100),
padding: UiRect::all(px(tokens::SPACING_SM)),
flex_shrink: 0.0,
..Default::default()
},
children![(text_edit::text_edit(
TextEditProps::default()
.with_placeholder("Search...")
.allow_empty()
),)],
),
(
crate::project_files::ProjectFilesTree,
EditorEntity,
Node {
flex_direction: FlexDirection::Column,
width: percent(100),
flex_grow: 1.0,
min_height: px(0.0),
overflow: Overflow::scroll_y(),
padding: UiRect::all(px(tokens::SPACING_SM)),
..Default::default()
},
),
],
)
}
fn center_column(icon_font: Handle<Font>) -> impl Bundle {
(
EditorEntity,
Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Column,
..Default::default()
},
split_panel::panel_group(
0.15,
(
Spawn((
split_panel::panel(4),
viewport_with_toolbar(icon_font.clone()),
)),
Spawn(split_panel::panel_handle()),
Spawn((split_panel::panel(1), bottom_panels(icon_font))),
),
),
)
}
fn viewport_with_toolbar(icon_font: Handle<Font>) -> impl Bundle {
(
EditorEntity,
Node {
height: percent(100),
flex_direction: FlexDirection::Column,
overflow: Overflow::clip(),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_LG)),
..Default::default()
},
BackgroundColor(tokens::PANEL_BG),
children![
toolbar(icon_font),
crate::navmesh::toolbar::navmesh_toolbar(),
crate::terrain::toolbar::terrain_toolbar(),
scene_view(),
],
)
}
fn toolbar(icon_font: Handle<Font>) -> impl Bundle {
let f = icon_font.clone();
(
Toolbar,
EditorEntity,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
padding: UiRect::axes(px(tokens::SPACING_MD), px(tokens::SPACING_SM)),
column_gap: px(tokens::SPACING_SM),
width: percent(100),
height: px(32.0),
flex_shrink: 0.0,
..Default::default()
},
BackgroundColor(tokens::PANEL_HEADER_BG),
children![
toolbar_button(
Icon::Move3d,
"",
GizmoMode::Translate,
icon_font.clone(),
"Move (Esc)"
),
toolbar_button(
Icon::Rotate3d,
"R",
GizmoMode::Rotate,
icon_font.clone(),
"Rotate (R)"
),
toolbar_button(
Icon::Scale3d,
"T",
GizmoMode::Scale,
icon_font.clone(),
"Scale (T)"
),
separator::separator(separator::SeparatorProps::vertical()),
toolbar_space_button(f.clone()),
separator::separator(separator::SeparatorProps::vertical()),
toolbar_edit_button(
Icon::MousePointer2,
EditToolButton::Object,
f.clone(),
"Object Mode"
),
toolbar_edit_button(Icon::Box, EditToolButton::Draw, f.clone(), "Draw Brush (B)"),
toolbar_edit_button(
Icon::CircleDot,
EditToolButton::Vertex,
f.clone(),
"Vertex Mode (1)"
),
toolbar_edit_button(
Icon::GitCommitHorizontal,
EditToolButton::Edge,
f.clone(),
"Edge Mode (2)"
),
toolbar_edit_button(
Icon::Hexagon,
EditToolButton::Face,
f.clone(),
"Face Mode (3)"
),
toolbar_edit_button(
Icon::ScissorsLineDashed,
EditToolButton::Clip,
f.clone(),
"Clip Mode (4)"
),
separator::separator(separator::SeparatorProps::vertical()),
toolbar_edit_button(
Icon::Zap,
EditToolButton::Physics,
f.clone(),
"Physics Tool"
),
(Node {
flex_grow: 1.0,
..Default::default()
},),
toolbar_help_button(f),
],
)
}
fn toolbar_button(
icon: Icon,
label: &str,
mode: GizmoMode,
font: Handle<Font>,
tooltip: &str,
) -> impl Bundle {
let label = label.to_string();
(
GizmoModeButton(mode),
Hovered::default(),
ToolbarTooltip(tooltip.into()),
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: px(tokens::SPACING_XS),
padding: UiRect::axes(px(tokens::SPACING_MD), px(tokens::SPACING_XS)),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_SM)),
..Default::default()
},
BackgroundColor(tokens::TOOLBAR_BUTTON_BG),
children![
(
Text::new(String::from(icon.unicode())),
TextFont {
font,
font_size: 15.0,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
),
(
Text::new(label),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
ThemedText,
)
],
observe(
move |_: On<Pointer<Click>>, mut gizmo_mode: ResMut<GizmoMode>| {
*gizmo_mode = mode;
},
),
)
}
fn toolbar_space_button(icon_font: Handle<Font>) -> impl Bundle {
(
GizmoSpaceButton,
Hovered::default(),
ToolbarTooltip("Toggle World/Local (X)".into()),
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: px(tokens::SPACING_XS),
padding: UiRect::axes(px(tokens::SPACING_MD), px(tokens::SPACING_XS)),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_SM)),
..Default::default()
},
BackgroundColor(tokens::TOOLBAR_BUTTON_BG),
children![
(
Text::new(String::from(Icon::Globe.unicode())),
TextFont {
font: icon_font,
font_size: tokens::FONT_MD,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
),
(
Text::new("World"),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
ThemedText,
)
],
observe(|_: On<Pointer<Click>>, mut space: ResMut<GizmoSpace>| {
*space = match *space {
GizmoSpace::World => GizmoSpace::Local,
GizmoSpace::Local => GizmoSpace::World,
};
}),
)
}
fn toolbar_edit_button(
icon: Icon,
tool: EditToolButton,
font: Handle<Font>,
tooltip: &str,
) -> impl Bundle {
(
tool,
Hovered::default(),
ToolbarTooltip(tooltip.into()),
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
padding: UiRect::axes(px(tokens::SPACING_MD), px(tokens::SPACING_XS)),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_SM)),
..Default::default()
},
BackgroundColor(tokens::TOOLBAR_BUTTON_BG),
children![(
Text::new(String::from(icon.unicode())),
TextFont {
font,
font_size: 15.0,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
)],
observe(
move |_: On<Pointer<Click>>,
mut edit_mode: ResMut<EditMode>,
mut brush_selection: ResMut<BrushSelection>,
mut draw_state: ResMut<DrawBrushState>,
selection: Res<Selection>,
brushes: Query<(), With<jackdaw_jsn::Brush>>| {
match tool {
EditToolButton::Object => {
*edit_mode = EditMode::Object;
brush_selection.entity = None;
brush_selection.faces.clear();
brush_selection.vertices.clear();
brush_selection.edges.clear();
draw_state.active = None;
}
EditToolButton::Draw => {
if draw_state.active.is_some() {
draw_state.active = None;
} else {
if *edit_mode != EditMode::Object {
*edit_mode = EditMode::Object;
brush_selection.entity = None;
brush_selection.faces.clear();
brush_selection.vertices.clear();
brush_selection.edges.clear();
}
let append_target =
selection.primary().filter(|&e| brushes.contains(e));
draw_state.active = Some(crate::draw_brush::ActiveDraw {
corner1: Vec3::ZERO,
corner2: Vec3::ZERO,
depth: 0.0,
phase: crate::draw_brush::DrawPhase::PlacingFirstCorner,
mode: crate::draw_brush::DrawMode::Add,
plane: crate::draw_brush::DrawPlane {
origin: Vec3::ZERO,
normal: Vec3::Y,
axis_u: Vec3::X,
axis_v: Vec3::Z,
},
extrude_start_cursor: Vec2::ZERO,
plane_locked: false,
cursor_on_plane: None,
append_target,
drag_footprint: false,
press_screen_pos: None,
polygon_vertices: Vec::new(),
polygon_cursor: None,
diagonal_snap: false,
cached_face_hit: None,
});
}
}
EditToolButton::Physics => {
draw_state.active = None;
brush_selection.entity = None;
brush_selection.faces.clear();
brush_selection.vertices.clear();
brush_selection.edges.clear();
if *edit_mode == EditMode::Physics {
*edit_mode = EditMode::Object;
} else {
*edit_mode = EditMode::Physics;
}
}
EditToolButton::Vertex
| EditToolButton::Edge
| EditToolButton::Face
| EditToolButton::Clip => {
draw_state.active = None;
let target_mode = match tool {
EditToolButton::Vertex => BrushEditMode::Vertex,
EditToolButton::Edge => BrushEditMode::Edge,
EditToolButton::Face => BrushEditMode::Face,
EditToolButton::Clip => BrushEditMode::Clip,
_ => unreachable!(),
};
if let EditMode::BrushEdit(current) = *edit_mode {
if current == target_mode {
*edit_mode = EditMode::Object;
brush_selection.entity = None;
brush_selection.faces.clear();
brush_selection.vertices.clear();
brush_selection.edges.clear();
} else {
*edit_mode = EditMode::BrushEdit(target_mode);
brush_selection.faces.clear();
brush_selection.vertices.clear();
brush_selection.edges.clear();
}
} else {
if let Some(entity) =
selection.primary().filter(|&e| brushes.contains(e))
{
*edit_mode = EditMode::BrushEdit(target_mode);
brush_selection.entity = Some(entity);
brush_selection.faces.clear();
brush_selection.vertices.clear();
brush_selection.edges.clear();
}
}
}
}
},
),
)
}
fn toolbar_help_button(icon_font: Handle<Font>) -> impl Bundle {
(
KeybindHelpButton,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
padding: UiRect::axes(px(tokens::SPACING_MD), px(tokens::SPACING_XS)),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_SM)),
..Default::default()
},
BackgroundColor(tokens::TOOLBAR_BUTTON_BG),
children![(
Text::new(String::from(Icon::Keyboard.unicode())),
TextFont {
font: icon_font,
font_size: tokens::FONT_MD,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
)],
observe(
|trigger: On<Pointer<Click>>,
mut commands: Commands,
mut popover_state: ResMut<KeybindHelpPopover>,
registry: Res<crate::keybinds::KeybindRegistry>| {
if let Some(entity) = popover_state.entity.take() {
if let Ok(mut ec) = commands.get_entity(entity) {
ec.despawn();
}
return;
}
let anchor = trigger.event_target();
let popover_entity = commands
.spawn(popover::popover(
popover::PopoverProps::new(anchor)
.with_placement(popover::PopoverPlacement::BottomEnd)
.with_padding(12.0)
.with_z_index(200),
))
.with_children(|parent| {
parent
.spawn(Node {
flex_direction: FlexDirection::Column,
max_height: px(500.0),
overflow: Overflow::scroll_y(),
..Default::default()
})
.with_children(|scroll_parent| {
spawn_keybind_help_content(scroll_parent, ®istry);
});
})
.id();
popover_state.entity = Some(popover_entity);
},
),
)
}
fn spawn_keybind_help_content(
parent: &mut ChildSpawnerCommands,
registry: &crate::keybinds::KeybindRegistry,
) {
use jackdaw_commands::keybinds::EditorAction;
let mouse_entries: &[(&str, &[(&str, &str)])] = &[
(
"Navigation",
&[
("RMB + Drag", "Look around"),
("Shift", "Double speed"),
("Scroll", "Dolly forward/back"),
("RMB + Scroll", "Adjust move speed"),
],
),
(
"Selection",
&[
("LMB", "Select entity"),
("Ctrl+Click", "Toggle multi-select"),
("Shift+LMB Drag", "Box select"),
],
),
(
"Transform",
&[
("MMB", "Toggle snap"),
("Ctrl", "Toggle snap (during drag)"),
],
),
(
"Brush Edit",
&[
("Shift+Click", "Multi-select"),
("Click+Drag", "Move selected"),
],
),
(
"Draw Brush",
&[
("Click", "Place vertex / advance"),
("Right-click", "Cancel"),
],
),
("View", &[("Ctrl+Alt+Scroll", "Grid size")]),
];
let category_order = [
"File",
"Entity",
"Transform",
"Brush Edit",
"Draw Brush",
"CSG",
"Gizmo",
"Navigation",
"View",
];
let display_order = [
"Navigation",
"Selection",
"Transform",
"Entity",
"Brush Edit",
"CSG",
"Draw Brush",
"Gizmo",
"View",
"File",
];
for §ion in &display_order {
let mut entries: Vec<(String, String)> = Vec::new();
for (cat, mouse_binds) in mouse_entries {
if *cat == section {
for (key, desc) in *mouse_binds {
entries.push((key.to_string(), desc.to_string()));
}
}
}
if category_order.contains(§ion) {
for &action in EditorAction::all() {
if action.category() != section {
continue;
}
let bindings = registry.bindings.get(&action).cloned().unwrap_or_default();
if bindings.is_empty() {
continue;
}
let key_str = bindings
.iter()
.map(|b| b.to_string())
.collect::<Vec<_>>()
.join(" / ");
entries.push((key_str, action.to_string()));
}
}
if entries.is_empty() {
continue;
}
parent.spawn((
Text::new(section),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_PRIMARY),
Node {
margin: UiRect::top(px(tokens::SPACING_SM)),
..Default::default()
},
));
for (key, desc) in &entries {
parent.spawn((
Node {
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
column_gap: px(tokens::SPACING_LG),
width: px(260.0),
..Default::default()
},
children![
(
Text::new(key.clone()),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_PRIMARY),
),
(
Text::new(desc.clone()),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
)
],
));
}
}
}
fn entity_heiarchy(icon_font: Handle<Font>) -> impl Bundle {
let add_entity_icon_font = icon_font.clone();
(
HierarchyPanel,
Node {
height: percent(100),
flex_direction: FlexDirection::Column,
overflow: Overflow::clip(),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_LG)),
..Default::default()
},
BackgroundColor(tokens::PANEL_BG),
children![
panel_header::panel_tab_bar(
&[
panel_header::TabDef::new("Scene Tree", true),
panel_header::TabDef::new("Import", false),
],
true,
),
(
panel_header::PanelTabContent(0),
Node {
flex_direction: FlexDirection::Column,
flex_grow: 1.0,
min_height: px(0.0),
padding: UiRect::all(px(tokens::SPACING_SM)),
display: Display::Flex,
..Default::default()
},
children![
(
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: px(tokens::SPACING_XS),
width: percent(100),
..Default::default()
},
children![
(
Node {
flex_grow: 1.0,
..Default::default()
},
children![(
HierarchyFilter,
text_edit::text_edit(
TextEditProps::default()
.with_placeholder("Filter...")
.allow_empty()
),
)],
),
(
HierarchyShowAllButton,
Interaction::default(),
Node {
width: px(24.0),
height: px(24.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_SM),),
..Default::default()
},
children![(
Text::new(String::from(Icon::Eye.unicode())),
TextFont {
font: icon_font,
font_size: 14.0,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
)],
),
],
),
(
Interaction::default(),
Hovered::default(),
Node {
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: percent(100),
height: px(tokens::ROW_HEIGHT),
column_gap: px(tokens::SPACING_SM),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_MD)),
margin: UiRect::vertical(px(tokens::SPACING_XS)),
flex_shrink: 0.0,
..Default::default()
},
BackgroundColor(tokens::ELEVATED_BG),
observe(
|hover: On<Pointer<Over>>, mut bg: Query<&mut BackgroundColor>| {
if let Ok(mut bg) = bg.get_mut(hover.event_target()) {
bg.0 = tokens::TOOLBAR_ACTIVE_BG;
}
},
),
observe(
|out: On<Pointer<Out>>, mut bg: Query<&mut BackgroundColor>| {
if let Ok(mut bg) = bg.get_mut(out.event_target()) {
bg.0 = tokens::ELEVATED_BG;
}
},
),
children![
(
Text::new(String::from(Icon::PackagePlus.unicode())),
TextFont {
font: add_entity_icon_font,
font_size: tokens::ICON_SM,
..Default::default()
},
TextColor(tokens::TEXT_PRIMARY),
),
(
Text::new("Add Entity"),
TextFont {
font_size: tokens::TEXT_SIZE,
weight: FontWeight::MEDIUM,
..Default::default()
},
TextColor(tokens::TEXT_PRIMARY),
),
],
),
(
HierarchyTreeContainer,
Node {
flex_direction: FlexDirection::Column,
width: percent(100),
flex_grow: 1.0,
min_height: px(0.0),
overflow: Overflow::scroll_y(),
margin: UiRect::top(px(tokens::SPACING_SM)),
..Default::default()
},
BackgroundColor(Color::NONE),
tree_container_drop_observers(),
),
(
crate::status_bar::SceneStatsText,
Text::new(""),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
TextLayout::new_with_justify(Justify::Center),
Node {
padding: UiRect::all(px(tokens::SPACING_XS)),
flex_shrink: 0.0,
width: percent(100),
..Default::default()
},
)
],
),
(
panel_header::PanelTabContent(1),
Node {
flex_direction: FlexDirection::Column,
flex_grow: 1.0,
min_height: px(0.0),
padding: UiRect::all(px(tokens::SPACING_MD)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
display: Display::None,
..Default::default()
},
children![(
Text::new("Import"),
TextFont {
font_size: tokens::FONT_MD,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
)],
)
],
)
}
fn scene_view() -> impl Bundle {
(
EditorEntity,
SceneViewport,
Node {
width: percent(100),
flex_grow: 1.0,
..Default::default()
},
)
}
pub fn update_toolbar_highlights(
mode: Res<GizmoMode>,
mut buttons: Query<(&GizmoModeButton, &mut BackgroundColor)>,
) {
if !mode.is_changed() {
return;
}
for (button, mut bg) in &mut buttons {
bg.0 = if button.0 == *mode {
tokens::TOOLBAR_ACTIVE_BG
} else {
tokens::TOOLBAR_BUTTON_BG
};
}
}
pub fn update_toolbar_tooltips(
buttons: Query<(Entity, &ToolbarTooltip, &Hovered), Changed<Hovered>>,
mut commands: Commands,
mut active: ResMut<ActiveTooltip>,
) {
for (entity, tooltip, hovered) in &buttons {
if hovered.get() {
if let Some(old) = active.0.take() {
commands.entity(old).try_despawn();
}
let tip = commands
.spawn(popover::popover(
popover::PopoverProps::new(entity)
.with_placement(popover::PopoverPlacement::Bottom)
.with_padding(4.0)
.with_z_index(300),
))
.id();
commands.spawn((
Text::new(tooltip.0.clone()),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_PRIMARY),
ChildOf(tip),
));
active.0 = Some(tip);
} else if let Some(old) = active.0.take() {
commands.entity(old).try_despawn();
}
}
}
pub fn update_space_toggle_label(
space: Res<GizmoSpace>,
buttons: Query<&Children, With<GizmoSpaceButton>>,
mut texts: Query<&mut Text, With<ThemedText>>,
) {
if !space.is_changed() {
return;
}
let label = match *space {
GizmoSpace::World => "World",
GizmoSpace::Local => "Local",
};
for children in &buttons {
for child in children.iter() {
if let Ok(mut text) = texts.get_mut(child) {
text.0 = label.to_string();
return;
}
}
}
}
pub(crate) fn update_edit_tool_highlights(
edit_mode: Res<EditMode>,
draw_state: Res<DrawBrushState>,
mut buttons: Query<(&EditToolButton, &mut BackgroundColor)>,
) {
if !edit_mode.is_changed() && !draw_state.is_changed() {
return;
}
let draw_active = draw_state.active.is_some();
for (button, mut bg) in &mut buttons {
let active = match button {
EditToolButton::Object => !draw_active && *edit_mode == EditMode::Object,
EditToolButton::Draw => draw_active,
EditToolButton::Vertex => {
!draw_active && *edit_mode == EditMode::BrushEdit(BrushEditMode::Vertex)
}
EditToolButton::Edge => {
!draw_active && *edit_mode == EditMode::BrushEdit(BrushEditMode::Edge)
}
EditToolButton::Face => {
!draw_active && *edit_mode == EditMode::BrushEdit(BrushEditMode::Face)
}
EditToolButton::Clip => {
!draw_active && *edit_mode == EditMode::BrushEdit(BrushEditMode::Clip)
}
EditToolButton::Physics => !draw_active && *edit_mode == EditMode::Physics,
};
bg.0 = if active {
tokens::TOOLBAR_ACTIVE_BG
} else {
tokens::TOOLBAR_BUTTON_BG
};
}
}
pub fn update_active_document_display(
active: Res<ActiveDocument>,
mut roots: Query<(&DocumentRoot, &mut Node)>,
) {
if !active.is_changed() {
return;
}
for (root, mut node) in &mut roots {
node.display = if root.0 == active.kind {
Display::Flex
} else {
Display::None
};
}
}
pub fn on_dock_sidebar_icon_click(
trigger: On<Pointer<Click>>,
icons: Query<&DockSidebarIcon>,
mut active: ResMut<ActiveDockWindow>,
) {
let Ok(icon) = icons.get(trigger.event_target()) else {
return;
};
if active.0 != icon.0 {
active.0 = icon.0;
}
}
pub fn update_dock_body_visibility(
active: Res<ActiveDockWindow>,
mut bodies: Query<(&DockBody, &mut Node)>,
) {
if !active.is_changed() {
return;
}
for (body, mut node) in &mut bodies {
node.display = if body.0 == active.0 {
Display::Flex
} else {
Display::None
};
}
}
pub fn update_dock_sidebar_highlights(
active: Res<ActiveDockWindow>,
mut icons: Query<(&DockSidebarIcon, &mut BorderColor, &Children)>,
mut text_colors: Query<&mut TextColor>,
) {
if !active.is_changed() {
return;
}
for (icon, mut border, children) in &mut icons {
let is_active = icon.0 == active.0;
*border = BorderColor::all(if is_active {
tokens::ACCENT_BLUE
} else {
Color::NONE
});
for child in children.iter() {
if let Ok(mut tc) = text_colors.get_mut(child) {
tc.0 = if is_active {
tokens::TEXT_PRIMARY
} else {
tokens::TAB_INACTIVE_TEXT
};
}
}
}
}
pub fn update_tab_strip_highlights(
active: Res<ActiveDocument>,
manager: Res<ConnectionManager>,
mut tabs: Query<(
&DocumentTabButton,
&mut BackgroundColor,
&mut BorderColor,
&Children,
)>,
mut texts: Query<&mut TextColor>,
) {
if !active.is_changed() && !manager.is_changed() {
return;
}
let connected = manager.is_connected();
for (tab, mut tab_bg, mut tab_border, children) in &mut tabs {
let is_active = tab.0 == active.kind;
let is_disabled = tab.0 == TabKind::ScheduleExplorer && !connected;
tab_bg.0 = if is_active {
tokens::DOC_TAB_ACTIVE_BG
} else {
Color::NONE
};
*tab_border = BorderColor::all(if is_active {
tokens::DOC_TAB_ACTIVE_BORDER
} else {
Color::NONE
});
let label_color = if is_disabled {
Color::srgba(0.4, 0.4, 0.4, 0.5)
} else if is_active {
tokens::DOC_TAB_ACTIVE_LABEL
} else {
tokens::DOC_TAB_INACTIVE_LABEL
};
for child in children.iter().skip(1) {
if let Ok(mut tc) = texts.get_mut(child) {
tc.0 = label_color;
}
}
}
}
fn bottom_panels(icon_font: Handle<Font>) -> impl Bundle {
(
EditorEntity,
Node {
width: percent(100),
height: percent(100),
flex_direction: FlexDirection::Row,
overflow: Overflow::clip(),
..Default::default()
},
BackgroundColor(tokens::PANEL_BG),
children![
dock_window_sidebar(icon_font.clone()),
dock_body_area(icon_font),
],
)
}
fn dock_window_sidebar(icon_font: Handle<Font>) -> impl Bundle {
(
EditorEntity,
Node {
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceBetween,
align_items: AlignItems::Center,
width: Val::Px(30.0),
padding: UiRect::new(Val::Px(1.0), Val::ZERO, Val::Px(4.0), Val::Px(9.0)),
flex_shrink: 0.0,
border: UiRect {
left: Val::Px(1.0),
top: Val::Px(1.0),
bottom: Val::Px(1.0),
right: Val::ZERO,
},
border_radius: BorderRadius::left(Val::Px(5.0)),
..Default::default()
},
BackgroundColor(tokens::WINDOW_BG),
BorderColor::all(tokens::PANEL_BORDER),
children![
(
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..Default::default()
},
children![
dock_sidebar_icon(DockWindowKind::Terminal, Icon::Terminal, icon_font.clone()),
dock_sidebar_icon(DockWindowKind::Assets, Icon::FolderOpen, icon_font.clone()),
dock_sidebar_icon(DockWindowKind::Timeline, Icon::Ruler, icon_font.clone()),
],
),
(
Node {
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..Default::default()
},
children![(
Text::new(String::from(Icon::GripVertical.unicode())),
TextFont {
font: icon_font,
font_size: 15.0,
..Default::default()
},
TextColor(tokens::TAB_INACTIVE_TEXT),
)],
),
],
)
}
fn dock_sidebar_icon(kind: DockWindowKind, icon: Icon, icon_font: Handle<Font>) -> impl Bundle {
(
DockSidebarIcon(kind),
Interaction::default(),
Node {
width: Val::Px(29.0),
height: Val::Px(30.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border: UiRect::left(Val::Px(2.0)),
..Default::default()
},
BorderColor::all(Color::NONE),
children![(
Text::new(String::from(icon.unicode())),
TextFont {
font: icon_font,
font_size: tokens::ICON_MD,
..Default::default()
},
TextColor(tokens::TAB_INACTIVE_TEXT),
)],
)
}
fn dock_body_area(icon_font: Handle<Font>) -> impl Bundle {
(
EditorEntity,
Node {
flex_grow: 1.0,
flex_direction: FlexDirection::Column,
min_width: Val::Px(0.0),
min_height: Val::Px(0.0),
..Default::default()
},
children![
(
DockBody(DockWindowKind::Assets),
Node {
flex_grow: 1.0,
width: percent(100),
min_height: Val::Px(0.0),
display: Display::Flex,
..Default::default()
},
children![asset_browser::asset_browser_panel(icon_font.clone())],
),
(
DockBody(DockWindowKind::Timeline),
Node {
flex_grow: 1.0,
width: percent(100),
min_height: Val::Px(0.0),
display: Display::None,
..Default::default()
},
children![jackdaw_animation::timeline_panel()],
),
(
DockBody(DockWindowKind::Terminal),
Node {
flex_grow: 1.0,
width: percent(100),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
display: Display::None,
..Default::default()
},
children![(
Text::new("Terminal window — not implemented yet"),
TextFont {
font_size: tokens::FONT_MD,
..Default::default()
},
TextColor(tokens::TEXT_MUTED_COLOR.into()),
)],
),
],
)
}
fn editor_status_bar() -> impl Bundle {
(
status_bar::StatusBar,
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
width: Val::Percent(100.0),
height: Val::Px(tokens::STATUS_BAR_HEIGHT),
padding: UiRect::horizontal(Val::Px(tokens::SPACING_MD)),
flex_shrink: 0.0,
..Default::default()
},
BackgroundColor(tokens::WINDOW_BG),
children![
(
status_bar::StatusBarLeft,
Text::new("Ready"),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
bevy::feathers::theme::ThemedText,
),
(
status_bar::StatusBarCenter,
Text::new(""),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
),
(
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(tokens::SPACING_LG),
..Default::default()
},
children![
(
status_bar::StatusBarRight,
Text::new(""),
TextFont {
font_size: tokens::FONT_SM,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
),
crate::remote::panel::connection_indicator()
],
)
],
)
}
fn entity_inspector(icon_font: Handle<Font>) -> impl Bundle {
(
Node {
height: percent(100),
flex_direction: FlexDirection::Column,
overflow: Overflow::clip(),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_LG)),
..Default::default()
},
BackgroundColor(tokens::PANEL_BG),
children![
panel_header::panel_tab_bar(
&[
panel_header::TabDef::new("Components", true),
panel_header::TabDef::new("Materials", false),
panel_header::TabDef::new("Resources", false),
panel_header::TabDef::new("Systems", false),
],
true,
),
(
panel_header::PanelTabContent(0),
Node {
flex_direction: FlexDirection::Column,
flex_grow: 1.0,
min_height: px(0.0),
display: Display::Flex,
..Default::default()
},
children![
(
Node {
flex_direction: FlexDirection::Column,
width: percent(100),
padding: UiRect::all(px(tokens::SPACING_SM)),
row_gap: px(tokens::SPACING_XS),
flex_shrink: 0.0,
..Default::default()
},
children![
(
crate::inspector::InspectorSearch,
text_edit::text_edit(
TextEditProps::default()
.with_placeholder("Filter...")
.allow_empty()
),
),
(
crate::inspector::AddComponentButton,
Interaction::default(),
Node {
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
width: percent(100),
height: px(tokens::ROW_HEIGHT),
column_gap: px(tokens::SPACING_SM),
border_radius: BorderRadius::all(px(tokens::BORDER_RADIUS_MD)),
flex_shrink: 0.0,
..Default::default()
},
BackgroundColor(tokens::ELEVATED_BG),
observe(
|hover: On<Pointer<Over>>,
mut bg: Query<&mut BackgroundColor>| {
if let Ok(mut bg) = bg.get_mut(hover.event_target()) {
bg.0 = tokens::TOOLBAR_ACTIVE_BG;
}
},
),
observe(
|out: On<Pointer<Out>>,
mut bg: Query<&mut BackgroundColor>| {
if let Ok(mut bg) = bg.get_mut(out.event_target()) {
bg.0 = tokens::ELEVATED_BG;
}
},
),
children![
(
Text::new(String::from(Icon::PackagePlus.unicode())),
TextFont {
font: icon_font.clone(),
font_size: tokens::ICON_SM,
..Default::default()
},
TextColor(tokens::TEXT_PRIMARY),
),
(
Text::new("Add Component"),
TextFont {
font_size: tokens::TEXT_SIZE,
weight: FontWeight::MEDIUM,
..Default::default()
},
TextColor(tokens::TEXT_PRIMARY),
),
],
observe(
|click: On<Pointer<Click>>, mut commands: Commands| {
commands.trigger(
jackdaw_feathers::button::ButtonClickEvent {
entity: click.event_target(),
},
);
},
),
),
],
),
(
Inspector,
Node {
flex_direction: FlexDirection::Column,
row_gap: px(tokens::SPACING_SM),
overflow: Overflow::scroll_y(),
flex_grow: 1.0,
min_height: px(0.0),
padding: UiRect::all(px(tokens::SPACING_SM)),
..Default::default()
}
),
],
),
(
panel_header::PanelTabContent(1),
Node {
flex_direction: FlexDirection::Column,
flex_grow: 1.0,
min_height: px(0.0),
display: Display::None,
..Default::default()
},
children![material_browser::material_browser_panel(icon_font)],
),
(
panel_header::PanelTabContent(2),
Node {
flex_direction: FlexDirection::Column,
flex_grow: 1.0,
min_height: px(0.0),
padding: UiRect::all(px(tokens::SPACING_MD)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
display: Display::None,
..Default::default()
},
children![(
Text::new("Resources"),
TextFont {
font_size: tokens::FONT_MD,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
)],
),
(
panel_header::PanelTabContent(3),
Node {
flex_direction: FlexDirection::Column,
flex_grow: 1.0,
min_height: px(0.0),
padding: UiRect::all(px(tokens::SPACING_MD)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
display: Display::None,
..Default::default()
},
children![(
Text::new("Systems"),
TextFont {
font_size: tokens::FONT_MD,
..Default::default()
},
TextColor(tokens::TEXT_SECONDARY),
)],
),
],
)
}