use std::cell::RefCell;
use std::rc::{Rc, Weak};
use crate::base::{Point, Rect, Size};
use crate::gfx::Bitmap;
use crate::reactive::{request_frame, Scope};
use crate::render::{Blend, Cell, CellShader, ColorTransform, Layer, Surface};
use crate::ui::{StyledCanvas, SurfaceCanvas, UiTree, View};
pub(crate) const ROOT_LAYER_ID: u64 = 0;
type DrawFn = Box<dyn FnMut(&mut dyn StyledCanvas, Rect)>;
pub(crate) enum OverlayContent {
Root,
Manual,
Draw {
paint: Option<DrawFn>,
needs_paint: bool,
},
Tree {
tree: UiTree,
modal: bool,
on_outside: Option<Box<dyn FnMut()>>,
},
}
pub(crate) struct OverlayMeta {
pub id: u64,
pub content: OverlayContent,
}
pub(crate) struct ImageEntry {
pub id: u64,
pub rect: Rect,
pub bitmap: Bitmap,
pub version: u64,
pub dirty: bool,
}
#[derive(Default)]
pub(crate) struct OverlayStore {
pub layers: Vec<Layer>,
pub meta: Vec<OverlayMeta>,
pub images: Vec<ImageEntry>,
pub retired_images: Vec<u64>,
next_id: u64,
}
impl OverlayStore {
pub fn index_of(&self, id: u64) -> Option<usize> {
self.meta.iter().position(|m| m.id == id)
}
fn damage_root_under(&mut self, bounds: Rect) {
if let Some(root) = self.index_of(ROOT_LAYER_ID) {
let clip = self.layers[root].bounds();
let rect = bounds.intersect(clip);
if !rect.is_empty() {
self.layers[root].surface_mut().add_damage(rect);
}
}
}
}
#[derive(Clone)]
pub struct Overlays {
store: Rc<RefCell<OverlayStore>>,
}
impl Default for Overlays {
fn default() -> Self {
Overlays::new()
}
}
impl Overlays {
pub fn new() -> Overlays {
Overlays {
store: Rc::new(RefCell::new(OverlayStore::default())),
}
}
fn create(&self, z: i32, bounds: Rect, content: OverlayContent) -> LayerHandle {
let mut store = self.store.borrow_mut();
store.next_id += 1;
let id = store.next_id;
let surface = Surface::new(bounds.size(), Cell::EMPTY);
let mut layer = Layer::new(surface, bounds.origin(), z);
layer.surface_mut().damage_all();
store.layers.push(layer);
store.meta.push(OverlayMeta { id, content });
drop(store);
request_frame();
LayerHandle {
store: Rc::downgrade(&self.store),
id,
}
}
pub fn layer(&self, z: i32, bounds: Rect) -> LayerHandle {
self.create(z, bounds, OverlayContent::Manual)
}
pub fn layer_draw(
&self,
z: i32,
bounds: Rect,
paint: impl FnMut(&mut dyn StyledCanvas, Rect) + 'static,
) -> LayerHandle {
self.create(
z,
bounds,
OverlayContent::Draw {
paint: Some(Box::new(paint)),
needs_paint: true,
},
)
}
pub fn layer_tree(
&self,
z: i32,
bounds: Rect,
modal: bool,
cx: Scope,
view: View,
) -> LayerHandle {
let mut tree = UiTree::new(bounds.size());
tree.mount(cx, view);
if modal {
tree.focus_init();
}
self.create(
z,
bounds,
OverlayContent::Tree {
tree,
modal,
on_outside: None,
},
)
}
pub fn on_outside_press(&self, handle: &LayerHandle, f: impl FnMut() + 'static) {
let mut store = self.store.borrow_mut();
if let Some(i) = store.index_of(handle.id) {
if let OverlayContent::Tree { on_outside, .. } = &mut store.meta[i].content {
*on_outside = Some(Box::new(f));
}
}
}
pub fn top_z(&self) -> i32 {
self.store
.try_borrow()
.map(|s| s.layers.iter().map(|l| l.z()).max().unwrap_or(0))
.unwrap_or(0)
}
pub fn image(&self, rect: Rect, bitmap: Bitmap) -> ImageHandle {
let mut store = self.store.borrow_mut();
store.next_id += 1;
let id = store.next_id;
store.images.push(ImageEntry {
id,
rect,
bitmap,
version: 1,
dirty: true,
});
drop(store);
request_frame();
ImageHandle {
store: Rc::downgrade(&self.store),
id,
}
}
pub(crate) fn store(&self) -> &Rc<RefCell<OverlayStore>> {
&self.store
}
pub(crate) fn ensure_root(&self, size: Size) {
let mut store = self.store.borrow_mut();
match store.index_of(ROOT_LAYER_ID) {
Some(i) => {
store.layers[i].surface_mut().resize(size, Cell::EMPTY);
store.layers[i].surface_mut().damage_all();
}
None => {
let mut layer = Layer::new(Surface::new(size, Cell::EMPTY), Point::ZERO, 0);
layer.surface_mut().damage_all();
store.layers.insert(0, layer);
store.meta.insert(
0,
OverlayMeta {
id: ROOT_LAYER_ID,
content: OverlayContent::Root,
},
);
}
}
}
pub(crate) fn has_pending_work(&self) -> bool {
let store = self.store.borrow();
store.images.iter().any(|i| i.dirty)
|| store.meta.iter().any(|m| match &m.content {
OverlayContent::Draw { needs_paint, .. } => *needs_paint,
OverlayContent::Tree { tree, .. } => tree.has_pending_work(),
_ => false,
})
}
pub(crate) fn dispatch(&self, event: &crate::ui::UiEvent) -> Option<bool> {
use crate::ui::UiEvent;
let mut targets: Vec<(UiTree, bool, Rect, i32, u64)> = {
let store = self.store.borrow();
store
.meta
.iter()
.zip(&store.layers)
.filter(|(_, l)| l.visible())
.filter_map(|(m, l)| match &m.content {
OverlayContent::Tree { tree, modal, .. } => {
Some((tree.handle(), *modal, l.bounds(), l.z(), m.id))
}
_ => None,
})
.collect()
};
targets.sort_by_key(|(_, _, _, z, _)| std::cmp::Reverse(*z));
let mut fell_through_press = false;
for (tree, modal, bounds, _, id) in targets.iter() {
let mut tree = tree.handle();
let modal = *modal;
let bounds = *bounds;
match event {
UiEvent::Mouse(m) => {
if modal || bounds.contains(m.pos) {
if modal
&& !bounds.contains(m.pos)
&& matches!(m.kind, crate::ui::MouseKind::Down(_))
{
self.fire_outside_press(*id);
return Some(true);
}
let mut local = *m;
local.pos = Point::new(m.pos.x - bounds.x, m.pos.y - bounds.y);
let consumed = tree.dispatch(&UiEvent::Mouse(local));
if modal {
return Some(true); }
return Some(consumed);
}
if matches!(m.kind, crate::ui::MouseKind::Down(_)) {
fell_through_press = true;
}
}
UiEvent::Key(_) | UiEvent::Paste(_) => {
if modal {
return Some(tree.dispatch(event));
}
if tree.focused().is_some() {
return Some(tree.dispatch(event));
}
}
_ => {}
}
}
if fell_through_press {
for (tree, modal, _, _, _) in targets.iter() {
if !modal {
tree.handle().set_focus(None);
}
}
}
None
}
fn fire_outside_press(&self, id: u64) {
let taken = {
let mut store = self.store.borrow_mut();
store
.index_of(id)
.and_then(|i| match &mut store.meta[i].content {
OverlayContent::Tree { on_outside, .. } => on_outside.take(),
_ => None,
})
};
let Some(mut f) = taken else { return };
f();
let mut store = self.store.borrow_mut();
if let Some(i) = store.index_of(id) {
if let OverlayContent::Tree { on_outside, .. } = &mut store.meta[i].content {
*on_outside = Some(f);
}
}
}
pub(crate) fn take_collapse_notices(&self) -> Vec<String> {
let trees: Vec<UiTree> = {
let store = self.store.borrow();
store
.meta
.iter()
.filter_map(|m| match &m.content {
OverlayContent::Tree { tree, .. } => Some(tree.handle()),
_ => None,
})
.collect()
};
let mut out = Vec::new();
for mut tree in trees {
out.extend(tree.take_collapse_notices());
}
out
}
pub(crate) fn layout_all(&self) {
let trees: Vec<UiTree> = {
let store = self.store.borrow();
store
.meta
.iter()
.filter_map(|m| match &m.content {
OverlayContent::Tree { tree, .. } => Some(tree.handle()),
_ => None,
})
.collect()
};
for mut tree in trees {
tree.layout();
}
}
pub(crate) fn draw_all(&self) {
let ids: Vec<u64> = {
let store = self.store.borrow();
store
.meta
.iter()
.filter(|m| m.id != ROOT_LAYER_ID)
.map(|m| m.id)
.collect()
};
for id in ids {
enum Job {
Tree(UiTree),
Draw(DrawFn),
Skip,
}
let (mut surface, job, bounds) = {
let mut store = self.store.borrow_mut();
let Some(i) = store.index_of(id) else {
continue;
};
if !store.layers[i].visible() {
continue; }
let bounds = store.layers[i].bounds();
let job = match &mut store.meta[i].content {
OverlayContent::Tree { tree, .. } => {
if tree.has_pending_work() || tree.needs_layout() {
Job::Tree(tree.handle())
} else {
Job::Tree(tree.handle())
}
}
OverlayContent::Draw { paint, needs_paint } => {
if *needs_paint {
*needs_paint = false;
match paint.take() {
Some(p) => Job::Draw(p),
None => Job::Skip,
}
} else {
Job::Skip
}
}
_ => Job::Skip,
};
if matches!(job, Job::Skip) {
continue;
}
let surface = std::mem::replace(
store.layers[i].surface_mut(),
Surface::new(Size::ZERO, Cell::EMPTY),
);
(surface, job, bounds)
};
match job {
Job::Tree(mut tree) => {
tree.layout();
let damage = tree.take_damage();
if !damage.is_empty() {
for &rect in &damage {
surface.fill_rect(rect, Cell::EMPTY);
}
let mut canvas = SurfaceCanvas::new(&mut surface);
tree.draw_damaged(&mut canvas, &damage);
}
let mut store = self.store.borrow_mut();
if let Some(i) = store.index_of(id) {
*store.layers[i].surface_mut() = surface;
}
}
Job::Draw(mut paint) => {
surface.clear(Cell::EMPTY);
{
let mut canvas = SurfaceCanvas::new(&mut surface);
let _guard = crate::reactive::enter_draw_phase();
paint(&mut canvas, Rect::from_size(bounds.size()));
}
let mut store = self.store.borrow_mut();
if let Some(i) = store.index_of(id) {
*store.layers[i].surface_mut() = surface;
if let OverlayContent::Draw { paint: slot, .. } = &mut store.meta[i].content
{
*slot = Some(paint);
}
}
}
Job::Skip => {}
}
}
}
}
#[derive(Clone)]
pub struct LayerHandle {
store: Weak<RefCell<OverlayStore>>,
id: u64,
}
impl LayerHandle {
fn with_layer<R>(&self, f: impl FnOnce(&mut Layer) -> R) -> Option<R> {
let store = self.store.upgrade()?;
let mut store = match store.try_borrow_mut() {
Ok(s) => s,
Err(_) => {
if cfg!(debug_assertions) {
panic!(
"abstracttui overlays: LayerHandle mutation while the overlay store \
is busy — layer ops are forbidden inside draw closures (draw is pure; \
mutate from effects/handlers instead)"
);
}
return None;
}
};
let i = store.index_of(self.id)?;
let r = f(&mut store.layers[i]);
drop(store);
request_frame();
Some(r)
}
pub fn set_offset(&self, offset: Point) {
self.with_layer(|l| l.set_origin(offset));
}
pub fn set_opacity(&self, opacity: f32) {
self.with_layer(|l| l.set_opacity(opacity));
}
pub fn set_visible(&self, visible: bool) {
self.with_layer(|l| l.set_visible(visible));
}
pub fn set_blend(&self, blend: Blend) {
self.with_layer(|l| l.set_blend(blend));
}
pub fn set_color_transform(&self, transform: ColorTransform) {
self.with_layer(|l| l.set_color_transform(transform));
}
pub fn set_shader(&self, shader: Option<Box<dyn CellShader>>) {
self.with_layer(|l| l.set_shader(shader));
}
pub fn set_shader_t(&self, t: f32) {
self.with_layer(|l| l.set_shader_t(t));
}
pub fn with_surface<R>(&self, f: impl FnOnce(&mut Surface) -> R) -> Option<R> {
self.with_layer(|l| f(l.surface_mut()))
}
pub fn damage(&self) {
let Some(store) = self.store.upgrade() else {
return;
};
let mut store = match store.try_borrow_mut() {
Ok(s) => s,
Err(_) => return,
};
if let Some(i) = store.index_of(self.id) {
if let OverlayContent::Draw { needs_paint, .. } = &mut store.meta[i].content {
*needs_paint = true;
}
store.layers[i].surface_mut().damage_all();
}
drop(store);
request_frame();
}
pub fn bounds(&self) -> Option<Rect> {
let store = self.store.upgrade()?;
let store = store.try_borrow().ok()?;
let i = store.index_of(self.id)?;
Some(store.layers[i].bounds())
}
pub fn tree(&self) -> Option<UiTree> {
let store = self.store.upgrade()?;
let store = store.try_borrow().ok()?;
let i = store.index_of(self.id)?;
match &store.meta[i].content {
OverlayContent::Tree { tree, .. } => Some(tree.handle()),
_ => None,
}
}
pub fn is_alive(&self) -> bool {
self.bounds().is_some()
}
pub fn remove(&self) {
let Some(store) = self.store.upgrade() else {
return;
};
let mut store = match store.try_borrow_mut() {
Ok(s) => s,
Err(_) => return,
};
if let Some(i) = store.index_of(self.id) {
let bounds = store.layers[i].bounds();
store.layers.remove(i);
store.meta.remove(i);
store.damage_root_under(bounds);
}
drop(store);
request_frame();
}
}
#[derive(Clone)]
pub struct ImageHandle {
store: Weak<RefCell<OverlayStore>>,
id: u64,
}
impl ImageHandle {
fn with_entry(&self, f: impl FnOnce(&mut ImageEntry)) {
let Some(store) = self.store.upgrade() else {
return;
};
let mut store = match store.try_borrow_mut() {
Ok(s) => s,
Err(_) => return,
};
if let Some(entry) = store.images.iter_mut().find(|e| e.id == self.id) {
f(entry);
entry.dirty = true;
}
drop(store);
request_frame();
}
pub fn set_bitmap(&self, bitmap: Bitmap) {
self.with_entry(|e| {
e.bitmap = bitmap;
e.version += 1;
});
}
pub fn set_rect(&self, rect: Rect) {
self.with_entry(|e| e.rect = rect);
}
pub fn remove(&self) {
let Some(store) = self.store.upgrade() else {
return;
};
let mut store = match store.try_borrow_mut() {
Ok(s) => s,
Err(_) => return,
};
if let Some(i) = store.images.iter().position(|e| e.id == self.id) {
let rect = store.images[i].rect;
let key = store.images[i].id;
store.images.remove(i);
store.retired_images.push(key);
store.damage_root_under(rect);
}
drop(store);
request_frame();
}
}
#[cfg(test)]
#[path = "overlay_tests.rs"]
mod tests;