use bevy_ecs::prelude::*;
use bevy_ecs::system::NonSendMarker;
use windows::Win32::Foundation::HWND;
use super::transition_diag::{ORIGIN_ZORDER_PAIR, WriteTag};
use super::zorder_pair::{
InsertSpec, PairFixDecision, PairObservation, PairTrigger, PeerLoss, SkipReason,
decide_pair_fix, log_orphan_request_dropped, log_owner_detach_failed, log_owner_detached,
log_unbacked_verification_dropped, measure_window_above, measure_window_below,
measure_window_is_always_on_top, record_decision, record_verification,
};
use super::zorder_pair_sink::{SinkMarkQuery, observe_marked_pair_sinks};
use super::{
ExpectedOrder, KeepDirectlyAbove, OwnerLink, ReassertZOrder, SetWindowPosCommand, WindowHandle,
WindowPos, ZOrder, ZOrderPairStrategy,
};
use crate::api::clear_window_owner;
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
pub struct IssuedPairFix {
pub(crate) insert_after: InsertSpec,
}
unsafe impl Send for IssuedPairFix {}
unsafe impl Sync for IssuedPairFix {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PairFixPlan {
pub target: HWND,
pub insert_after: InsertSpec,
pub expected: ExpectedOrder,
}
pub(crate) fn plan_pair_fix(obs: &PairObservation, fix: PairFixDecision) -> Option<PairFixPlan> {
let above = obs.above_hwnd?;
let below = obs.below_hwnd?;
let target = match fix {
PairFixDecision::Skip(_) => return None,
PairFixDecision::PlaceAboveOverBelow { .. } => above,
PairFixDecision::PlaceBelowUnderAbove { .. } => below,
};
Some(PairFixPlan {
target,
insert_after: fix.insert_spec()?,
expected: ExpectedOrder { above, below },
})
}
pub(crate) fn pair_fix_command(hwnd: HWND, insert_after: InsertSpec) -> SetWindowPosCommand {
let pos = WindowPos {
zorder: match insert_after {
InsertSpec::After(after) => ZOrder::InsertAfter(after),
InsertSpec::TopEdge | InsertSpec::TopOfNormalBand => ZOrder::Top,
},
position: None,
size: None,
no_activate: true,
..WindowPos::new()
};
SetWindowPosCommand::new(
hwnd,
0,
0,
0,
0,
pos.build_flags_for_system(),
pos.get_hwnd_insert_after(),
)
.with_tag(WriteTag {
origin: ORIGIN_ZORDER_PAIR,
..WriteTag::UNTAGGED
})
}
type OwnerLinkQuery<'w, 's> = Query<
'w,
's,
(
Entity,
&'static KeepDirectlyAbove,
&'static OwnerLink,
Option<&'static WindowHandle>,
),
>;
pub(crate) type HandleQuery<'w, 's> = Query<'w, 's, Option<&'static WindowHandle>>;
fn detach_owner_links_for_lost_peers(
commands: &mut Commands,
links: &OwnerLinkQuery,
handles: &HandleQuery,
) {
for (entity, declaration, link, handle) in links.iter() {
let peer = declaration.peer;
let loss = match handles.get(peer) {
Ok(Some(_)) => continue,
Ok(None) => PeerLoss::HandleRemoved,
Err(_) => PeerLoss::Despawned,
};
match handle {
Some(handle) => match clear_window_owner(handle.hwnd) {
Ok(()) => log_owner_detached(entity, peer, handle.hwnd, link.owner_hwnd, loss),
Err(err) => {
log_owner_detach_failed(entity, peer, handle.hwnd, link.owner_hwnd, &err)
}
},
None => {
record_decision(
entity,
peer,
PairFixDecision::Skip(SkipReason::HandleMissing),
);
}
}
commands.entity(entity).remove::<OwnerLink>();
}
}
pub fn apply_zorder_pair_maintenance(
_ui_thread: NonSendMarker,
mut commands: Commands,
strategy: Option<Res<ZOrderPairStrategy>>,
mut requests: Query<(
Entity,
&KeepDirectlyAbove,
&mut ReassertZOrder,
Option<&IssuedPairFix>,
)>,
orphans: Query<Entity, (With<ReassertZOrder>, Without<KeepDirectlyAbove>)>,
links: OwnerLinkQuery,
sink_marks: SinkMarkQuery,
handles: HandleQuery,
) {
if !requests.is_empty() || !orphans.is_empty() {
crate::ecs::world::tick_wake::mark(crate::ecs::world::tick_wake::ZORDER);
}
let strategy = strategy.map(|s| *s).unwrap_or_default();
detach_owner_links_for_lost_peers(&mut commands, &links, &handles);
observe_marked_pair_sinks(&mut commands, &sink_marks, &handles);
for entity in orphans.iter() {
log_orphan_request_dropped(entity);
commands.entity(entity).remove::<ReassertZOrder>();
}
let mut fix_issued_this_pass = false;
for (entity, declaration, mut request, issued) in requests.iter_mut() {
let peer = declaration.peer;
let above_hwnd = handles.get(entity).ok().flatten().map(|h| h.hwnd);
let peer_lookup = handles.get(peer);
let below_alive = peer_lookup.is_ok();
let below_hwnd = peer_lookup.ok().flatten().map(|h| h.hwnd);
if let Some(expected) = request.pending_verify {
commands
.entity(entity)
.remove::<(ReassertZOrder, IssuedPairFix)>();
if !below_alive {
record_decision(entity, peer, PairFixDecision::Skip(SkipReason::PeerMissing));
continue;
}
if above_hwnd.is_none() || below_hwnd.is_none() {
record_decision(
entity,
peer,
PairFixDecision::Skip(SkipReason::HandleMissing),
);
continue;
}
let Some(issued) = issued else {
log_unbacked_verification_dropped(entity, expected);
continue;
};
let _verified = record_verification(
entity,
peer,
issued.insert_after,
expected,
measure_window_below(expected.above),
);
continue;
}
let (measured_below_of_above, measured_above_of_below) = match (above_hwnd, below_hwnd) {
(Some(above), Some(below)) if below_alive => {
(measure_window_below(above), measure_window_above(below))
}
_ => (None, None),
};
let measured_above_of_below_is_always_on_top = measured_above_of_below
.map(measure_window_is_always_on_top)
.unwrap_or(false);
let observation = PairObservation {
trigger: PairTrigger::Reassert,
strategy,
above_alive: true,
below_alive,
above_hwnd,
below_hwnd,
measured_below_of_above,
measured_above_of_below,
measured_above_of_below_is_always_on_top,
};
let Some(fix) = record_decision(entity, peer, decide_pair_fix(&observation)) else {
commands.entity(entity).remove::<ReassertZOrder>();
continue;
};
if fix_issued_this_pass {
continue;
}
let Some(plan) = plan_pair_fix(&observation, fix) else {
commands.entity(entity).remove::<ReassertZOrder>();
continue;
};
SetWindowPosCommand::enqueue(pair_fix_command(plan.target, plan.insert_after));
fix_issued_this_pass = true;
request.pending_verify = Some(plan.expected);
commands.entity(entity).insert(IssuedPairFix {
insert_after: plan.insert_after,
});
}
}
#[cfg(test)]
#[path = "zorder_pair_maintain_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "zorder_pair_maintain_multipair_tests.rs"]
mod multipair_tests;
#[cfg(test)]
#[path = "zorder_pair_maintain_detach_tests.rs"]
mod detach_tests;
#[cfg(test)]
#[path = "zorder_pair_maintain_survivor_tests.rs"]
mod survivor_tests;
#[cfg(test)]
#[path = "zorder_pair_maintain_always_on_top_tests.rs"]
mod always_on_top_tests;