use bevy::app::{App, Plugin, PostUpdate, PreUpdate, Update};
use bevy::ecs::change_detection::{DetectChanges as _, DetectChangesMut, Ref};
use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use bevy::ecs::hierarchy::ChildOf;
use bevy::ecs::lifecycle::Add;
use bevy::ecs::message::MessageReader;
use bevy::ecs::observer::On;
use bevy::ecs::query::{Added, Has, With, Without};
use bevy::ecs::schedule::IntoScheduleConfigs as _;
use bevy::ecs::schedule::common_conditions::{not, resource_exists};
use bevy::ecs::system::{Commands, Local, ParamSet, Populated, Query, Res, ResMut, Single};
use bevy::ecs::world::World;
use bevy::time::common_conditions::on_timer;
use std::collections::HashSet;
use std::time::Duration;
use tracing::{Level, debug, error, instrument, warn};
use super::{ActiveDisplayMarker, SpawnWindowTrigger};
use crate::commands::{
Direction, MoveFocus, Operation, filter_window_operations, get_window_in_direction,
};
use crate::config::Config;
use crate::ecs::focus::FocusHistory;
use crate::ecs::layout::{LayoutStrip, PARKED_STRIP_SLIVER, origin_exposing};
use crate::ecs::params::{ActiveDisplay, WindowCtx, Windows};
use crate::ecs::{
ActiveWorkspaceMarker, DockPosition, FocusedMarker, Initializing, ManualStripOffset,
NativeFullscreenMarker, Position, RaiseWindow, RepositionMarker, Scrolling,
SelectedVirtualMarker, SendMessageTrigger, SpawnCommandsExt, Timeout, Unmanaged,
};
use crate::errors::Result;
use crate::events::{DestroySource, Event};
use crate::manager::{Application, Display, Origin, Window, WindowManager};
use crate::platform::{WinID, WorkspaceId};
pub struct WorkspaceEventsPlugin;
type MovableStrips<'w, 's> = Query<
'w,
's,
(
Entity,
&'static mut LayoutStrip,
&'static mut Position,
Has<ActiveWorkspaceMarker>,
Option<&'static mut PreviousStripPosition>,
),
Without<Window>,
>;
type RestorableStrips<'w, 's> = Query<
'w,
's,
(
Entity,
&'static mut Position,
&'static LayoutStrip,
&'static ChildOf,
Option<&'static PreviousStripPosition>,
Option<&'static RepositionMarker>,
),
Without<Window>,
>;
type RenumberStrips<'w, 's> = ParamSet<
'w,
's,
(
Query<'static, 'static, &'static LayoutStrip, Added<LayoutStrip>>,
Query<'static, 'static, (Entity, &'static mut LayoutStrip)>,
),
>;
impl Plugin for WorkspaceEventsPlugin {
fn build(&self, app: &mut App) {
const DISPLAY_CHANGE_CHECK_FREQ: Duration = Duration::from_secs(1);
let reap_workspaces = |config: Option<Res<Config>>| {
config.is_some_and(|config| config.reap_empty_workspaces())
};
app.add_systems(
PreUpdate,
(switch_virtual_workspace_bind, move_virtual_workspace_bind),
);
app.add_systems(
PreUpdate,
find_orphaned_workspaces
.after(crate::ecs::display::reconcile_displays)
.run_if(on_timer(DISPLAY_CHANGE_CHECK_FREQ)),
);
app.add_systems(
Update,
(
renumber_virtual_indexes,
reap_empty_virtual_workspaces.run_if(reap_workspaces),
workspace_change_handler,
workspace_created_handler,
show_active_workspace,
handle_virtual_window_moves,
detect_moved_windows.run_if(not(resource_exists::<Initializing>)),
),
);
app.add_systems(PostUpdate, workspace_destroyed_handler);
app.add_observer(cleanup_active_workspace_marker)
.add_observer(cleanup_selected_space_marker);
}
}
#[derive(Component)]
pub(super) struct VirtualMoveMarker {
pub target_virtual_index: u32,
pub move_focus: MoveFocus,
}
#[derive(Component, Debug)]
pub(crate) struct PreviousStripPosition {
pub origin: Origin,
pub focus: Option<Entity>,
}
#[derive(Component, Debug)]
pub(crate) struct RestoreFocusMarker {
pub entity: Entity,
}
const RESTORE_FOCUS_GUARD_TIMEOUT: Duration = Duration::from_secs(2);
fn spawn_restore_focus_guard(entity: Entity, commands: &mut Commands) {
let timeout = Timeout::new(RESTORE_FOCUS_GUARD_TIMEOUT, None, commands);
commands.spawn((timeout, RestoreFocusMarker { entity }));
}
#[derive(Component, Debug)]
pub(crate) struct SnapStripMarker {
pub strip: Entity,
}
const SNAP_STRIP_GUARD_TIMEOUT: Duration = Duration::from_millis(500);
fn spawn_snap_strip_guard(strip: Entity, commands: &mut Commands) {
let timeout = Timeout::new(SNAP_STRIP_GUARD_TIMEOUT, None, commands);
commands.spawn((timeout, SnapStripMarker { strip }));
}
fn fullscreen_window_in_strip(
workspace_id: WorkspaceId,
strip: &LayoutStrip,
windows: &Windows,
window_manager: &WindowManager,
) -> Option<Entity> {
window_manager
.windows_in_workspace(workspace_id)
.ok()
.and_then(|window_ids| {
window_ids.into_iter().find_map(|window_id| {
windows
.find_managed(window_id)
.map(|(_, entity)| entity)
.filter(|entity| strip.contains(*entity))
})
})
.or_else(|| {
windows.managed_iter().find_map(|(window, entity, _)| {
(strip.contains(entity) && window.is_full_screen()).then_some(entity)
})
})
.or_else(|| {
windows
.focused()
.map(|(_, entity)| entity)
.filter(|entity| strip.contains(*entity))
})
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
fn workspace_change_handler(
mut messages: MessageReader<Event>,
windows: Windows,
mut workspaces: Query<(
&mut LayoutStrip,
Entity,
Has<ActiveWorkspaceMarker>,
Has<SelectedVirtualMarker>,
)>,
active_display: Single<(&Display, Entity), With<ActiveDisplayMarker>>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
if !messages
.read()
.any(|event| matches!(event, Event::SpaceChanged))
{
return;
}
let (active_display, display_entity) = *active_display;
let Ok(workspace_id) = window_manager.active_display_space(active_display.id()) else {
error!("Unable to get active workspace id!");
return;
};
let mut remove_from = None;
let mut insert_into = None;
for (strip, entity, active, selected) in &workspaces {
if active && strip.id() == workspace_id {
debug!("Workspace id {} already active", strip.id());
return;
}
if active && strip.id() != workspace_id {
debug!("Workspace id {} no longer active", strip.id());
remove_from = Some(entity);
}
if !active && strip.id() == workspace_id && selected {
debug!("Workspace id {} is active", strip.id());
insert_into = Some(entity);
}
}
if insert_into.is_none() {
insert_into = workspaces
.iter()
.filter(|(strip, _, _, _)| strip.id() == workspace_id)
.min_by_key(|(strip, _, _, _)| strip.virtual_index)
.map(|(_, entity, _, _)| entity);
}
if insert_into.is_none()
&& let Some(old_space) = remove_from
&& window_manager.is_fullscreen_space(active_display.id())
&& let Ok((mut old_strip, old_strip_entity, _, _)) = workspaces.get_mut(old_space)
&& let Some(fullscreen_window) =
fullscreen_window_in_strip(workspace_id, &old_strip, &windows, &window_manager)
&& let Ok(original_index) = old_strip.index_of(fullscreen_window)
{
debug!("workspace_change: space={workspace_id} fullscreen");
let fullscreen_marker = NativeFullscreenMarker {
layout_strip: old_strip_entity,
workspace_id: old_strip.id(),
index: original_index,
};
old_strip.remove(fullscreen_window);
let fullscreen_strip = LayoutStrip::fullscreen(workspace_id, fullscreen_window);
let entity = commands
.spawn((
Position(active_display.bounds().min),
fullscreen_marker,
fullscreen_strip,
ChildOf(display_entity),
))
.id();
insert_into = Some(entity);
}
if let Some(into) = insert_into
&& let Ok(mut entity_commands) = commands.get_entity(into)
{
entity_commands.try_insert(ActiveWorkspaceMarker);
}
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
fn detect_moved_windows(
activated_workspace: Single<Entity, Added<ActiveWorkspaceMarker>>,
mut workspaces: Query<(&mut LayoutStrip, Entity, Has<NativeFullscreenMarker>)>,
apps: Query<&mut Application>,
window_manager: Res<WindowManager>,
mut ignored_windows: Local<HashSet<WinID>>,
mut ctx: WindowCtx,
) {
let Ok(workspace_id) = workspaces
.get(*activated_workspace)
.map(|strip| strip.0.id())
else {
return;
};
debug!("workspace {workspace_id}");
let strips = workspaces
.iter()
.filter_map(|strip| (strip.0.id() == workspace_id).then_some(strip.0))
.collect::<Vec<_>>();
let find_window = |window_id| {
ctx.windows
.find_managed(window_id)
.map(|(_, entity)| entity)
};
let Ok((moved_windows, mut unresolved)) =
windows_not_in_strips(workspace_id, find_window, &strips, &window_manager).inspect_err(
|err| {
warn!("unable to get windows in the current workspace: {err}");
},
)
else {
return;
};
unresolved.retain(|window_id| {
!ignored_windows.contains(window_id) && ctx.windows.find(*window_id).is_none()
});
if !unresolved.is_empty() {
let unresolved_ids = unresolved.iter().copied().collect::<HashSet<_>>();
let retry_windows = apps
.into_iter()
.flat_map(|app| {
app.window_list(&ctx.config)
.into_iter()
.filter(|window| unresolved_ids.contains(&window.id()))
})
.collect::<Vec<_>>();
if retry_windows.is_empty() {
for id in unresolved_ids {
ignored_windows.insert(id);
}
} else {
debug!(
"retrying unresolved windows: {}",
retry_windows
.iter()
.map(|window| format!("{}", window.id()))
.collect::<Vec<_>>()
.join(" ")
);
ctx.commands.trigger(SpawnWindowTrigger(retry_windows));
}
}
for entity in moved_windows {
if workspaces
.iter()
.any(|(strip, _, fullscreen)| fullscreen && strip.contains(entity))
{
continue;
}
debug!("Window {entity} moved to workspace {workspace_id}.");
let moving_entities = workspaces
.iter()
.find_map(|(strip, _, _)| strip.tab_group(entity))
.unwrap_or_else(|| vec![entity]);
for (mut strip, strip_entity, _) in &mut workspaces {
if strip_entity == *activated_workspace {
strip.append_tab_group(&moving_entities);
} else {
for moving_entity in &moving_entities {
strip.remove(*moving_entity);
}
}
}
}
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
fn workspace_destroyed_handler(
mut messages: MessageReader<Event>,
mut workspaces: Populated<(&mut LayoutStrip, Entity, Option<&NativeFullscreenMarker>)>,
mut focus_history: ResMut<FocusHistory>,
mut commands: Commands,
) {
for event in messages.read() {
let Event::SpaceDestroyed { space_id } = event else {
continue;
};
focus_history.forget_workspace(*space_id);
let Some((entity, fullscreen)) =
workspaces.iter().find_map(|(strip, entity, fullscreen)| {
let window = strip.first().ok().and_then(|col| col.top());
(strip.id() == *space_id).then_some((entity, window.zip(fullscreen.cloned())))
})
else {
continue;
};
if let Some((
window,
NativeFullscreenMarker {
layout_strip,
workspace_id,
index,
},
)) = fullscreen
{
let mut strip = workspaces
.iter_mut()
.find_map(|(strip, entity, _)| (entity == layout_strip).then_some(strip));
if strip.is_none() {
strip = workspaces
.iter_mut()
.find_map(|(strip, _, _)| (strip.id() == workspace_id).then_some(strip));
}
debug!(
"previously fullscreened window {entity} inserted at {}",
index
);
if let Some(mut strip) = strip {
strip.insert_at(index, window);
commands.reshuffle_around(window);
}
}
if let Ok(mut entity_commands) = commands.get_entity(entity) {
debug!("Workspace destroyed {space_id} {entity}");
entity_commands.try_despawn();
}
}
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
fn workspace_created_handler(
mut messages: MessageReader<Event>,
active_display: Single<(&Display, Entity), With<ActiveDisplayMarker>>,
workspaces: Query<&LayoutStrip>,
mut commands: Commands,
) {
for event in messages.read() {
let Event::SpaceCreated { space_id } = event else {
continue;
};
if workspaces.into_iter().any(|strip| strip.id() == *space_id) {
warn!("Workspace {space_id} already exists!");
continue;
}
debug!("Workspace create {space_id}");
let (active_display, display_entity) = *active_display;
let strip = LayoutStrip::new(*space_id, 0);
let origin = active_display.bounds().min;
commands.spawn_layout_strip(strip, origin, display_entity, false);
}
}
fn windows_not_in_strips<F: Fn(WinID) -> Option<Entity>>(
workspace_id: WorkspaceId,
find_window: F,
strips: &[&LayoutStrip],
window_manager: &WindowManager,
) -> Result<(Vec<Entity>, Vec<WinID>)> {
window_manager
.windows_in_workspace(workspace_id)
.map(|ids| {
let mut moved = Vec::new();
let mut unresolved = Vec::new();
for id in ids {
if let Some(entity) = find_window(id) {
if strips.iter().any(|strip| strip.contains(entity)) {
continue;
}
moved.push(entity);
} else {
unresolved.push(id);
}
}
(moved, unresolved)
})
}
#[instrument(level = Level::DEBUG, skip_all)]
fn find_orphaned_workspaces(
orphans: Populated<(&LayoutStrip, Entity, &Timeout, Option<&ChildOf>), With<Timeout>>,
displays: Populated<(&Display, Entity)>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
let present = window_manager.present_displays();
let mut rehomed = false;
for (orphan, orphan_entity, timeout, child) in orphans {
if orphan.len() == 0 && orphan.virtual_index > 0 {
if let Ok(mut cmd) = commands.get_entity(orphan_entity) {
cmd.try_despawn();
}
debug!("despawning empty orphan workspace {}", orphan.id());
continue;
}
if child.is_some() {
if let Ok(mut cmd) = commands.get_entity(orphan_entity) {
cmd.try_remove::<Timeout>();
}
debug!(
"layout strip {} was re-parented, removing timeout.",
orphan.id()
);
continue;
}
if timeout.timer.is_finished() {
debug!("Rescue windows from timed out orphan {}.", orphan.id());
for lost_window in orphan.all_windows() {
if let Ok(mut cmd) = commands.get_entity(lost_window) {
cmd.try_insert(Unmanaged::Floating);
}
}
continue;
}
let target = present.iter().find_map(|(present_display, spaces)| {
if spaces.iter().any(|&id| id == orphan.id()) {
displays
.iter()
.find(|(d, _)| d.id() == present_display.id())
} else {
None
}
});
let Some((target_display, target_entity)) = target else {
continue; };
debug!(
"Re-parenting orphaned strip {} to display {}",
orphan.id(),
target_display.id(),
);
if let Ok(mut cmd) = commands.get_entity(orphan_entity) {
cmd.try_remove::<Timeout>().insert(ChildOf(target_entity));
commands.queue(move |world: &mut World| {
crate::ecs::display::refresh_reparented_strip(world, orphan_entity);
});
rehomed = true;
}
}
if rehomed {
commands.trigger(SendMessageTrigger(Event::DisplayChanged));
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn cleanup_unordered_windows(
windows: Query<&Window>,
workspaces: Query<&LayoutStrip>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
let windows = workspaces
.iter()
.flat_map(|strip| {
strip
.all_windows()
.into_iter()
.filter(|entity| !strip.tabbed(*entity))
})
.filter_map(|entity| windows.get(entity).ok());
for window in windows {
let window_id = window.id();
if window_manager.window_is_unordered(window_id) && window.role().is_err() {
debug!("Window {window_id} is unordered; removing it.");
commands.trigger(SendMessageTrigger(Event::WindowDestroyed {
window_id,
source: DestroySource::Accessibility,
}));
}
}
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
fn cleanup_active_workspace_marker(
trigger: On<Add, ActiveWorkspaceMarker>,
workspaces: Query<(Entity, Has<ActiveWorkspaceMarker>), With<LayoutStrip>>,
mut commands: Commands,
) {
workspaces.iter().for_each(|(entity, marker)| {
if entity == trigger.entity
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_insert(SelectedVirtualMarker);
} else if marker && let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_remove::<ActiveWorkspaceMarker>();
}
});
}
#[instrument(level = Level::DEBUG, skip_all, fields(trigger))]
fn cleanup_selected_space_marker(
trigger: On<Add, SelectedVirtualMarker>,
workspaces: Query<(Entity, &LayoutStrip, Has<SelectedVirtualMarker>)>,
mut commands: Commands,
) {
let Ok(workspace_id) = workspaces
.get(trigger.entity)
.map(|(_, strip, _)| strip.id())
else {
return;
};
workspaces.iter().for_each(|(entity, strip, marker)| {
if marker
&& entity != trigger.entity
&& strip.id() == workspace_id
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_remove::<SelectedVirtualMarker>();
}
});
}
#[allow(clippy::too_many_lines)]
fn handle_virtual_window_moves(
moved_windows: Populated<(Entity, &VirtualMoveMarker), With<Window>>,
mut workspaces: MovableStrips,
windows: Windows,
mut scrollings: Query<&mut Scrolling>,
active_display: Single<(Entity, &Display, Option<&DockPosition>), With<ActiveDisplayMarker>>,
config: Res<Config>,
mut commands: Commands,
) {
let Some((workspace_id, source_entity)) = workspaces
.iter()
.find_map(|(entity, strip, _, active, _)| active.then_some((strip.id(), entity)))
else {
return;
};
let (display_entity, active_display, dock) = *active_display;
let viewport = active_display.actual_display_bounds(dock, &config);
for (window_entity, move_marker) in &moved_windows {
let moving_entities = workspaces
.get(source_entity)
.ok()
.and_then(|(_, strip, _, _, _)| strip.tab_group(window_entity))
.unwrap_or_else(|| vec![window_entity]);
for moving_entity in &moving_entities {
if let Ok(mut entity_commands) = commands.get_entity(*moving_entity) {
entity_commands.try_remove::<VirtualMoveMarker>();
}
}
let follow = matches!(move_marker.move_focus, MoveFocus::Follow);
let target_idx = move_marker.target_virtual_index;
let target = workspaces.iter().find_map(|(entity, strip, _, _, _)| {
(strip.id() == workspace_id && strip.virtual_index == target_idx).then_some(entity)
});
let source_neighbour =
workspaces
.get(source_entity)
.ok()
.and_then(|(_, strip, _, _, _)| {
strip
.left_neighbour(window_entity)
.or_else(|| strip.right_neighbour(window_entity))
});
let stay = !follow && source_neighbour.is_some();
let moved_left = config
.insert_windows_mid_strip()
.then(|| windows.moving_frame(window_entity).map(|frame| frame.min.x))
.flatten();
let mid_placement = moved_left.and_then(|moved_left| {
let (_, strip, position, _, previous) = workspaces.get(target?).ok()?;
let scroll_x = previous.map_or(position.0.x, |previous| previous.origin.x);
Some(mid_strip_slot(strip, scroll_x, moved_left, &windows))
});
let target_entity = if let Some(entity) = target {
entity
} else {
let visible_origin = viewport.min;
let shown = Origin::new(moved_left.unwrap_or(visible_origin.x), visible_origin.y);
let parked = viewport.max - PARKED_STRIP_SLIVER;
let origin = if stay { parked } else { shown };
debug!(
"Creating new virtual row {target_idx} on workspace {}",
workspace_id
);
let mut new_strip = LayoutStrip::new(workspace_id, target_idx);
new_strip.append_tab_group(&moving_entities);
let mut spawned = commands.spawn_layout_strip(new_strip, origin, display_entity, false);
if stay {
spawned.insert(PreviousStripPosition {
origin: shown,
focus: Some(window_entity),
});
}
spawned.id()
};
if !stay
&& let Ok(mut entity_commands) = commands.get_entity(source_entity)
&& let Ok((_, source_strip, position, _, _)) = workspaces.get(source_entity)
{
let focus = source_strip
.left_neighbour(window_entity)
.or_else(|| source_strip.right_neighbour(window_entity));
entity_commands.try_insert(PreviousStripPosition {
origin: position.0,
focus,
});
}
for (entity, mut strip, _, _, _) in &mut workspaces {
if entity == target_entity {
match mid_placement {
Some((slot, _)) => strip.insert_tab_group_at(slot, &moving_entities),
None => strip.append_tab_group(&moving_entities),
}
} else {
for moving_entity in &moving_entities {
strip.remove(*moving_entity);
}
}
}
if let Some((_, desired_scroll)) = mid_placement {
if let Ok((_, _, _, _, Some(mut previous))) = workspaces.get_mut(target_entity) {
previous.origin.x = desired_scroll;
}
if let Ok(mut scroll) = scrollings.get_mut(target_entity) {
scroll.position = f64::from(desired_scroll);
scroll.velocity = 0.0;
}
}
if let Ok(mut entity_commands) = commands.get_entity(target_entity)
&& !stay
{
entity_commands.try_insert(ActiveWorkspaceMarker);
}
if stay && let Some(neighbour) = source_neighbour {
commands.focus_entity(neighbour, false);
if let Ok(mut position) = workspaces
.get_mut(target_entity)
.map(|(_, _, pos, _, _)| pos)
{
position.set_changed();
}
} else {
let previous_position = workspaces
.get_mut(target_entity)
.ok()
.and_then(|(_, _, _, _, previos_pos)| previos_pos);
if let Some(mut previous) = previous_position {
previous.focus = Some(window_entity);
}
commands.reshuffle_around(window_entity);
if mid_placement.is_none()
&& let Ok(mut entity_commands) = commands.get_entity(window_entity)
{
entity_commands.try_remove::<FocusedMarker>();
commands.focus_entity(window_entity, false);
}
}
debug!(
"Moved window {} to virtual workspace {}",
window_entity, target_idx
);
}
}
fn mid_strip_slot(
strip: &LayoutStrip,
scroll_x: i32,
moved_left: i32,
windows: &Windows,
) -> (usize, i32) {
let columns: Vec<(i32, i32)> = strip
.all_columns()
.into_iter()
.filter_map(|column| {
let layout_x = windows.layout_position(column)?.0.x;
let width = windows
.moving_frame(column)
.map_or(0, |frame| frame.width());
Some((layout_x, width))
})
.collect();
let mut index = columns.len();
let mut chosen_layout_x = columns
.last()
.map_or(0, |(layout_x, width)| layout_x + width);
let mut best = (chosen_layout_x + scroll_x - moved_left).abs();
for (i, (layout_x, _)) in columns.iter().enumerate() {
let dist = (layout_x + scroll_x - moved_left).abs();
if dist < best {
best = dist;
index = i;
chosen_layout_x = *layout_x;
}
}
(index, moved_left - chosen_layout_x)
}
#[allow(clippy::too_many_lines)]
#[instrument(level = Level::DEBUG, skip_all)]
fn switch_virtual_workspace_bind(
mut messages: MessageReader<Event>,
windows: Windows,
active_display: ActiveDisplay,
workspaces: Query<(Entity, &LayoutStrip, Has<ActiveWorkspaceMarker>)>,
config: Res<Config>,
mut commands: Commands,
) {
let Some(operation) = filter_window_operations(&mut messages, |op| {
matches!(
op,
Operation::Virtual(_)
| Operation::VirtualNumber(_)
| Operation::VirtualAdd
| Operation::FocusOrVirtual(_)
)
})
.next() else {
return;
};
let synthesized_virtual;
let operation = match operation {
Operation::FocusOrVirtual(direction @ (Direction::North | Direction::South)) => {
if let Some((_, focused_entity)) = windows.focused()
&& let Some(target) = get_window_in_direction(
direction,
focused_entity,
active_display.active_strip(),
)
{
commands.focus_entity(target, true);
commands.reshuffle_around(target);
return;
}
synthesized_virtual = Operation::Virtual(direction.clone());
&synthesized_virtual
}
Operation::FocusOrVirtual(_) => return,
operation => operation,
};
let workspace_id = active_display.active_strip().id();
let mut rows = workspaces
.iter()
.filter(|(_, strip, _)| strip.id() == workspace_id)
.collect::<Vec<_>>();
if rows.is_empty() {
return;
}
rows.sort_by_key(|(_, strip, _)| strip.virtual_index);
let current_index = rows.iter().position(|(_, _, active)| *active).unwrap_or(0);
let next_index = match operation {
Operation::Virtual(Direction::South | Direction::East) => {
if current_index + 1 < rows.len() {
current_index + 1
} else if active_display.active_strip().len() != 0
&& config.create_workspace_automatically()
{
let target_index = rows[current_index].1.virtual_index + 1;
commands.spawn_layout_strip(
LayoutStrip::new(workspace_id, target_index),
active_display.bounds().min,
active_display.entity(),
true,
);
if config.workspace_popup_status() {
commands.flash_message(format!("{}", target_index + 1), 1.0);
}
return;
} else {
current_index
}
}
Operation::Virtual(Direction::North | Direction::West) => current_index.saturating_sub(1),
Operation::Virtual(Direction::First) => 0,
Operation::Virtual(Direction::Last) => rows.len().saturating_sub(1),
Operation::VirtualNumber(target_virtual_index) => {
let Some(index) = rows
.iter()
.position(|(_, strip, _)| strip.virtual_index == *target_virtual_index)
else {
commands.spawn_layout_strip(
LayoutStrip::new(workspace_id, *target_virtual_index),
active_display.bounds().min,
active_display.entity(),
true,
);
if config.workspace_popup_status() {
commands.flash_message(format!("{}", *target_virtual_index + 1), 1.0);
}
return;
};
index
}
Operation::VirtualAdd => {
let next_virtual_index = rows
.iter()
.map(|(_, strip, _)| strip.virtual_index)
.max()
.unwrap_or(0)
+ 1;
commands.spawn_layout_strip(
LayoutStrip::new(workspace_id, next_virtual_index),
active_display.bounds().min,
active_display.entity(),
true,
);
if config.workspace_popup_status() {
commands.flash_message(format!("{}", next_virtual_index + 1), 1.0);
}
return;
}
_ => return,
};
if next_index == current_index {
return;
}
let new_entity = rows[next_index].0;
let next_virtual_index = rows[next_index].1.virtual_index;
if let Ok(mut entity_commands) = commands.get_entity(new_entity) {
entity_commands.try_insert(ActiveWorkspaceMarker);
if config.workspace_popup_status() {
commands.flash_message(format!("{}", next_virtual_index + 1), 1.0);
}
}
debug!(
"Switched virtual workspace on display {} from {} to {}",
active_display.id(),
rows[current_index].1.virtual_index,
rows[next_index].1.virtual_index
);
}
#[instrument(level = Level::DEBUG, skip_all)]
fn move_virtual_workspace_bind(
mut messages: MessageReader<Event>,
windows: Windows,
active_display: ActiveDisplay,
workspaces: Query<(Entity, &LayoutStrip, Has<ActiveWorkspaceMarker>)>,
config: Res<Config>,
mut commands: Commands,
) {
let Some(operation) = filter_window_operations(&mut messages, |op| {
matches!(
op,
Operation::VirtualMove(_, _) | Operation::VirtualMoveNumber(_, _)
)
})
.next() else {
return;
};
let Some((_, focused_entity)) = windows.focused() else {
return;
};
let mut rows = workspaces
.iter()
.filter(|(_, strip, _)| strip.id() == active_display.active_strip().id())
.collect::<Vec<_>>();
rows.sort_by_key(|(_, strip, _)| strip.virtual_index);
let current_virtual_index = active_display.active_strip().virtual_index;
let current_index = rows.iter().position(|(_, _, active)| *active).unwrap_or(0);
let (target_virtual_index, move_focus) = match operation {
Operation::VirtualMove(Direction::South | Direction::East, move_focus)
if active_display.active_strip().len() > 1 =>
{
if config.create_workspace_automatically() && current_index + 1 < rows.len() {
(rows[current_index + 1].1.virtual_index, *move_focus)
} else {
(current_virtual_index + 1, *move_focus)
}
}
Operation::VirtualMove(Direction::North | Direction::West, move_focus) => {
if current_virtual_index == 0 {
return;
}
(current_virtual_index - 1, *move_focus)
}
Operation::VirtualMove(Direction::First, move_focus) => {
if current_virtual_index == 0 {
return;
}
(0, *move_focus)
}
Operation::VirtualMove(Direction::Last, move_focus) => {
let last_virtual_index = rows.last().map_or(0, |(_, strip, _)| strip.virtual_index);
if current_virtual_index == last_virtual_index {
return;
}
(last_virtual_index, *move_focus)
}
Operation::VirtualMoveNumber(target_virtual_index, move_focus) => {
if *target_virtual_index == current_virtual_index {
return;
}
(*target_virtual_index, *move_focus)
}
_ => return,
};
if let Ok(mut entity_commands) = commands.get_entity(focused_entity) {
entity_commands.try_insert(VirtualMoveMarker {
target_virtual_index,
move_focus,
});
}
if move_focus == MoveFocus::Follow && config.workspace_popup_status() {
commands.flash_message(format!("{}", target_virtual_index + 1), 1.0);
}
debug!("Moving {focused_entity} to new virtual space {target_virtual_index}");
}
fn column_closest_to_center(
strip: &LayoutStrip,
display: &Display,
windows: &Windows,
) -> Option<Entity> {
let display_center = display.bounds().center().x;
strip
.all_columns()
.into_iter()
.filter_map(|candidate| {
let center = windows.moving_frame(candidate)?.center().x;
Some((candidate, (center - display_center).abs()))
})
.min_by_key(|(_, distance)| *distance)
.map(|(candidate, _)| candidate)
}
#[instrument(level = Level::DEBUG, skip_all)]
pub(crate) fn show_active_workspace(
activated: Single<Entity, Added<ActiveWorkspaceMarker>>,
windows: Windows,
mut workspaces: RestorableStrips,
displays: Query<(&Display, Option<&DockPosition>)>,
focus_markers: Query<Ref<FocusedMarker>>,
config: Res<Config>,
mut commands: Commands,
) {
let Some(workspace_id) = workspaces
.iter()
.find_map(|(entity, _, strip, _, _, _)| (entity == *activated).then_some(strip.id()))
else {
return;
};
let current_focus = windows.focused();
let current_workspace = workspaces
.iter_mut()
.filter(|(entity, _, strip, _, _, _)| strip.id() == workspace_id && *entity != *activated);
for (entity, mut position, strip, child, previous, moving) in current_workspace {
let Ok((active_display, _)) = displays.get(child.parent()) else {
continue;
};
if previous.is_none() {
let mut focus =
current_focus.and_then(|(_, entity)| strip.contains(entity).then_some(entity));
if focus.is_none() {
focus = column_closest_to_center(strip, active_display, &windows);
debug!("No previous focus, taking centered one {focus:?}.");
}
let origin = match &moving {
Some(RepositionMarker(destination)) => *destination,
None => position.0,
};
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_insert(PreviousStripPosition { origin, focus });
}
}
let bounds = active_display.bounds();
if let Ok(mut cmd) = commands.get_entity(entity) {
cmd.try_remove::<Scrolling>()
.try_remove::<RepositionMarker>()
.try_remove::<ManualStripOffset>();
}
if config.virtual_workspace_animations() {
commands.reposition_entity(entity, bounds.max - PARKED_STRIP_SLIVER);
} else {
position.0 = bounds.max - PARKED_STRIP_SLIVER;
}
}
let Ok((_, mut position, strip, child, previous_position, _)) = workspaces.get_mut(*activated)
else {
return;
};
debug!("showing virtual workspace {} ({})", strip.id(), *activated);
if let Some(PreviousStripPosition { origin, focus }) = previous_position {
if let Ok(mut entity_commands) = commands.get_entity(*activated) {
entity_commands.try_remove::<PreviousStripPosition>();
}
let focused = windows
.focused()
.map(|(_, entity)| entity)
.filter(|entity| strip.contains(*entity));
let keeps_focus = focused.is_some();
let arriving_focus = focused.filter(|entity| {
focus_markers
.get(*entity)
.is_ok_and(|marker| marker.is_added())
});
let origin = arriving_focus
.and_then(|focus_entity| {
let layout = windows.layout_position(focus_entity)?.0;
let size = windows.size(focus_entity)?;
let (display, dock) = displays.get(child.parent()).ok()?;
let viewport = display.actual_display_bounds(dock, &config);
Some(origin_exposing(layout, size, *origin, viewport))
})
.unwrap_or(*origin);
if config.virtual_workspace_animations() {
commands.reposition_entity(*activated, origin);
} else {
position.0 = origin;
spawn_snap_strip_guard(*activated, &mut commands);
}
if keeps_focus {
if let Some(focus_entity) = arriving_focus {
commands.ensure_visible_snap(focus_entity, !config.virtual_workspace_animations());
}
return;
}
if let Some(focus) = *focus
&& strip.contains(focus)
{
spawn_restore_focus_guard(focus, &mut commands);
commands.focus_entity(focus, false);
commands.trigger(RaiseWindow {
entity: focus,
with_strip: true,
});
}
}
}
fn renumber_virtual_indexes(mut set: RenumberStrips) {
let affected: HashSet<WorkspaceId> = set.p0().iter().map(LayoutStrip::id).collect();
if affected.is_empty() {
return;
}
let mut strips = set.p1();
for workspace_id in affected {
let mut rows = strips
.iter_mut()
.filter(|(_, strip)| strip.id() == workspace_id)
.collect::<Vec<_>>();
rows.sort_by_key(|(_, strip)| strip.virtual_index);
let mut taken: HashSet<u32> = HashSet::new();
let mut dups: Vec<usize> = Vec::new();
for (i, (_, strip)) in rows.iter().enumerate() {
if !taken.insert(strip.virtual_index) {
dups.push(i);
}
}
if dups.is_empty() {
continue;
}
for i in dups {
let mut probe = 0u32;
while !taken.insert(probe) {
probe += 1;
}
rows[i].1.virtual_index = probe;
}
}
}
fn reap_empty_virtual_workspaces(
changed: Single<Entity, Added<ActiveWorkspaceMarker>>,
strips: Populated<(Entity, &LayoutStrip)>,
mut commands: Commands,
) {
let changed_entity = *changed;
let Some(workspace_id) = strips.get(changed_entity).ok().map(|(_, strip)| strip.id()) else {
return;
};
debug!("cleaning up virtual workspaces on space {workspace_id}");
let mut rows = strips
.iter()
.filter(|(_, strip)| strip.id() == workspace_id)
.collect::<Vec<_>>();
rows.sort_by_key(|(_, strip)| strip.virtual_index);
if rows.is_empty() {
return;
}
for (entity, strip) in rows {
if entity != changed_entity
&& strip.virtual_index > 0
&& strip.len() == 0
&& let Ok(mut entity_commands) = commands.get_entity(entity)
{
entity_commands.try_despawn();
}
}
}