use crate::ecs::{
components::buffer::{EditorView, FocusedEditorView, ViewEntity},
events::focus::FocusRequested,
schedules::EditorSet,
};
use bevy::prelude::{
App, Commands, Entity, IntoScheduleConfigs, MessageReader, Plugin, Query, Update, With, Without,
};
#[derive(Clone, Copy, Debug, Default)]
pub struct FocusPlugin;
impl Plugin for FocusPlugin {
fn build(&self, app: &mut App) {
let _app = app.add_systems(
Update,
(apply_focus_requests, enforce_single_focused_editor_view)
.chain()
.in_set(EditorSet::Input),
);
}
}
fn apply_focus_requests(
mut commands: Commands,
mut requests: MessageReader<FocusRequested>,
editor_views: Query<Entity, With<EditorView>>,
focused_views: Query<Entity, (With<EditorView>, With<FocusedEditorView>)>,
) {
let Some(request) = requests.read().last().copied() else {
return;
};
if !editor_views.contains(request.target.get()) {
return;
}
for focused in &focused_views {
if ViewEntity(focused) != request.target {
let _entity = commands.entity(focused).remove::<FocusedEditorView>();
}
}
let _entity = commands
.entity(request.target.get())
.insert(FocusedEditorView);
}
fn enforce_single_focused_editor_view(
mut commands: Commands,
focused_buffers: Query<Entity, (With<EditorView>, With<FocusedEditorView>)>,
unfocused_buffers: Query<Entity, (With<EditorView>, Without<FocusedEditorView>)>,
) {
let mut focused = focused_buffers.iter().collect::<Vec<_>>();
if focused.len() > 1 {
focused.sort_unstable();
for entity in focused.into_iter().skip(1) {
let _entity = commands.entity(entity).remove::<FocusedEditorView>();
}
return;
}
if focused.is_empty() {
let mut candidates = unfocused_buffers.iter().collect::<Vec<_>>();
candidates.sort_unstable();
if let Some(entity) = candidates.first().copied() {
let _entity = commands.entity(entity).insert(FocusedEditorView);
}
}
}
#[cfg(test)]
mod tests {
use super::FocusPlugin;
use crate::ecs::{
components::buffer::{EditorBuffer, EditorView, FocusedEditorView, ViewEntity},
events::focus::FocusRequested,
};
use bevy::prelude::{App, Entity, With};
use proptest::prelude::*;
fn spawn_view(app: &mut App, focused: bool) -> Entity {
let buffer = app.world_mut().spawn(EditorBuffer).id();
let mut entity = app.world_mut().spawn(EditorView {
buffer: crate::ecs::components::buffer::BufferEntity(buffer),
});
if focused {
let _entity = entity.insert(FocusedEditorView);
}
entity.id()
}
fn focused_views(app: &mut App) -> Vec<Entity> {
let mut query = app
.world_mut()
.query_filtered::<Entity, (With<EditorView>, With<FocusedEditorView>)>();
let mut entities = query.iter(app.world()).collect::<Vec<_>>();
entities.sort_unstable();
entities
}
#[test]
fn focus_request_moves_focus_to_target_view() {
let mut app = App::new();
let _app = app
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(FocusPlugin);
let first = spawn_view(&mut app, true);
let second = spawn_view(&mut app, false);
let _message = app.world_mut().write_message(FocusRequested {
target: ViewEntity(second),
});
app.update();
assert_eq!(focused_views(&mut app), vec![second]);
assert!(!app.world().entity(first).contains::<FocusedEditorView>());
}
#[test]
fn invalid_focus_request_keeps_existing_focus() {
let mut app = App::new();
let _app = app
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(FocusPlugin);
let focused = spawn_view(&mut app, true);
let unrelated = app.world_mut().spawn_empty().id();
let _message = app.world_mut().write_message(FocusRequested {
target: ViewEntity(unrelated),
});
app.update();
assert_eq!(focused_views(&mut app), vec![focused]);
}
proptest! {
#[test]
fn focus_invariant_collapses_zero_or_many_focused_buffers(focused_flags in proptest::collection::vec(any::<bool>(), 1..24)) {
let mut app = App::new();
let _app = app
.add_plugins(crate::ecs::EditorCorePlugin)
.add_plugins(FocusPlugin);
for focused in focused_flags {
let _entity = spawn_view(&mut app, focused);
}
app.update();
prop_assert_eq!(focused_views(&mut app).len(), 1);
}
}
}