#![allow(non_snake_case)]
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use crate::composable;
use crate::modifier::Modifier;
use cranpose_core::{
mutableStateOf, remember, staticCompositionLocalOf, CompositionLocalProvider, MutableState,
SideEffect, StaticCompositionLocal,
};
use cranpose_ui_graphics::{Point, Rect};
use super::box_widget::{Box, BoxSpec};
#[derive(Clone)]
struct PopupEntry {
id: u64,
position: Point,
content: Rc<dyn Fn()>,
}
struct PopupRegistryState {
entries: RefCell<Vec<PopupEntry>>,
next_id: Cell<u64>,
revision: Option<MutableState<u64>>,
}
#[derive(Clone)]
pub struct PopupRegistry {
inner: Rc<PopupRegistryState>,
}
impl PopupRegistry {
fn hosted() -> Self {
Self {
inner: Rc::new(PopupRegistryState {
entries: RefCell::new(Vec::new()),
next_id: Cell::new(0),
revision: Some(mutableStateOf(0u64)),
}),
}
}
fn detached() -> Self {
Self {
inner: Rc::new(PopupRegistryState {
entries: RefCell::new(Vec::new()),
next_id: Cell::new(0),
revision: None,
}),
}
}
fn allocate_id(&self) -> u64 {
let id = self.inner.next_id.get();
self.inner.next_id.set(id.wrapping_add(1));
id
}
fn bump(&self) {
if let Some(revision) = self.inner.revision.as_ref() {
revision.update(|value| *value = value.wrapping_add(1));
}
}
fn upsert(&self, id: u64, position: Point, content: Rc<dyn Fn()>) {
let mut entries = self.inner.entries.borrow_mut();
if let Some(existing) = entries.iter_mut().find(|entry| entry.id == id) {
let moved = existing.position != position;
existing.position = position;
existing.content = content;
drop(entries);
if moved {
self.bump();
}
} else {
entries.push(PopupEntry {
id,
position,
content,
});
drop(entries);
self.bump();
}
}
fn remove(&self, id: u64) {
let mut entries = self.inner.entries.borrow_mut();
let before = entries.len();
entries.retain(|entry| entry.id != id);
let changed = entries.len() != before;
drop(entries);
if changed {
self.bump();
}
}
fn subscribe(&self) {
if let Some(revision) = self.inner.revision.as_ref() {
let _ = revision.value();
}
}
fn snapshot(&self) -> Vec<PopupEntry> {
self.inner.entries.borrow().clone()
}
}
fn local_popup_registry() -> StaticCompositionLocal<PopupRegistry> {
thread_local! {
static LOCAL: RefCell<Option<StaticCompositionLocal<PopupRegistry>>> =
const { RefCell::new(None) };
}
LOCAL.with(|cell| {
cell.borrow_mut()
.get_or_insert_with(|| staticCompositionLocalOf(PopupRegistry::detached))
.clone()
})
}
#[composable]
pub fn PopupHost<F>(content: F)
where
F: FnMut() + 'static,
{
let registry = remember(PopupRegistry::hosted).with(PopupRegistry::clone);
Box(
Modifier::empty().fill_max_size(),
BoxSpec::default(),
move || {
let registry = registry.clone();
CompositionLocalProvider([local_popup_registry().provides(registry.clone())], || {
content();
registry.subscribe();
for entry in registry.snapshot() {
let content = entry.content;
Box(
Modifier::empty().absolute_offset(entry.position.x, entry.position.y),
BoxSpec::default(),
move || content(),
);
}
});
},
);
}
#[composable]
pub fn Popup<F>(anchor: Rect, offset: Point, content: F)
where
F: Fn() + 'static,
{
let registry = local_popup_registry().current();
let id = remember(|| registry.allocate_id()).with(|id| *id);
let content: Rc<dyn Fn()> = remember(move || {
let content: Rc<dyn Fn()> = Rc::new(content);
content
})
.with(Rc::clone);
let position = Point {
x: anchor.x + offset.x,
y: anchor.y + offset.y,
};
let sync_registry = registry.clone();
let sync_content = content.clone();
SideEffect(move || sync_registry.upsert(id, position, sync_content.clone()));
let dispose_registry = registry;
cranpose_core::DisposableEffect!((), move |scope| {
let dispose_registry = dispose_registry.clone();
scope.on_dispose(move || dispose_registry.remove(id))
});
}