use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use bevy::ecs::entity::EntityHashMap;
use bevy::prelude::*;
use bevy::ui::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform};
use crate::bridge::{JsBridge, ReactNode};
use crate::protocol::ROOT_ID;
use crate::react_message;
use crate::reconcile::climb;
use super::DevtoolsState;
use super::pick::highlight_rect;
const FADE_SECS: f32 = 0.4;
const PEAK_ALPHA: f32 = 0.3;
#[react_message(name = "devtools.paintFlash")]
pub(super) struct DevtoolsPaintFlashMessage {
on: bool,
}
pub(super) fn on_paint_flash_message(
msg: On<DevtoolsPaintFlashMessage>,
mut state: ResMut<DevtoolsState>,
) {
state.paint_flash = msg.event().on;
}
#[derive(Resource, Clone, Default)]
pub(super) struct RepaintFeed(Arc<FeedInner>);
#[derive(Default)]
struct FeedInner {
on: AtomicBool,
nodes: Mutex<Vec<Entity>>,
}
impl RepaintFeed {
fn push(&self, nodes: impl Iterator<Item = Entity>) {
if let Ok(mut feed) = self.0.nodes.lock() {
feed.extend(nodes);
}
}
fn take(&self) -> Vec<Entity> {
self.0
.nodes
.lock()
.map(|mut feed| std::mem::take(&mut *feed))
.unwrap_or_default()
}
}
pub(super) fn record_repaints(
feed: Res<RepaintFeed>,
extracted: Option<Res<bevy::ui_render::ExtractedUiNodes>>,
) {
if feed.0.on.load(Ordering::Relaxed)
&& let Some(extracted) = extracted
{
feed.push(extracted.changed.iter().map(|main| main.id()));
}
}
#[derive(Component)]
pub(super) struct PaintFlashBox;
#[derive(Resource, Default)]
pub(super) struct PaintFlashes {
live: EntityHashMap<(Entity, f32)>,
free: Vec<Entity>,
}
#[allow(clippy::too_many_arguments)]
pub(super) fn drive_paint_flash(
state: Res<DevtoolsState>,
feed: Res<RepaintFeed>,
time: Res<Time>,
bridge: Option<Res<JsBridge>>,
targets: Query<(&ComputedNode, &UiGlobalTransform), With<ReactNode>>,
cameras: Query<&ComputedUiTargetCamera>,
child_of: Query<&ChildOf>,
flashes: ResMut<PaintFlashes>,
mut boxes: Query<(&mut Node, &mut BackgroundColor), With<PaintFlashBox>>,
mut commands: Commands,
) {
let on = state.open && state.paint_flash;
if feed.0.on.swap(on, Ordering::Relaxed) && !on {
feed.take();
}
if !on && flashes.live.is_empty() {
return;
}
let PaintFlashes { live, free } = flashes.into_inner();
let dt = time.delta_secs();
live.retain(|_, (flash_box, age)| {
let Ok((mut node, mut color)) = boxes.get_mut(*flash_box) else {
return false;
};
*age += dt;
if *age >= FADE_SECS {
node.display = Display::None;
free.push(*flash_box);
return false;
}
color.0 = flash_color(*age);
true
});
let Some(bridge) = bridge.filter(|_| on) else {
return;
};
let camera_of = |e: Entity| cameras.get(e).ok().and_then(ComputedUiTargetCamera::get);
let window_camera = bridge.nodes.get(&ROOT_ID).copied().and_then(camera_of);
let panel = state
.panel_root
.and_then(|id| bridge.nodes.get(&id).copied());
let mut repainted = feed.take();
repainted.sort_unstable();
repainted.dedup();
for node in repainted {
let Ok((computed, transform)) = targets.get(node) else {
continue;
};
if camera_of(node) != window_camera
|| panel.is_some_and(|panel| climb(node, &child_of, |e| e == panel).is_some())
{
continue;
}
let (pos, size) = highlight_rect(computed, transform);
let rect = Node {
position_type: PositionType::Absolute,
left: Val::Px(pos.x),
top: Val::Px(pos.y),
width: Val::Px(size.x),
height: Val::Px(size.y),
..default()
};
let peak = BackgroundColor(flash_color(0.0));
let pooled = live
.get(&node)
.map(|&(flash_box, _)| flash_box)
.or_else(|| free.pop());
let flash_box = if let Some(flash_box) = pooled
&& let Ok((mut placed, mut color)) = boxes.get_mut(flash_box)
{
placed.set_if_neq(rect);
*color = peak;
flash_box
} else {
commands
.spawn((
PaintFlashBox,
rect,
peak,
GlobalZIndex(i32::MAX - 2),
Pickable::IGNORE,
))
.id()
};
live.insert(node, (flash_box, 0.0));
}
}
fn flash_color(age: f32) -> Color {
Color::srgba(0.2, 0.9, 0.3, PEAK_ALPHA * (1.0 - age / FADE_SECS).max(0.0))
}
#[cfg(test)]
mod tests;