use std::sync::{Arc, Weak};
use teksilo_canvas::sync::{AtomicBool, AtomicU32, AtomicU64, Mutex, Ordering};
use teksilo_canvas::wake::{RedrawWaker, WakeFlag, WakeKind};
#[derive(Clone, Default)]
pub struct RepaintTrigger {
shared: Arc<TriggerShared>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct RepaintTriggerStats {
pub requests: u64,
pub wakes: u64,
pub wakes_coalesced: u64,
pub repaints: u64,
pub relayouts: u64,
pub pulls: u64,
pub attachments: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PullOutcome {
Unchanged,
Repaint,
Relayout,
}
#[derive(Default)]
struct TriggerShared {
nodes: Mutex<Vec<Weak<NodeWakeState>>>,
counters: Arc<TriggerCounters>,
}
#[derive(Default)]
pub(crate) struct TriggerCounters {
requests: AtomicU64,
wakes: AtomicU64,
wakes_coalesced: AtomicU64,
repaints: AtomicU64,
relayouts: AtomicU64,
pulls: AtomicU64,
attachments: AtomicU32,
}
#[derive(Clone, Copy)]
enum Request {
Repaint,
Relayout,
Pull,
}
impl RepaintTrigger {
pub fn new() -> Self {
Self::default()
}
pub fn request_repaint(&self) {
self.request(Request::Repaint);
}
pub fn request_relayout(&self) {
self.request(Request::Relayout);
}
pub fn request_pull(&self) {
self.request(Request::Pull);
}
pub fn is_attached(&self) -> bool {
self.shared.counters.attachments.load(Ordering::Relaxed) > 0
}
pub fn stats(&self) -> RepaintTriggerStats {
let c = &self.shared.counters;
RepaintTriggerStats {
requests: c.requests.load(Ordering::Relaxed),
wakes: c.wakes.load(Ordering::Relaxed),
wakes_coalesced: c.wakes_coalesced.load(Ordering::Relaxed),
repaints: c.repaints.load(Ordering::Relaxed),
relayouts: c.relayouts.load(Ordering::Relaxed),
pulls: c.pulls.load(Ordering::Relaxed),
attachments: c.attachments.load(Ordering::Relaxed),
}
}
pub fn ptr_eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.shared, &other.shared)
}
fn request(&self, request: Request) {
let counters = &self.shared.counters;
counters.requests.fetch_add(1, Ordering::Relaxed);
let live: Vec<Arc<NodeWakeState>> = {
let mut nodes = self
.shared
.nodes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
nodes.retain(|weak| {
weak.upgrade()
.is_some_and(|state| !state.detached.load(Ordering::Acquire))
});
nodes.iter().filter_map(Weak::upgrade).collect()
};
let mut wakers: Vec<(Arc<dyn RedrawWaker>, WakeKind)> = Vec::new();
let mut raised_any = false;
for state in &live {
let flag = match request {
Request::Repaint => &state.repaint,
Request::Relayout => &state.relayout,
Request::Pull => &state.pull,
};
if !flag.raise() {
continue;
}
raised_any = true;
let Some(waker) = state.waker() else {
continue;
};
let kind = match request {
Request::Repaint => WakeKind::Draw,
Request::Relayout | Request::Pull => WakeKind::Layout,
};
let data = Arc::as_ptr(&waker) as *const ();
match wakers
.iter_mut()
.find(|(other, _)| Arc::as_ptr(other) as *const () == data)
{
Some((_, other_kind)) => *other_kind = (*other_kind).max(kind),
None => wakers.push((waker, kind)),
}
}
for (waker, kind) in wakers {
waker.wake(kind);
counters.wakes.fetch_add(1, Ordering::Relaxed);
}
if !live.is_empty() && !raised_any {
counters.wakes_coalesced.fetch_add(1, Ordering::Relaxed);
}
}
pub(crate) fn detach_state(&self, state: &Arc<NodeWakeState>) {
self.shared
.nodes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.retain(|weak| !std::ptr::eq(weak.as_ptr(), Arc::as_ptr(state)));
if state.remove_sink(&self.shared.counters) {
self.shared
.counters
.attachments
.fetch_sub(1, Ordering::Relaxed);
}
}
pub(crate) fn attach_state(&self, state: &Arc<NodeWakeState>) {
state.add_sink(self.shared.counters.clone());
let mut nodes = self
.shared
.nodes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
nodes.retain(|weak| {
weak.upgrade()
.is_some_and(|other| !other.detached.load(Ordering::Acquire))
});
nodes.push(Arc::downgrade(state));
drop(nodes);
self.shared
.counters
.attachments
.fetch_add(1, Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn node_list_len(&self) -> usize {
self.shared
.nodes
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.len()
}
}
impl std::fmt::Debug for RepaintTrigger {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = self.stats();
write!(
f,
"RepaintTrigger(attachments {}, requests {}, wakes {})",
s.attachments, s.requests, s.wakes
)
}
}
pub(crate) struct NodeWakeState {
repaint: WakeFlag,
relayout: WakeFlag,
pull: WakeFlag,
detached: AtomicBool,
waker: Mutex<Option<Arc<dyn RedrawWaker>>>,
sinks: Mutex<Vec<Arc<TriggerCounters>>>,
}
impl NodeWakeState {
pub(crate) fn new(waker: Option<Arc<dyn RedrawWaker>>) -> Self {
Self {
repaint: WakeFlag::new(),
relayout: WakeFlag::new(),
pull: WakeFlag::new(),
detached: AtomicBool::new(false),
waker: Mutex::new(waker),
sinks: Mutex::new(Vec::new()),
}
}
fn waker(&self) -> Option<Arc<dyn RedrawWaker>> {
self.waker
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub(crate) fn set_waker(&self, waker: Option<Arc<dyn RedrawWaker>>) {
*self
.waker
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = waker.clone();
let Some(waker) = waker else {
return;
};
if self.relayout.is_raised() || self.pull.is_raised() {
waker.wake(WakeKind::Layout);
} else if self.repaint.is_raised() {
waker.wake(WakeKind::Draw);
}
}
fn add_sink(&self, counters: Arc<TriggerCounters>) {
self.sinks
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push(counters);
}
fn remove_sink(&self, counters: &Arc<TriggerCounters>) -> bool {
let mut sinks = self
.sinks
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let before = sinks.len();
sinks.retain(|sink| !Arc::ptr_eq(sink, counters));
sinks.len() != before
}
fn for_each_sink(&self, f: impl Fn(&TriggerCounters)) {
for sink in self
.sinks
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.iter()
{
f(sink);
}
}
pub(crate) fn consume_repaint(&self) {
if self.repaint.take() {
self.for_each_sink(|c| {
c.repaints.fetch_add(1, Ordering::Relaxed);
});
}
}
pub(crate) fn take_relayout(&self) -> bool {
let taken = self.relayout.take();
if taken {
self.for_each_sink(|c| {
c.relayouts.fetch_add(1, Ordering::Relaxed);
});
}
taken
}
pub(crate) fn keep_relayout_for_activation(&self) {
self.relayout.park();
}
pub(crate) fn take_pull(&self) -> bool {
let taken = self.pull.take();
if taken {
self.for_each_sink(|c| {
c.pulls.fetch_add(1, Ordering::Relaxed);
});
}
taken
}
pub(crate) fn repaint_pending(&self) -> bool {
self.repaint.peek()
}
pub(crate) fn state_pending(&self) -> bool {
self.relayout.is_raised() || self.pull.is_raised()
}
pub(crate) fn detach(&self) {
if self.detached.swap(true, Ordering::AcqRel) {
return;
}
let waker = self
.waker
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
drop(waker);
self.for_each_sink(|c| {
c.attachments.fetch_sub(1, Ordering::Relaxed);
});
}
}
#[cfg(all(test, not(teksilo_loom)))]
mod tests;
#[cfg(all(test, teksilo_loom))]
mod loom_tests;