mod live_image;
mod repaint_trigger;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use teksilo_canvas::ImageGeometry;
use teksilo_canvas::live_image::{LiveImageConsumer, LiveImageId, LiveImageSource, LiveImageStats};
use teksilo_canvas::wake::RedrawWaker;
pub(crate) use live_image::AttachmentEntry;
pub use live_image::{LiveImageAttachment, LiveImageSignals};
pub(crate) use repaint_trigger::NodeWakeState;
pub use repaint_trigger::{PullOutcome, RepaintTrigger, RepaintTriggerStats};
use crate::widget_id::WidgetId;
use crate::widget_tree::WidgetTree;
pub(crate) type PullHook = Box<dyn FnMut() -> PullOutcome>;
#[derive(Default)]
pub(crate) struct OffThreadRegistry {
waker: Option<Arc<dyn RedrawWaker>>,
triggers: HashMap<WidgetId, TriggerNode>,
live: HashMap<WidgetId, Vec<Rc<AttachmentEntry>>>,
}
struct TriggerNode {
state: Arc<NodeWakeState>,
triggers: Vec<RepaintTrigger>,
hook: Option<PullHook>,
previous: Option<Vec<RepaintTrigger>>,
}
impl TriggerNode {
fn new(waker: Option<Arc<dyn RedrawWaker>>) -> Self {
Self {
state: Arc::new(NodeWakeState::new(waker)),
triggers: Vec::new(),
hook: None,
previous: None,
}
}
}
impl OffThreadRegistry {
pub(crate) fn waker(&self) -> Option<&Arc<dyn RedrawWaker>> {
self.waker.as_ref()
}
pub(crate) fn set_waker(&mut self, waker: Option<Arc<dyn RedrawWaker>>) {
for node in self.triggers.values() {
node.state.set_waker(waker.clone());
}
for entry in self.live.values().flatten() {
entry.consumer.set_waker(waker.clone());
}
self.waker = waker;
}
pub(crate) fn cancel_by_widget(&mut self, id: WidgetId) {
self.release_triggers(id);
self.detach_live(id);
}
fn release_triggers(&mut self, id: WidgetId) {
if let Some(node) = self.triggers.remove(&id) {
node.state.detach();
}
}
pub(crate) fn begin_rebuild(&mut self, id: WidgetId) {
if let Some(node) = self.triggers.get_mut(&id) {
node.previous = Some(std::mem::take(&mut node.triggers));
node.hook = None;
}
self.detach_live(id);
}
fn detach_live(&mut self, id: WidgetId) {
for entry in self.live.remove(&id).unwrap_or_default() {
entry.consumer.detach();
}
}
#[cfg(test)]
pub(crate) fn trigger_node_count(&self) -> usize {
self.triggers.len()
}
}
impl Drop for OffThreadRegistry {
fn drop(&mut self) {
for node in self.triggers.values() {
node.state.detach();
}
for entry in self.live.values().flatten() {
entry.consumer.detach();
}
}
}
impl WidgetTree {
pub(crate) fn attach_repaint_trigger(&mut self, id: WidgetId, trigger: &RepaintTrigger) {
let waker = self.off_thread.waker.clone();
let node = self
.off_thread
.triggers
.entry(id)
.or_insert_with(|| TriggerNode::new(waker));
if node.triggers.iter().any(|t| t.ptr_eq(trigger)) {
return;
}
let state = node.state.clone();
node.triggers.push(trigger.clone());
if let Some(arena_node) = self.arena.get_mut(id) {
debug_assert!(
arena_node.dirty.needs_paint,
"a widget attaching in build() is about to paint, which is the read"
);
arena_node.repaint_wake = Some(state.clone());
}
if let Some(previous) = node.previous.as_mut()
&& let Some(at) = previous.iter().position(|t| t.ptr_eq(trigger))
{
previous.remove(at);
return;
}
trigger.attach_state(&state);
}
pub(crate) fn finish_off_thread_rebuild(&mut self, id: WidgetId) {
let Some(node) = self.off_thread.triggers.get_mut(&id) else {
return;
};
for stale in node.previous.take().unwrap_or_default() {
stale.detach_state(&node.state);
}
if node.triggers.is_empty() && node.hook.is_none() {
self.off_thread.release_triggers(id);
if let Some(arena_node) = self.arena.get_mut(id) {
arena_node.repaint_wake = None;
}
}
}
pub(crate) fn set_trigger_pull_hook(&mut self, id: WidgetId, hook: PullHook) {
let waker = self.off_thread.waker.clone();
let node = self
.off_thread
.triggers
.entry(id)
.or_insert_with(|| TriggerNode::new(waker));
let state = node.state.clone();
node.hook = Some(hook);
if let Some(arena_node) = self.arena.get_mut(id) {
arena_node.repaint_wake = Some(state);
}
}
pub fn repaint_trigger_count(&self) -> usize {
self.off_thread
.triggers
.values()
.filter(|node| !node.triggers.is_empty())
.count()
}
pub(crate) fn attach_live_image(
&mut self,
id: WidgetId,
source: &LiveImageSource,
signals: &LiveImageSignals,
) -> LiveImageAttachment {
let consumer = source.attach(self.off_thread.waker.clone());
let meta = consumer.layout_meta();
signals.frame_size.set_if_changed(meta.size);
signals.status.set_if_changed(meta.status);
let entry = Rc::new(AttachmentEntry::new(consumer, signals.clone(), id));
self.off_thread
.live
.entry(id)
.or_default()
.push(entry.clone());
LiveImageAttachment { entry }
}
pub fn live_image_attachment_count(&self) -> usize {
self.off_thread.live.values().map(Vec::len).sum()
}
pub fn live_image_consumer(&self, id: WidgetId) -> Option<LiveImageConsumer> {
self.first_live(id).map(|e| e.consumer.clone())
}
pub fn live_image_stats(&self, id: WidgetId) -> Option<LiveImageStats> {
self.first_live(id).map(|e| e.consumer.stats())
}
pub fn live_image_geometry(&self, id: WidgetId) -> Option<teksilo_canvas::ImageGeometry> {
self.first_live(id).and_then(|e| e.geometry())
}
pub fn live_image_attachments(
&self,
) -> impl Iterator<Item = (WidgetId, LiveImageConsumer)> + '_ {
self.off_thread
.live
.iter()
.flat_map(|(&id, entries)| entries.iter().map(move |e| (id, e.consumer.clone())))
}
fn first_live(&self, id: WidgetId) -> Option<&Rc<AttachmentEntry>> {
self.off_thread
.live
.get(&id)
.and_then(|entries| entries.first())
}
pub(crate) fn poll_off_thread(&mut self) {
self.poll_triggers();
self.poll_live_images();
}
fn poll_live_images(&mut self) {
if self.off_thread.live.is_empty() {
return;
}
let mut orphans = Vec::new();
let mut resnap = Vec::new();
let mut by_source: Vec<(LiveImageId, Vec<Rc<AttachmentEntry>>)> = Vec::new();
for (&id, entries) in &self.off_thread.live {
if self.arena.get(id).is_none() {
orphans.push(id);
continue;
}
if self.arena.is_active(id)
&& entries.iter().any(|entry| {
entry.snapped_under().is_some_and(|then| {
let now = self.arena.effective_transform(id);
now != then
&& (ImageGeometry::snaps_under(then) || ImageGeometry::snaps_under(now))
})
})
{
resnap.push(id);
}
for entry in entries {
let source = entry.consumer.source().id();
match by_source.iter_mut().find(|(other, _)| *other == source) {
Some((_, group)) => group.push(entry.clone()),
None => by_source.push((source, vec![entry.clone()])),
}
}
}
for id in orphans {
self.off_thread.cancel_by_widget(id);
}
for id in resnap {
self.arena.mark_needs_layout(id);
self.arena.mark_ancestors_need_layout(id);
}
let mut writes = Vec::new();
for (_, group) in by_source {
for entry in &group {
let _ = entry.consumer.take_geometry();
}
let meta = group[0].consumer.source().meta();
for entry in group {
entry.consumer.record_layout_meta(meta);
writes.push((entry.signals.clone(), meta));
}
}
for (signals, meta) in writes {
signals.frame_size.set_if_changed(meta.size);
signals.status.set_if_changed(meta.status);
}
}
fn poll_triggers(&mut self) {
if self.off_thread.triggers.is_empty() {
return;
}
let mut orphans = Vec::new();
let mut relayout = Vec::new();
let mut repaint = Vec::new();
let mut pulled = Vec::new();
for (&id, node) in &mut self.off_thread.triggers {
let alive = self.arena.get(id).is_some_and(|arena_node| {
arena_node
.repaint_wake
.as_ref()
.is_some_and(|state| Arc::ptr_eq(state, &node.state))
});
if !alive {
orphans.push(id);
continue;
}
if node.state.take_pull() {
let outcome = match node.hook.as_mut() {
Some(hook) => hook(),
None => PullOutcome::Repaint,
};
pulled.push((id, outcome));
}
if !self.arena.is_active(id) {
continue;
}
if node.state.take_relayout() {
relayout.push(id);
}
let cached = self
.arena
.get(id)
.is_some_and(|arena_node| arena_node.cached_paint.is_some());
if cached && node.state.repaint_pending() {
repaint.push(id);
}
}
for id in orphans {
self.off_thread.cancel_by_widget(id);
}
for (id, outcome) in pulled {
self.apply_pull_outcome(id, outcome);
}
for id in relayout {
self.arena.mark_needs_layout(id);
self.arena.mark_ancestors_need_layout(id);
}
for id in repaint {
self.arena.mark_needs_paint(id);
}
}
fn apply_pull_outcome(&mut self, id: WidgetId, outcome: PullOutcome) {
let active = self.arena.is_active(id);
let shown = self.off_thread_widget_shown(id);
match outcome {
PullOutcome::Unchanged => {}
PullOutcome::Repaint if shown => self.arena.mark_needs_paint(id),
PullOutcome::Repaint => {
if let Some(node) = self.arena.get_mut(id) {
node.cached_paint = None;
node.cached_post_paint = None;
}
}
PullOutcome::Relayout if active => {
self.arena.mark_needs_layout(id);
self.arena.mark_ancestors_need_layout(id);
}
PullOutcome::Relayout => {
if let Some(node) = self.off_thread.triggers.get(&id) {
node.state.keep_relayout_for_activation();
}
}
}
}
fn off_thread_widget_shown(&self, id: WidgetId) -> bool {
self.arena.is_active(id)
&& crate::motion_visibility::painted_this_frame(&self.arena, id, self.paint_epoch)
&& self.arena.get(id).is_some_and(|node| {
node.opacity_prop
.as_ref()
.is_none_or(|opacity| opacity.get() >= 1.0 / 512.0)
})
}
#[doc(hidden)]
pub fn off_thread_needs_frame(&self) -> bool {
self.off_thread
.triggers
.iter()
.any(|(&id, node)| node.state.state_pending() && self.off_thread_widget_shown(id))
|| self.off_thread.live.iter().any(|(&id, entries)| {
entries.iter().any(|e| e.consumer.geometry_pending())
&& self.off_thread_widget_shown(id)
})
}
}