use bevy::app::AppExit;
use bevy::ecs::entity::Entity;
use bevy::ecs::hierarchy::{ChildOf, Children};
use bevy::ecs::message::{MessageReader, MessageWriter};
use bevy::ecs::query::{Changed, Has, Or, With, Without};
use bevy::ecs::system::{
Commands, Local, NonSend, NonSendMut, ParallelCommands, Populated, Query, Res, ResMut, Single,
};
use bevy::math::IRect;
use bevy::tasks::AsyncComputeTaskPool;
use bevy::tasks::futures_lite::future;
use bevy::time::Time;
use objc2_foundation::NSPoint;
use std::collections::HashSet;
use std::pin::Pin;
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::time::Duration;
use tracing::{Level, debug, error, info, instrument, trace, warn};
use super::{
ActiveDisplayMarker, BProcess, ExistingMarker, FreshMarker, PollForNotifications,
RepositionMarker, ResizeMarker, RetryFrontSwitch, SpawnWindowTrigger, Timeout, WMEventTrigger,
};
use crate::config::{Config, decorations::BorderRadiusOption};
use crate::ecs::layout::LayoutStrip;
use crate::ecs::params::{ActiveDisplay, Windows};
use crate::ecs::{
ActiveWorkspaceMarker, Bounds, BruteforceWindows, FlashMessage, Initializing,
LocateDockTrigger, LowPowerMode, MissionControlActive, Position, RestoreWindowState, Scrolling,
SelectedVirtualMarker, Unmanaged, WidthRatio, WindowProperties, focus_entity,
};
use crate::events::Event;
use crate::manager::{
Application, Display, Process, Window, WindowManager, WindowOS, bruteforce_windows,
};
use crate::overlay::{FlashMessageManager, OverlayManager};
use crate::platform::PlatformCallbacks;
const ANIAMTE_SNAP_THRESHOLD: f32 = 5.0;
#[allow(clippy::needless_pass_by_value)]
pub(super) fn dispatch_toplevel_triggers(
mut messages: MessageReader<Event>,
broken_notifications: Option<Res<PollForNotifications>>,
mut commands: Commands,
) {
for event in messages.read() {
match event {
Event::WindowCreated { element } => {
if let Ok(window) = WindowOS::new(element)
.inspect_err(|err| {
trace!("not adding window {element:?}: {err}");
})
.map(|window| Window::new(Box::new(window)))
{
commands.trigger(SpawnWindowTrigger(vec![window]));
}
}
Event::SpaceChanged => {
if broken_notifications.is_some() {
info!(
"Workspace and display notifications arriving correctly. Disabling the polling.",
);
commands.remove_resource::<PollForNotifications>();
}
commands.trigger(WMEventTrigger(event.clone()));
}
Event::WindowTitleChanged { window_id } => {
trace!("WindowTitleChanged: {window_id:?}");
}
Event::MenuClosed { window_id } => {
trace!("MenuClosed event: {window_id:?}");
}
Event::DisplayResized { display_id } => {
debug!("Display Resized: {display_id:?}");
}
Event::DisplayConfigured { display_id } => {
debug!("Display Configured: {display_id:?}");
}
Event::SystemWoke { msg } => {
debug!("system woke: {msg:?}");
}
_ => commands.trigger(WMEventTrigger(event.clone())),
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub fn gather_displays(window_manager: Res<WindowManager>, mut commands: Commands) {
let Ok(active_display_id) = window_manager.active_display_id() else {
error!("Unable to get active display id!");
return;
};
for (display, workspaces) in window_manager.present_displays() {
let origin = Position(display.bounds().min);
let entity = if display.id() == active_display_id {
commands.spawn((display, ActiveDisplayMarker))
} else {
commands.spawn(display)
}
.id();
commands.trigger(LocateDockTrigger(entity));
let Ok(active_space) = window_manager.active_display_space(active_display_id) else {
return;
};
for id in workspaces {
let strip = LayoutStrip::new(id, 0);
if id == active_space {
commands.spawn((
strip,
origin.clone(),
ActiveWorkspaceMarker,
SelectedVirtualMarker,
ChildOf(entity),
));
} else {
commands.spawn((
strip,
origin.clone(),
SelectedVirtualMarker,
ChildOf(entity),
));
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::DEBUG, skip_all)]
pub(crate) fn add_existing_process(
window_manager: Res<WindowManager>,
processes: Populated<(Entity, &BProcess), With<ExistingMarker>>,
mut commands: Commands,
) {
for (entity, process) in processes {
let Ok(app) = window_manager.new_application(&*process.0) else {
error!("creating aplication from process '{}'", process.name());
return;
};
commands.spawn((app, ExistingMarker, ChildOf(entity)));
commands.entity(entity).try_remove::<ExistingMarker>();
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::DEBUG, skip_all)]
pub(crate) fn add_existing_application(
window_manager: Res<WindowManager>,
workspaces: Query<&LayoutStrip>,
fresh_apps: Populated<(&mut Application, Entity), With<ExistingMarker>>,
mut commands: Commands,
) {
let spaces = workspaces
.into_iter()
.map(LayoutStrip::id)
.collect::<Vec<_>>();
let thread_pool = AsyncComputeTaskPool::get();
for (mut app, entity) in fresh_apps {
let mut offscreen_windows = vec![];
if app.observe().is_ok_and(|result| result)
&& let Ok((found_windows, offscreen)) = window_manager
.find_existing_application_windows(&mut app, &spaces)
.inspect_err(|err| warn!("{err}"))
{
offscreen_windows.extend(offscreen);
commands.trigger(SpawnWindowTrigger(found_windows));
}
commands.entity(entity).try_remove::<ExistingMarker>();
if !offscreen_windows.is_empty() {
let pid = app.pid();
let bruteforce_task =
thread_pool.spawn(async move { bruteforce_windows(pid, offscreen_windows) });
commands.spawn(BruteforceWindows(bruteforce_task));
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::DEBUG, skip_all)]
pub(crate) fn finish_setup(
process_query: Query<Entity, With<ExistingMarker>>,
windows: Windows,
mut bruteforce_tasks: Query<(Entity, &mut BruteforceWindows)>,
mut workspaces: Query<(&mut LayoutStrip, Has<ActiveWorkspaceMarker>, &ChildOf)>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
if !process_query.is_empty() {
return;
}
if !bruteforce_tasks.is_empty() {
for (entity, mut job) in &mut bruteforce_tasks {
if let Some(found_windows) = future::block_on(future::poll_once(&mut job.0)) {
commands.trigger(SpawnWindowTrigger(found_windows));
commands.entity(entity).despawn();
}
}
return;
}
info!(
"Initialization: found {:?} windows.",
windows.iter().size_hint()
);
for (mut strip, active_strip, _) in &mut workspaces {
debug!("space {}: before refresh {strip:?}", strip.id());
let workspace_windows = window_manager
.windows_in_workspace(strip.id())
.inspect_err(|err| {
warn!("failed to get windows on workspace {}: {err}", strip.id());
})
.ok()
.map(|workspace_windows| {
workspace_windows
.into_iter()
.filter_map(|window_id| windows.find_managed(window_id))
.filter(|(window, entity)| {
if window.is_minimized() {
commands.entity(*entity).try_insert(Unmanaged::Minimized);
false
} else {
true
}
})
.collect::<Vec<_>>()
});
let Some(workspace_windows) = workspace_windows else {
continue;
};
for entity in strip.all_windows() {
if !workspace_windows.iter().any(|(_, e)| *e == entity) {
strip.remove(entity);
}
}
for (_, entity) in workspace_windows {
if !strip.contains(entity) {
strip.append(entity);
}
}
debug!("space {}: after refresh {strip:?}", strip.id());
if active_strip && let Some(entity) = strip.first().ok().and_then(|column| column.top()) {
focus_entity(entity, true, &mut commands);
}
}
commands.remove_resource::<Initializing>();
commands.trigger(RestoreWindowState);
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn add_launched_process(
window_manager: Res<WindowManager>,
fresh_processes: Populated<(Entity, &mut BProcess, Has<Children>), With<FreshMarker>>,
mut commands: Commands,
) {
const APP_OBSERVABLE_TIMEOUT_SEC: u64 = 5;
let mut already_seen = HashSet::new();
for (entity, mut process, children) in fresh_processes {
let process = &mut *process.0;
if !already_seen.insert(process.psn()) {
continue;
}
if !process.ready() {
continue;
}
if children {
commands.entity(entity).try_remove::<FreshMarker>();
continue;
}
let Ok(mut app) = window_manager.new_application(process) else {
error!("creating aplication from process '{}'", process.name());
return;
};
if app.observe().is_ok_and(|good| good) {
let timeout = Timeout::new(
Duration::from_secs(APP_OBSERVABLE_TIMEOUT_SEC),
Some(format!(
"{app} did not become observable in {APP_OBSERVABLE_TIMEOUT_SEC}s.",
)),
);
commands.spawn((app, FreshMarker, timeout, ChildOf(entity)));
} else {
debug!("failed to register some observers {}", process.name());
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn add_launched_application(
app_query: Populated<(&mut Application, Entity, Has<Children>), With<FreshMarker>>,
windows: Windows,
mut commands: Commands,
) {
let find_window = |window_id| windows.find(window_id);
for (app, entity, has_children) in app_query {
let mut create_windows = app.window_list();
create_windows.retain(|window| find_window(window.id()).is_none());
if !create_windows.is_empty() {
commands.entity(entity).try_remove::<FreshMarker>();
debug!(
"spawn! (polling path found {} new windows for {entity})",
create_windows.len(),
);
commands.trigger(SpawnWindowTrigger(create_windows));
} else if has_children {
debug!("removing FreshMarker from {entity}: windows already created via AXCreated");
commands.entity(entity).try_remove::<FreshMarker>();
}
}
}
#[allow(clippy::type_complexity)]
pub(super) fn fresh_marker_cleanup(
cleanup: Populated<
(Entity, Has<FreshMarker>, &Timeout),
Or<(With<BProcess>, With<Application>)>,
>,
mut commands: Commands,
) {
for (entity, fresh, _) in cleanup {
if !fresh {
commands.entity(entity).try_remove::<Timeout>();
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn timeout_ticker(
timers: Populated<(Entity, &mut Timeout)>,
clock: Res<Time>,
mut commands: Commands,
) {
for (entity, mut timeout) in timers {
if timeout.timer.is_finished() {
trace!("Despawning entity {entity} due to timeout.");
if let Some(message) = &timeout.message {
debug!("{message}");
}
trace!("Removing timer {entity}");
commands.entity(entity).despawn();
} else {
timeout.timer.tick(clock.delta());
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn retry_front_switch(
retries: Populated<(Entity, &RetryFrontSwitch)>,
applications: Query<&Application>,
mut commands: Commands,
) {
for (entity, retry) in retries.iter() {
let Ok(app) = applications.get(retry.0) else {
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
continue;
};
if !app.is_frontmost() {
debug!("Discarding stale front switch retry (app no longer frontmost).");
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
continue;
}
if let Ok(focused_id) = app.focused_window_id() {
debug!("Front switch retry succeeded for window {focused_id}.");
commands.trigger(WMEventTrigger(Event::WindowFocused {
window_id: focused_id,
}));
if let Ok(mut entity_commands) = commands.get_entity(entity) {
entity_commands.try_despawn();
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn display_changes_watcher(
displays: Query<(&Display, Has<ActiveDisplayMarker>)>,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
let Ok(current_display_id) = window_manager.active_display_id() else {
return;
};
let found = displays
.iter()
.find(|(display, _)| display.id() == current_display_id);
if let Some((_, active)) = found {
if active {
return;
}
debug!("detected dislay change from {current_display_id}.");
commands.trigger(WMEventTrigger(Event::DisplayChanged));
} else {
debug!("new display {current_display_id} detected.");
commands.trigger(WMEventTrigger(Event::DisplayAdded {
display_id: current_display_id,
}));
}
let present_displays = window_manager.present_displays();
displays.iter().for_each(|(display, _)| {
if !present_displays
.iter()
.any(|(present_display, _)| present_display.id() == display.id())
{
let display_id = display.id();
debug!("detected removal of display {display_id}");
commands.trigger(WMEventTrigger(Event::DisplayRemoved {
display_id: display.id(),
}));
}
});
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn animate_entities(
mut animate: Populated<(&mut Position, Entity, &RepositionMarker)>,
time: Res<Time>,
config: Res<Config>,
commands: ParallelCommands,
) {
let rate = config.animation_speed();
let t = (1.0 - (-rate * time.delta_secs_f64()).exp()).clamp(0.0, 1.0) as f32;
animate
.par_iter_mut()
.for_each(|(mut position, entity, RepositionMarker(origin))| {
let target = origin.as_vec2();
let current = position.0.as_vec2();
let lerped = current.lerp(target, t);
let finished = (target - lerped).length() <= ANIAMTE_SNAP_THRESHOLD;
let new_pos = if finished {
*origin
} else {
lerped.round().as_ivec2()
};
trace!(
"entity {entity} source {} dest {origin} t {t:.3} moving to {new_pos}",
position.0,
);
position.0 = new_pos;
if finished {
commands.command_scope(|mut command| {
command.entity(entity).try_remove::<RepositionMarker>();
});
}
});
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn animate_resize_entities(
mut animate: Populated<(&mut Bounds, Entity, &ResizeMarker)>,
time: Res<Time>,
config: Res<Config>,
commands: ParallelCommands,
) {
let rate = config.animation_speed();
let t = (1.0 - (-rate * time.delta_secs_f64()).exp()).clamp(0.0, 1.0) as f32;
animate
.par_iter_mut()
.for_each(|(mut bounds, entity, ResizeMarker(size))| {
let target = size.as_vec2();
let current = bounds.0.as_vec2();
let lerped = current.lerp(target, t);
let finished = (target - lerped).length() <= ANIAMTE_SNAP_THRESHOLD;
let new_size = if finished {
*size
} else {
lerped.round().as_ivec2()
};
trace!(
"entity {entity} source {} dest {size} t {t:.3} resizing to {new_size}",
bounds.0,
);
bounds.0 = new_size;
if finished {
commands.command_scope(|mut command| {
command.entity(entity).try_remove::<ResizeMarker>();
});
}
});
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn pump_events(
mut exit: MessageWriter<AppExit>,
mut messages: MessageWriter<Event>,
low_power_mode: Option<Res<LowPowerMode>>,
incoming_events: Option<NonSend<Receiver<Event>>>,
platform: Option<NonSendMut<Pin<Box<PlatformCallbacks>>>>,
mut timeout: Local<u32>,
) {
const LOOP_MAX_TIMEOUT_LOWPOWER_MS: u32 = 500;
const LOOP_MAX_TIMEOUT_MS: u32 = 50;
const LOOP_TIMEOUT_STEP: u32 = 1;
let Some((ref mut platform, incoming_events)) = platform.zip(incoming_events) else {
return;
};
platform.pump_cocoa_event_loop(f64::from(*timeout) / 1000.0);
loop {
match incoming_events.recv_timeout(Duration::from_millis(1)) {
Ok(Event::Exit) | Err(RecvTimeoutError::Disconnected) => {
exit.write(AppExit::Success);
break;
}
Ok(event) => {
messages.write(event);
*timeout = LOOP_TIMEOUT_STEP;
}
Err(RecvTimeoutError::Timeout) => {
let timeout_limit = if low_power_mode.is_some_and(|low_power| low_power.0) {
LOOP_MAX_TIMEOUT_LOWPOWER_MS
} else {
LOOP_MAX_TIMEOUT_MS
};
*timeout = timeout.min(timeout_limit) + LOOP_TIMEOUT_STEP;
break;
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn window_resized_update_frame(
mut messages: MessageReader<Event>,
mut windows: Query<(&mut Window, Entity, &Position, &mut Bounds), Without<LayoutStrip>>,
mut active_workspace: Single<(&LayoutStrip, &mut Position), With<ActiveWorkspaceMarker>>,
) {
for event in messages.read() {
let Event::WindowResized { window_id } = event else {
continue;
};
let Some((mut window, entity, position, mut bounds)) = windows
.iter_mut()
.find(|window| window.0.id() == *window_id)
else {
continue;
};
let Ok(new_frame) = window.update_frame() else {
continue;
};
let old_frame = IRect::from_corners(position.0, position.0 + bounds.0);
if old_frame.size() != new_frame.size() {
bounds.0 = new_frame.size();
}
let (active_strip, ref mut strip_position) = *active_workspace;
if old_frame.min.x != new_frame.min.x {
let shift = (old_frame.size() - new_frame.size()).with_y(0);
strip_position.0.x += shift.x;
}
let diff = old_frame.min.y - new_frame.min.y;
if diff.abs() > 0
&& let Some(above_entity) = active_strip.above(entity)
&& let Ok((_, _, _, mut above_bounds)) = windows.get_mut(above_entity)
&& above_bounds.0.y - diff > 200
{
above_bounds.0.y -= diff;
}
}
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn window_moved_update_frame(
mut messages: MessageReader<Event>,
mut windows: Query<(&mut Window, &mut Position, &Bounds), Without<LayoutStrip>>,
) {
for event in messages.read() {
let Event::WindowMoved { window_id } = event else {
continue;
};
let Some((mut window, mut position, bounds)) = windows
.iter_mut()
.find(|window| window.0.id() == *window_id)
else {
continue;
};
let Ok(new_frame) = window.update_frame() else {
continue;
};
let old_frame = IRect::from_corners(position.0, position.0 + bounds.0);
if old_frame.min != new_frame.min {
position.0 = new_frame.min;
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn gather_initial_processes(
receiver: Option<NonSendMut<Receiver<Event>>>,
mut displays: Query<&mut Display>,
mut commands: Commands,
) {
let Some(receiver) = receiver else {
return;
};
let mut initial_processes: Vec<BProcess> = Vec::new();
let mut initial_config = None;
loop {
match receiver.recv().expect("error reading initial processes") {
Event::ProcessesLoaded | Event::Exit => break,
Event::ApplicationLaunched { psn, observer } => {
initial_processes.push(Process::new(&psn, observer.clone()).into());
}
Event::InitialConfig(config) => {
let height = config.menubar_height();
for mut display in &mut displays {
display.set_menubar_height_override(height);
}
initial_config = Some(config);
}
event => warn!("Stray event during initial process gathering: {event:?}"),
}
}
if let Some(config) = initial_config {
commands.insert_resource(config);
}
while let Some(mut process) = initial_processes.pop() {
if process.is_observable() {
debug!("Adding existing process {}", process.name());
commands.spawn((ExistingMarker, process));
} else {
debug!(
"Existing application '{}' is not observable, ignoring it.",
process.name(),
);
}
}
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn update_overlays(
windows: Windows,
applications: Query<&Application>,
active_workspace: Query<(Has<Scrolling>, &LayoutStrip), With<ActiveWorkspaceMarker>>,
overlay_mgr: Option<NonSendMut<OverlayManager>>,
mission_control_active: Res<MissionControlActive>,
config: Res<Config>,
) {
use crate::overlay::BorderParams;
use objc2_foundation::{NSPoint, NSRect, NSSize};
let Some(mut overlay_mgr) = overlay_mgr else {
return;
};
let dim_opacity = config.dim_inactive_opacity();
let border_enabled = config.border_active_window();
let Some((swiping, active_strip)) = active_workspace.iter().next() else {
return;
};
if swiping || mission_control_active.0 || active_strip.is_fullscreen() {
overlay_mgr.hide_all();
return;
}
if dim_opacity == 0.0 && !border_enabled {
overlay_mgr.remove_all();
return;
}
let (focused_abs_cg, focused_border_radius, detected_border_radius) =
if let Some((window, _, unmanaged)) = windows
.focused()
.and_then(|(_, entity)| windows.get_managed(entity))
&& unmanaged.is_none()
&& !window.is_full_screen()
{
let frame = window.frame();
let h_pad = window.horizontal_padding();
let v_pad = window.vertical_padding();
let focused_abs_cg = Some(NSRect::new(
NSPoint::new(
f64::from(frame.min.x + h_pad),
f64::from(frame.min.y + v_pad),
),
NSSize::new(
f64::from(frame.width() - 2 * h_pad),
f64::from(frame.height() - 2 * v_pad),
),
));
let Some(app) = windows
.find_parent(window.id())
.and_then(|(_, _, parent)| applications.get(parent).ok())
else {
return;
};
let properties = WindowProperties::new(app, window, &config);
let focused_border_radius = properties.border_radius();
(
focused_abs_cg,
focused_border_radius,
window.border_radius(),
)
} else {
overlay_mgr.hide_all();
return;
};
let calculated_radius = match config.border_radius() {
BorderRadiusOption::Auto => detected_border_radius.unwrap_or(10.0),
BorderRadiusOption::Value(value) => value.max(0.0),
};
let border_params = border_enabled.then(|| BorderParams {
color: config.border_color(),
opacity: config.border_opacity(),
width: config.border_width(),
radius: focused_border_radius.unwrap_or(calculated_radius),
});
let dim_color = config.dim_inactive_color();
overlay_mgr.update(
dim_opacity,
dim_color,
focused_abs_cg,
border_params.as_ref(),
);
}
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn commit_window_position(
mut moved_windows: Populated<(&mut Window, &Position), Changed<Position>>,
) {
moved_windows
.par_iter_mut()
.for_each(|(mut window, position)| window.reposition(position.0));
}
#[allow(clippy::needless_pass_by_value)]
#[instrument(level = Level::TRACE, skip_all)]
pub(super) fn commit_window_size(
active_display: ActiveDisplay,
mut resized_windows: Populated<(&mut Window, &Bounds, &mut WidthRatio), Changed<Bounds>>,
) {
let display_bounds = active_display.bounds();
resized_windows
.par_iter_mut()
.for_each(|(mut window, size, mut width_ratio)| {
width_ratio.0 = f64::from(size.0.x) / f64::from(display_bounds.width());
window.resize(size.0);
});
}
#[allow(clippy::needless_pass_by_value)]
pub(super) fn cleanup_on_exit(
mut exit_events: MessageReader<AppExit>,
windows: Windows,
window_manager: Res<WindowManager>,
) {
for _ in exit_events.read() {
info!("Cleaning up before exit");
let ids = windows
.iter()
.map(|(window, _)| window.id())
.collect::<Vec<_>>();
window_manager.dim_windows(&ids, 0.0);
}
}
#[allow(clippy::needless_pass_by_value)]
pub(crate) fn update_flash_messages(
messages: Populated<(Entity, &FlashMessage, &Timeout)>,
active_display: Single<(&Display, Entity), With<ActiveDisplayMarker>>,
flash_mgr: Option<NonSendMut<FlashMessageManager>>,
mut commands: Commands,
) {
let Some(mut flash_manager) = flash_mgr else {
return;
};
if messages.is_empty() {
flash_manager.remove();
return;
}
let (display, _) = *active_display;
let bounds = display.bounds();
let top_right = NSPoint::new(f64::from(bounds.max.x), f64::from(bounds.min.y));
for (entity, FlashMessage(flash), timeout) in messages {
if timeout.timer.is_finished() {
flash_manager.remove();
commands.entity(entity).despawn();
} else {
let opacity = timeout.timer.fraction_remaining();
flash_manager.show(flash, opacity, top_right);
}
}
}
pub(crate) fn update_low_power_state(low_power_mode: Option<ResMut<LowPowerMode>>) {
let Some(mut state) = low_power_mode else {
return;
};
let process_info = objc2_foundation::NSProcessInfo::processInfo();
state.0 = process_info.isLowPowerModeEnabled();
}