use cranpose_core::NodeId;
use std::collections::HashSet;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Mutex;
#[cfg(not(any(test, feature = "test-helpers")))]
use std::sync::OnceLock;
#[cfg(test)]
use std::sync::OnceLock;
struct RenderState {
layout_repasses: Mutex<LayoutRepassManager>,
draw_repasses: Mutex<DrawRepassManager>,
render_invalidated: AtomicBool,
pointer_invalidated: AtomicBool,
focus_invalidated: AtomicBool,
layout_invalidated: AtomicBool,
density_bits: AtomicU32,
}
impl RenderState {
fn new() -> Self {
Self {
layout_repasses: Mutex::new(LayoutRepassManager::new()),
draw_repasses: Mutex::new(DrawRepassManager::new()),
render_invalidated: AtomicBool::new(false),
pointer_invalidated: AtomicBool::new(false),
focus_invalidated: AtomicBool::new(false),
layout_invalidated: AtomicBool::new(false),
density_bits: AtomicU32::new(f32::to_bits(1.0)),
}
}
}
#[cfg(not(any(test, feature = "test-helpers")))]
fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
static STATE: OnceLock<RenderState> = OnceLock::new();
f(STATE.get_or_init(RenderState::new))
}
#[cfg(any(test, feature = "test-helpers"))]
fn with_render_state<R>(f: impl FnOnce(&RenderState) -> R) -> R {
std::thread_local! {
static STATE: RenderState = RenderState::new();
}
STATE.with(f)
}
struct LayoutRepassManager {
dirty_nodes: HashSet<NodeId>,
}
impl LayoutRepassManager {
fn new() -> Self {
Self {
dirty_nodes: HashSet::new(),
}
}
fn schedule_repass(&mut self, node_id: NodeId) {
self.dirty_nodes.insert(node_id);
}
fn has_pending_repass(&self) -> bool {
!self.dirty_nodes.is_empty()
}
fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
self.dirty_nodes.drain().collect()
}
}
struct DrawRepassManager {
dirty_nodes: HashSet<NodeId>,
}
impl DrawRepassManager {
fn new() -> Self {
Self {
dirty_nodes: HashSet::new(),
}
}
fn schedule_repass(&mut self, node_id: NodeId) {
self.dirty_nodes.insert(node_id);
}
fn has_pending_repass(&self) -> bool {
!self.dirty_nodes.is_empty()
}
fn take_dirty_nodes(&mut self) -> Vec<NodeId> {
self.dirty_nodes.drain().collect()
}
}
pub fn schedule_layout_repass(node_id: NodeId) {
with_render_state(|state| {
state
.layout_repasses
.lock()
.expect("layout repass manager poisoned")
.schedule_repass(node_id);
state.layout_invalidated.store(true, Ordering::Relaxed);
});
request_render_invalidation();
}
pub fn schedule_draw_repass(node_id: NodeId) {
with_render_state(|state| {
state
.draw_repasses
.lock()
.expect("draw repass manager poisoned")
.schedule_repass(node_id);
});
}
pub fn has_pending_draw_repasses() -> bool {
with_render_state(|state| {
state
.draw_repasses
.lock()
.expect("draw repass manager poisoned")
.has_pending_repass()
})
}
pub fn take_draw_repass_nodes() -> Vec<NodeId> {
with_render_state(|state| {
state
.draw_repasses
.lock()
.expect("draw repass manager poisoned")
.take_dirty_nodes()
})
}
pub fn has_pending_layout_repasses() -> bool {
with_render_state(|state| {
state
.layout_repasses
.lock()
.expect("layout repass manager poisoned")
.has_pending_repass()
})
}
pub fn take_layout_repass_nodes() -> Vec<NodeId> {
with_render_state(|state| {
state
.layout_repasses
.lock()
.expect("layout repass manager poisoned")
.take_dirty_nodes()
})
}
pub fn current_density() -> f32 {
with_render_state(|state| f32::from_bits(state.density_bits.load(Ordering::Relaxed)))
}
pub fn set_density(density: f32) {
let normalized = if density.is_finite() && density > 0.0 {
density
} else {
1.0
};
let new_bits = normalized.to_bits();
with_render_state(|state| {
let old_bits = state.density_bits.swap(new_bits, Ordering::Relaxed);
if old_bits != new_bits {
state.layout_invalidated.store(true, Ordering::Relaxed);
}
});
}
pub fn request_render_invalidation() {
with_render_state(|state| state.render_invalidated.store(true, Ordering::Relaxed));
}
pub fn take_render_invalidation() -> bool {
with_render_state(|state| state.render_invalidated.swap(false, Ordering::Relaxed))
}
pub fn peek_render_invalidation() -> bool {
with_render_state(|state| state.render_invalidated.load(Ordering::Relaxed))
}
pub fn request_pointer_invalidation() {
with_render_state(|state| state.pointer_invalidated.store(true, Ordering::Relaxed));
}
pub fn take_pointer_invalidation() -> bool {
with_render_state(|state| state.pointer_invalidated.swap(false, Ordering::Relaxed))
}
pub fn peek_pointer_invalidation() -> bool {
with_render_state(|state| state.pointer_invalidated.load(Ordering::Relaxed))
}
pub fn request_focus_invalidation() {
with_render_state(|state| state.focus_invalidated.store(true, Ordering::Relaxed));
}
pub fn take_focus_invalidation() -> bool {
with_render_state(|state| state.focus_invalidated.swap(false, Ordering::Relaxed))
}
pub fn peek_focus_invalidation() -> bool {
with_render_state(|state| state.focus_invalidated.load(Ordering::Relaxed))
}
pub fn request_layout_invalidation() {
with_render_state(|state| state.layout_invalidated.store(true, Ordering::Relaxed));
}
pub fn take_layout_invalidation() -> bool {
with_render_state(|state| state.layout_invalidated.swap(false, Ordering::Relaxed))
}
pub fn peek_layout_invalidation() -> bool {
with_render_state(|state| state.layout_invalidated.load(Ordering::Relaxed))
}
#[cfg(any(test, feature = "test-helpers"))]
#[doc(hidden)]
pub fn reset_render_state_for_tests() {
let _ = take_draw_repass_nodes();
let _ = take_layout_repass_nodes();
let _ = take_render_invalidation();
let _ = take_pointer_invalidation();
let _ = take_focus_invalidation();
let _ = take_layout_invalidation();
set_density(1.0);
let _ = take_layout_invalidation();
}
#[cfg(test)]
pub(crate) fn render_state_test_guard() -> std::sync::MutexGuard<'static, ()> {
static TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
match TEST_LOCK.get_or_init(|| Mutex::new(())).lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{mpsc, Arc};
#[test]
fn invalidation_flags_are_shared_across_threads() {
let state = Arc::new(RenderState::new());
let (tx, rx) = mpsc::channel();
let worker_state = Arc::clone(&state);
let handle = std::thread::spawn(move || {
worker_state
.render_invalidated
.store(true, Ordering::Relaxed);
worker_state
.pointer_invalidated
.store(true, Ordering::Relaxed);
worker_state
.focus_invalidated
.store(true, Ordering::Relaxed);
worker_state
.layout_invalidated
.store(true, Ordering::Relaxed);
worker_state
.density_bits
.store(f32::to_bits(2.0), Ordering::Relaxed);
tx.send(()).expect("signal invalidation setup");
f32::from_bits(worker_state.density_bits.load(Ordering::Relaxed))
});
rx.recv().expect("wait for worker invalidation setup");
assert!(state.render_invalidated.load(Ordering::Relaxed));
assert!(state.pointer_invalidated.load(Ordering::Relaxed));
assert!(state.focus_invalidated.load(Ordering::Relaxed));
assert!(state.layout_invalidated.load(Ordering::Relaxed));
assert_eq!(
f32::from_bits(state.density_bits.load(Ordering::Relaxed)),
2.0
);
assert!(state.render_invalidated.swap(false, Ordering::Relaxed));
assert!(state.pointer_invalidated.swap(false, Ordering::Relaxed));
assert!(state.focus_invalidated.swap(false, Ordering::Relaxed));
assert!(state.layout_invalidated.swap(false, Ordering::Relaxed));
let density = handle.join().expect("worker invalidation snapshot");
assert_eq!(density, 2.0);
assert!(!state.render_invalidated.load(Ordering::Relaxed));
assert!(!state.pointer_invalidated.load(Ordering::Relaxed));
assert!(!state.focus_invalidated.load(Ordering::Relaxed));
assert!(!state.layout_invalidated.load(Ordering::Relaxed));
}
}