use bevy::prelude::*;
use crate::event::ReactEvents;
use crate::protocol::NodeId;
use crate::{react_event, react_message};
use super::{DevtoolsConfig, DevtoolsState, DockSide};
#[react_event(name = "devtools.toggle")]
pub(super) struct DevtoolsToggle {
pub(super) open: bool,
}
#[react_message(name = "devtools.open")]
pub(super) struct DevtoolsOpenMessage {
pub(super) open: bool,
}
#[react_message(name = "devtools.overlay")]
pub(super) struct DevtoolsOverlayMessage {
on: bool,
}
#[react_message(name = "devtools.panelRoot")]
pub(super) struct DevtoolsPanelRootMessage {
id: Option<NodeId>,
}
#[react_message(name = "devtools.dock")]
pub(super) struct DevtoolsDockMessage {
side: Option<String>,
width: f32,
}
pub(super) fn on_open_message(msg: On<DevtoolsOpenMessage>, mut state: ResMut<DevtoolsState>) {
state.open = msg.event().open;
if !state.open {
exit_interactions(&mut state);
}
}
pub(super) fn on_overlay_message(
msg: On<DevtoolsOverlayMessage>,
mut state: ResMut<DevtoolsState>,
) {
state.show_selection_overlay = msg.event().on;
}
pub(super) fn on_panel_root_message(
msg: On<DevtoolsPanelRootMessage>,
mut state: ResMut<DevtoolsState>,
) {
state.panel_root = msg.event().id;
}
pub(super) fn on_dock_message(msg: On<DevtoolsDockMessage>, mut state: ResMut<DevtoolsState>) {
state.dock_side = match msg.event().side.as_deref() {
Some("left") => Some(DockSide::Left),
Some("right") => Some(DockSide::Right),
_ => None,
};
state.dock_width = msg.event().width.max(0.0);
}
fn exit_interactions(state: &mut DevtoolsState) {
state.pick = false;
state.tree_hover = None;
state.pick_hover = None;
state.layers_tab_open = false;
state.console_tab_open = false;
state.console_last_seq = None;
}
pub(super) fn toggle_on_key(
keys: Res<ButtonInput<KeyCode>>,
cfg: Res<DevtoolsConfig>,
mut state: ResMut<DevtoolsState>,
events: ReactEvents,
) {
if !keys.just_pressed(cfg.toggle_key) {
return;
}
state.open = !state.open;
if !state.open {
exit_interactions(&mut state);
}
events.send(&DevtoolsToggle { open: state.open });
}
pub(super) fn apply_dock_reservation(
state: Res<DevtoolsState>,
windows: Query<&Window>,
mut root: Query<&mut Node, With<crate::plugin::UiRoot>>,
) {
let Ok(mut node) = root.single_mut() else {
return;
};
let reserved = if state.open { state.dock_side } else { None };
let width = match windows.single() {
Ok(window) => state.dock_width.min(window.width() - 100.0).max(0.0),
Err(_) => state.dock_width,
};
let (left, right) = match reserved {
Some(DockSide::Left) => (Val::Px(width), Val::ZERO),
Some(DockSide::Right) => (Val::ZERO, Val::Px(width)),
None => (Val::ZERO, Val::ZERO),
};
let margin = UiRect {
left,
right,
..node.margin
};
if node.margin != margin {
node.margin = margin;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::devtools::test_util::{drain_events, test_app};
#[test]
fn toggle_key_flips_state_and_notifies_js() {
let (mut app, mut rx) = test_app(DevtoolsConfig {
toggle_key: KeyCode::F9,
..default()
});
app.update();
assert!(drain_events(&mut rx).is_empty(), "no toggle before the key");
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(KeyCode::F9);
app.update();
let events = drain_events(&mut rx);
assert_eq!(
events
.iter()
.find(|(name, _)| name == "devtools.toggle")
.map(|(_, v)| v["open"].as_bool()),
Some(Some(true)),
"the configured key must open the panel and notify JS"
);
assert!(app.world().resource::<DevtoolsState>().open);
{
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.clear_just_pressed(KeyCode::F9);
keys.release(KeyCode::F9);
}
app.update();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.press(KeyCode::F9);
app.update();
let events = drain_events(&mut rx);
assert_eq!(
events
.iter()
.find(|(name, _)| name == "devtools.toggle")
.map(|(_, v)| v["open"].as_bool()),
Some(Some(false)),
"pressing again must close the panel"
);
assert!(!app.world().resource::<DevtoolsState>().open);
}
#[test]
fn dock_reservation_insets_uiroot_margin() {
let (mut app, _rx) = test_app(DevtoolsConfig::default());
let root = app
.world_mut()
.spawn((Node::default(), crate::plugin::UiRoot))
.id();
let margin = |app: &mut App| {
let node = app.world().entity(root).get::<Node>().unwrap();
(node.margin.left, node.margin.right)
};
{
let mut state = app.world_mut().resource_mut::<DevtoolsState>();
state.open = true;
state.dock_side = Some(DockSide::Right);
state.dock_width = 300.0;
}
app.update();
assert_eq!(margin(&mut app), (Val::ZERO, Val::Px(300.0)));
app.world_mut().resource_mut::<DevtoolsState>().dock_side = Some(DockSide::Left);
app.update();
assert_eq!(margin(&mut app), (Val::Px(300.0), Val::ZERO));
app.world_mut().resource_mut::<DevtoolsState>().open = false;
app.update();
assert_eq!(margin(&mut app), (Val::ZERO, Val::ZERO));
}
#[test]
fn dock_message_parses_side_and_clamps_width() {
let (mut app, _rx) = test_app(DevtoolsConfig::default());
let dock = |app: &mut App, side: Option<&str>, width: f32| {
app.world_mut().trigger(DevtoolsDockMessage {
side: side.map(String::from),
width,
});
let state = app.world().resource::<DevtoolsState>();
(state.dock_side, state.dock_width)
};
assert_eq!(
dock(&mut app, Some("left"), 320.0),
(Some(DockSide::Left), 320.0)
);
assert_eq!(
dock(&mut app, Some("right"), 280.0),
(Some(DockSide::Right), 280.0)
);
assert_eq!(dock(&mut app, Some("bogus"), -5.0), (None, 0.0));
assert_eq!(dock(&mut app, None, 380.0), (None, 380.0));
}
}