use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use blinc_animation::{AnimationPreset, MultiKeyframeAnimation};
use blinc_core::Color;
use indexmap::IndexMap;
use crate::div::{div, Div};
use crate::key::InstanceKey;
use crate::renderer::RenderTree;
use crate::stack::stack;
use crate::stateful::StateTransitions;
use crate::tree::LayoutNodeId;
pub mod overlay_events {
pub const OPEN: u32 = 20001;
pub const CLOSE: u32 = 20002;
pub const ANIMATION_COMPLETE: u32 = 20003;
pub const BACKDROP_CLICK: u32 = 20004;
pub const ESCAPE: u32 = 20005;
pub const CANCEL_CLOSE: u32 = 20006;
pub const HOVER_LEAVE: u32 = 20007;
pub const HOVER_ENTER: u32 = 20008;
pub const DELAY_EXPIRED: u32 = 20009;
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum OverlayKind {
#[default]
Modal,
Dialog,
ContextMenu,
Toast,
Tooltip,
Dropdown,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum AnchorDirection {
Top,
#[default]
Bottom,
Left,
Right,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum OverlayState {
#[default]
Closed,
Opening,
Open,
PendingClose,
Closing,
}
impl OverlayState {
pub fn is_visible(&self) -> bool {
!matches!(self, OverlayState::Closed)
}
pub fn is_open(&self) -> bool {
matches!(self, OverlayState::Open | OverlayState::PendingClose)
}
pub fn is_animating(&self) -> bool {
matches!(self, OverlayState::Opening | OverlayState::Closing)
}
pub fn is_pending_close(&self) -> bool {
matches!(self, OverlayState::PendingClose)
}
pub fn is_closing(&self) -> bool {
matches!(self, OverlayState::Closing)
}
}
impl StateTransitions for OverlayState {
fn on_event(&self, event: u32) -> Option<Self> {
use overlay_events::*;
use OverlayState::*;
match (self, event) {
(Closed, OPEN) => Some(Opening),
(Opening, ANIMATION_COMPLETE) => Some(Open),
(Open, CLOSE) | (Open, ESCAPE) | (Open, BACKDROP_CLICK) => Some(Closing),
(Open, HOVER_LEAVE) => Some(PendingClose),
(PendingClose, HOVER_ENTER) => Some(Open),
(PendingClose, DELAY_EXPIRED) => Some(Closing),
(PendingClose, CLOSE) | (PendingClose, ESCAPE) | (PendingClose, BACKDROP_CLICK) => {
Some(Closing)
}
(Closing, ANIMATION_COMPLETE) => Some(Closed),
(Opening, CLOSE) | (Opening, ESCAPE) => Some(Closing),
(Closing, CANCEL_CLOSE) => Some(Open),
_ => None,
}
}
}
#[derive(Clone, Debug, Default)]
pub enum OverlayPosition {
#[default]
Centered,
AtPoint { x: f32, y: f32 },
Corner(Corner),
RelativeToAnchor {
anchor: LayoutNodeId,
offset_x: f32,
offset_y: f32,
},
Edge(EdgeSide),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum EdgeSide {
#[default]
Left,
Right,
Top,
Bottom,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum Corner {
TopLeft,
#[default]
TopRight,
BottomLeft,
BottomRight,
}
#[derive(Clone, Debug)]
pub struct BackdropConfig {
pub color: Color,
pub dismiss_on_click: bool,
pub blur: f32,
}
impl Default for BackdropConfig {
fn default() -> Self {
Self {
color: Color::rgba(0.0, 0.0, 0.0, 0.5),
dismiss_on_click: true,
blur: 0.0,
}
}
}
impl BackdropConfig {
pub fn dark() -> Self {
Self::default()
}
pub fn light() -> Self {
Self {
color: Color::rgba(1.0, 1.0, 1.0, 0.3),
..Self::default()
}
}
pub fn persistent() -> Self {
Self {
dismiss_on_click: false,
..Self::default()
}
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn dismiss_on_click(mut self, dismiss: bool) -> Self {
self.dismiss_on_click = dismiss;
self
}
pub fn blur(mut self, blur: f32) -> Self {
self.blur = blur;
self
}
}
#[derive(Clone)]
pub struct OverlayAnimation {
pub enter: MultiKeyframeAnimation,
pub exit: MultiKeyframeAnimation,
}
impl Default for OverlayAnimation {
fn default() -> Self {
Self::modal()
}
}
impl std::fmt::Debug for OverlayAnimation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OverlayAnimation")
.field("enter", &"MultiKeyframeAnimation")
.field("exit", &"MultiKeyframeAnimation")
.finish()
}
}
impl OverlayAnimation {
pub fn modal() -> Self {
Self {
enter: AnimationPreset::scale_in(200),
exit: AnimationPreset::fade_out(170), }
}
pub fn context_menu() -> Self {
Self {
enter: AnimationPreset::pop_in(150),
exit: AnimationPreset::fade_out(120), }
}
pub fn toast() -> Self {
Self {
enter: AnimationPreset::slide_in_right(200, 100.0),
exit: AnimationPreset::slide_out_right(150, 100.0),
}
}
pub fn dropdown() -> Self {
Self {
enter: AnimationPreset::fade_in(100),
exit: AnimationPreset::fade_out(120), }
}
pub fn none() -> Self {
Self {
enter: AnimationPreset::fade_in(0),
exit: AnimationPreset::fade_out(0),
}
}
pub fn custom(enter: MultiKeyframeAnimation, exit: MultiKeyframeAnimation) -> Self {
Self { enter, exit }
}
}
#[derive(Clone, Debug)]
pub struct OverlayConfig {
pub kind: OverlayKind,
pub position: OverlayPosition,
pub backdrop: Option<BackdropConfig>,
pub animation: OverlayAnimation,
pub dismiss_on_escape: bool,
pub dismiss_on_click_outside: bool,
pub dismiss_on_scroll: bool,
pub follows_scroll: bool,
pub dismiss_on_hover_leave: bool,
pub auto_dismiss_ms: Option<u32>,
pub close_delay_ms: Option<u32>,
pub focus_trap: bool,
pub z_priority: i32,
pub size: Option<(f32, f32)>,
pub motion_key: Option<String>,
pub anchor_direction: AnchorDirection,
}
impl Default for OverlayConfig {
fn default() -> Self {
Self::modal()
}
}
impl OverlayConfig {
pub fn modal() -> Self {
Self {
kind: OverlayKind::Modal,
position: OverlayPosition::Centered,
backdrop: Some(BackdropConfig::default()),
animation: OverlayAnimation::modal(),
dismiss_on_escape: true,
dismiss_on_click_outside: false, dismiss_on_scroll: false, follows_scroll: false, dismiss_on_hover_leave: false,
auto_dismiss_ms: None,
close_delay_ms: None,
focus_trap: true,
z_priority: 100,
size: None,
motion_key: None,
anchor_direction: AnchorDirection::Bottom,
}
}
pub fn dialog() -> Self {
Self {
kind: OverlayKind::Dialog,
..Self::modal()
}
}
pub fn context_menu() -> Self {
Self {
kind: OverlayKind::ContextMenu,
position: OverlayPosition::AtPoint { x: 0.0, y: 0.0 },
backdrop: None,
animation: OverlayAnimation::context_menu(),
dismiss_on_escape: true,
dismiss_on_click_outside: true, dismiss_on_scroll: true, follows_scroll: false, dismiss_on_hover_leave: false,
auto_dismiss_ms: None,
close_delay_ms: None,
focus_trap: false,
z_priority: 200,
size: None,
motion_key: None,
anchor_direction: AnchorDirection::Bottom,
}
}
pub fn toast() -> Self {
Self {
kind: OverlayKind::Toast,
position: OverlayPosition::Corner(Corner::TopRight),
backdrop: None,
animation: OverlayAnimation::toast(),
dismiss_on_escape: false,
dismiss_on_click_outside: false, dismiss_on_scroll: false, follows_scroll: false, dismiss_on_hover_leave: false,
auto_dismiss_ms: Some(3000),
close_delay_ms: None,
focus_trap: false,
z_priority: 300,
size: None,
motion_key: None,
anchor_direction: AnchorDirection::Bottom,
}
}
pub fn dropdown() -> Self {
Self {
kind: OverlayKind::Dropdown,
position: OverlayPosition::Centered, backdrop: Some(BackdropConfig {
color: blinc_core::Color::TRANSPARENT,
dismiss_on_click: true,
blur: 0.0,
}),
animation: OverlayAnimation::dropdown(),
dismiss_on_escape: true,
dismiss_on_click_outside: false, dismiss_on_scroll: false, follows_scroll: false, dismiss_on_hover_leave: false,
auto_dismiss_ms: None,
close_delay_ms: None,
focus_trap: false,
z_priority: 150,
size: None,
motion_key: None,
anchor_direction: AnchorDirection::Bottom,
}
}
pub fn hover_card() -> Self {
Self {
kind: OverlayKind::Tooltip, position: OverlayPosition::Centered, backdrop: None,
animation: OverlayAnimation::dropdown(),
dismiss_on_escape: true,
dismiss_on_click_outside: false, dismiss_on_scroll: false, follows_scroll: false, dismiss_on_hover_leave: true,
auto_dismiss_ms: Some(5000), close_delay_ms: Some(300), focus_trap: false,
z_priority: 150,
size: None,
motion_key: None,
anchor_direction: AnchorDirection::Bottom, }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct OverlayHandle(u64);
impl OverlayHandle {
fn new(id: u64) -> Self {
Self(id)
}
pub fn from_raw(id: u64) -> Self {
Self(id)
}
pub fn id(&self) -> u64 {
self.0
}
}
pub type OnCloseCallback = Arc<dyn Fn() + Send + Sync>;
pub struct ActiveOverlay {
pub handle: OverlayHandle,
pub config: OverlayConfig,
pub state: OverlayState,
content_builder: Box<dyn Fn() -> Div + Send + Sync>,
created_at_ms: Option<u64>,
opened_at_ms: Option<u64>,
close_started_at_ms: Option<u64>,
pending_close_at_ms: Option<u64>,
pub cached_size: Option<(f32, f32)>,
on_close: Option<OnCloseCallback>,
pending_close_on_open: bool,
scroll_offset_y: f32,
}
impl ActiveOverlay {
pub fn is_visible(&self) -> bool {
self.state.is_visible()
}
pub fn build_content(&self) -> Div {
(self.content_builder)()
}
pub fn transition(&mut self, event: u32) -> bool {
if let Some(new_state) = self.state.on_event(event) {
let old_state = self.state;
self.state = new_state;
if new_state == OverlayState::Closing && old_state != OverlayState::Closing {
if let Some(ref key) = self.config.motion_key {
let full_motion_key = format!("motion:{}", key);
crate::selector::query_motion(&full_motion_key).exit();
tracing::debug!(
"Overlay {:?} transitioning to Closing - triggered motion exit for key={}",
self.handle,
full_motion_key
);
}
if self.config.backdrop.is_some() {
let backdrop_motion_key = format!("motion:overlay_backdrop_{}", self.handle.0);
crate::selector::query_motion(&backdrop_motion_key).exit();
tracing::debug!(
"Overlay {:?} transitioning to Closing - triggered backdrop motion exit",
self.handle
);
}
}
true
} else {
false
}
}
pub fn animation_progress(&self, current_time_ms: u64) -> Option<(f32, bool)> {
match self.state {
OverlayState::Opening => {
let duration = self.config.animation.enter.duration_ms() as f32;
if duration <= 0.0 {
return None;
}
let created_at = self.created_at_ms.unwrap_or(current_time_ms);
let elapsed = (current_time_ms.saturating_sub(created_at)) as f32;
let progress = (elapsed / duration).clamp(0.0, 1.0);
Some((progress, true))
}
OverlayState::Closing => {
let duration = self.config.animation.exit.duration_ms() as f32;
if duration <= 0.0 {
return None;
}
let close_started = self.close_started_at_ms.unwrap_or(current_time_ms);
let elapsed = (current_time_ms.saturating_sub(close_started)) as f32;
let progress = (elapsed / duration).clamp(0.0, 1.0);
Some((progress, false))
}
_ => None,
}
}
}
pub struct OverlayManagerInner {
overlays: IndexMap<OverlayHandle, ActiveOverlay>,
next_id: AtomicU64,
dirty: AtomicBool,
animation_dirty: AtomicBool,
viewport: (f32, f32),
scale_factor: f32,
toast_corner: Corner,
max_toasts: usize,
toast_gap: f32,
current_time_ms: u64,
}
impl OverlayManagerInner {
pub fn new() -> Self {
Self {
overlays: IndexMap::new(),
next_id: AtomicU64::new(1),
dirty: AtomicBool::new(false),
animation_dirty: AtomicBool::new(false),
viewport: (0.0, 0.0),
scale_factor: 1.0,
toast_corner: Corner::TopRight,
max_toasts: 5,
toast_gap: 8.0,
current_time_ms: 0,
}
}
pub fn set_viewport(&mut self, width: f32, height: f32) {
self.viewport = (width, height);
}
pub fn set_viewport_with_scale(&mut self, width: f32, height: f32, scale_factor: f32) {
self.viewport = (width, height);
self.scale_factor = scale_factor;
}
pub fn scale_factor(&self) -> f32 {
self.scale_factor
}
pub fn set_toast_corner(&mut self, corner: Corner) {
self.toast_corner = corner;
}
pub fn take_dirty(&self) -> bool {
self.dirty.swap(false, Ordering::SeqCst)
}
pub fn is_dirty(&self) -> bool {
self.dirty.load(Ordering::SeqCst)
}
pub fn take_animation_dirty(&self) -> bool {
self.animation_dirty.swap(false, Ordering::SeqCst)
}
pub fn needs_redraw(&self) -> bool {
self.dirty.load(Ordering::SeqCst) || self.animation_dirty.load(Ordering::SeqCst)
}
fn mark_dirty(&self) {
self.dirty.store(true, Ordering::SeqCst);
}
fn mark_animation_dirty(&self) {
self.animation_dirty.store(true, Ordering::SeqCst);
}
pub fn add(
&mut self,
config: OverlayConfig,
content: impl Fn() -> Div + Send + Sync + 'static,
) -> OverlayHandle {
self.add_with_close_callback(config, content, None)
}
pub fn add_with_close_callback(
&mut self,
config: OverlayConfig,
content: impl Fn() -> Div + Send + Sync + 'static,
on_close: Option<OnCloseCallback>,
) -> OverlayHandle {
let id = self.next_id.fetch_add(1, Ordering::SeqCst);
let handle = OverlayHandle::new(id);
tracing::debug!(
"OverlayManager::add - adding {:?} overlay with handle {:?}",
config.kind,
handle
);
let overlay = ActiveOverlay {
handle,
config,
state: OverlayState::Opening,
content_builder: Box::new(content),
created_at_ms: None, opened_at_ms: None,
close_started_at_ms: None,
pending_close_at_ms: None,
cached_size: None,
on_close,
pending_close_on_open: false,
scroll_offset_y: 0.0,
};
self.overlays.insert(handle, overlay);
self.mark_dirty();
tracing::debug!(
"OverlayManager::add - now have {} overlays",
self.overlays.len()
);
handle
}
pub fn update(&mut self, current_time_ms: u64) {
self.current_time_ms = current_time_ms;
let mut to_close = Vec::new();
let mut content_dirty = false;
let mut animation_dirty = false;
for (handle, overlay) in self.overlays.iter_mut() {
if overlay.created_at_ms.is_none() {
overlay.created_at_ms = Some(current_time_ms);
content_dirty = true;
}
match overlay.state {
OverlayState::Opening => {
let enter_duration = overlay.config.animation.enter.duration_ms();
if let Some(created_at) = overlay.created_at_ms {
let elapsed = current_time_ms.saturating_sub(created_at);
if elapsed >= enter_duration as u64 {
if overlay.transition(overlay_events::ANIMATION_COMPLETE) {
overlay.opened_at_ms = Some(current_time_ms);
animation_dirty = true;
if overlay.pending_close_on_open {
overlay.pending_close_on_open = false;
tracing::debug!(
"Overlay {:?} just opened but pending_close_on_open - removing immediately",
handle
);
overlay.state = OverlayState::Closed;
content_dirty = true;
}
}
} else {
animation_dirty = true;
}
}
}
OverlayState::Open => {
if let Some(duration_ms) = overlay.config.auto_dismiss_ms {
if let Some(opened_at) = overlay.opened_at_ms {
if current_time_ms >= opened_at + duration_ms as u64 {
to_close.push(*handle);
}
}
}
}
OverlayState::PendingClose => {
if let Some(close_delay_ms) = overlay.config.close_delay_ms {
if let Some(pending_close_at) = overlay.pending_close_at_ms {
let elapsed = current_time_ms.saturating_sub(pending_close_at);
if elapsed >= close_delay_ms as u64 {
tracing::debug!(
"Overlay {:?} close delay expired after {}ms, transitioning to Closing",
handle,
elapsed
);
if overlay.transition(overlay_events::DELAY_EXPIRED) {
animation_dirty = true;
}
}
}
} else {
if overlay.transition(overlay_events::DELAY_EXPIRED) {
animation_dirty = true;
}
}
}
OverlayState::Closing => {
if overlay.close_started_at_ms.is_none() {
overlay.close_started_at_ms = Some(current_time_ms);
tracing::debug!(
"Overlay {:?} started closing at {}ms, exit duration={}ms",
handle,
current_time_ms,
overlay.config.animation.exit.duration_ms()
);
animation_dirty = true;
}
let exit_duration = overlay.config.animation.exit.duration_ms();
let overlay_exit_complete =
if let Some(close_started) = overlay.close_started_at_ms {
let elapsed = current_time_ms.saturating_sub(close_started);
elapsed >= exit_duration as u64
} else {
false
};
let motion_exit_complete = if let Some(ref key) = overlay.config.motion_key {
let full_motion_key = format!("motion:{}", key);
let motion = crate::selector::query_motion(&full_motion_key);
!motion.is_animating()
} else {
true };
if overlay_exit_complete && motion_exit_complete {
tracing::debug!(
"Overlay {:?} exit complete (overlay_exit={}, motion_exit={})",
handle,
overlay_exit_complete,
motion_exit_complete
);
if overlay.transition(overlay_events::ANIMATION_COMPLETE) {
content_dirty = true;
}
} else {
animation_dirty = true;
}
}
OverlayState::Closed => {
}
}
}
for handle in to_close {
if let Some(overlay) = self.overlays.get_mut(&handle) {
overlay.transition(overlay_events::CLOSE);
animation_dirty = true;
}
}
let count_before = self.overlays.len();
let mut callbacks_to_invoke = Vec::new();
self.overlays.retain(|_, o| {
if o.state == OverlayState::Closed {
if let Some(cb) = o.on_close.take() {
callbacks_to_invoke.push(cb);
}
false } else {
true }
});
if self.overlays.len() != count_before {
content_dirty = true;
}
for cb in callbacks_to_invoke {
cb();
}
if content_dirty {
self.mark_dirty();
} else if animation_dirty {
self.mark_animation_dirty();
}
}
pub fn close(&mut self, handle: OverlayHandle) {
if let Some(overlay) = self.overlays.get_mut(&handle) {
if overlay.transition(overlay_events::CLOSE) {
self.mark_animation_dirty();
}
}
}
pub fn close_immediate(&mut self, handle: OverlayHandle) {
if let Some(overlay) = self.overlays.get_mut(&handle) {
overlay.state = OverlayState::Closed;
if let Some(cb) = overlay.on_close.take() {
cb();
}
self.mark_animation_dirty();
}
}
pub fn cancel_close(&mut self, handle: OverlayHandle) {
if let Some(overlay) = self.overlays.get_mut(&handle) {
if overlay.transition(overlay_events::CANCEL_CLOSE) {
self.mark_animation_dirty();
}
}
}
pub fn hover_leave(&mut self, handle: OverlayHandle) {
if let Some(overlay) = self.overlays.get_mut(&handle) {
let old_state = overlay.state;
if overlay.transition(overlay_events::HOVER_LEAVE) {
let new_state = overlay.state;
tracing::debug!(
"Overlay {:?} hover leave: {:?} -> {:?} at {}ms",
handle,
old_state,
new_state,
self.current_time_ms
);
if new_state == OverlayState::PendingClose {
overlay.pending_close_at_ms = Some(self.current_time_ms);
}
}
}
}
pub fn hover_enter(&mut self, handle: OverlayHandle) {
if let Some(overlay) = self.overlays.get_mut(&handle) {
if overlay.pending_close_on_open {
tracing::debug!("hover_enter: canceling pending_close_on_open");
overlay.pending_close_on_open = false;
}
if overlay.transition(overlay_events::HOVER_ENTER) {
overlay.pending_close_at_ms = None;
tracing::debug!(
"Overlay {:?} hover enter -> Open (canceled pending close)",
handle
);
}
}
}
pub fn is_pending_close(&self, handle: OverlayHandle) -> bool {
self.overlays
.get(&handle)
.map(|o| o.state.is_pending_close() || o.pending_close_on_open)
.unwrap_or(false)
}
pub fn is_closing(&self, handle: OverlayHandle) -> bool {
self.overlays
.get(&handle)
.map(|o| o.state.is_closing())
.unwrap_or(false)
}
pub fn set_content_size(&mut self, handle: OverlayHandle, width: f32, height: f32) {
if let Some(overlay) = self.overlays.get_mut(&handle) {
overlay.cached_size = Some((width, height));
}
}
pub fn handle_scroll(&mut self, delta_y: f32) -> bool {
let mut updated = false;
for overlay in self.overlays.values_mut() {
if overlay.config.follows_scroll && overlay.state.is_visible() {
overlay.scroll_offset_y += delta_y;
updated = true;
}
}
if updated {
self.mark_animation_dirty();
}
updated
}
pub fn get_scroll_offsets(&self) -> Vec<(String, f32)> {
self.overlays
.iter()
.filter(|(_, o)| o.config.follows_scroll && o.state.is_visible())
.map(|(handle, overlay)| {
let element_id = format!("overlay_scroll_{}", handle.id());
(element_id, overlay.scroll_offset_y)
})
.collect()
}
pub fn get_visible_overlay_bounds(&self) -> Vec<(f32, f32, f32, f32)> {
let (vp_width, vp_height) = self.viewport;
self.overlays
.values()
.filter(|o| o.is_visible())
.filter_map(|overlay| {
let (w, h) = overlay.cached_size.unwrap_or((300.0, 200.0));
let (mut x, mut y) = match &overlay.config.position {
OverlayPosition::AtPoint { x, y } => (*x, *y),
OverlayPosition::Centered => {
((vp_width - w) / 2.0, (vp_height - h) / 2.0)
}
OverlayPosition::Corner(corner) => {
let margin = 16.0;
match corner {
Corner::TopLeft => (margin, margin),
Corner::TopRight => (vp_width - w - margin, margin),
Corner::BottomLeft => (margin, vp_height - h - margin),
Corner::BottomRight => {
(vp_width - w - margin, vp_height - h - margin)
}
}
}
OverlayPosition::RelativeToAnchor { .. } => {
return None;
}
OverlayPosition::Edge(side) => {
match side {
EdgeSide::Left => (0.0, 0.0),
EdgeSide::Right => (vp_width - w, 0.0),
EdgeSide::Top => (0.0, 0.0),
EdgeSide::Bottom => (0.0, vp_height - h),
}
}
};
if matches!(overlay.config.position, OverlayPosition::AtPoint { .. }) {
match overlay.config.anchor_direction {
AnchorDirection::Top => {
y -= h;
}
AnchorDirection::Left => {
x -= w;
}
AnchorDirection::Bottom | AnchorDirection::Right => {
}
}
}
y += overlay.scroll_offset_y;
tracing::debug!(
"get_visible_overlay_bounds: kind={:?} pos={:?} dir={:?} bounds=({}, {}, {}, {})",
overlay.config.kind,
overlay.config.position,
overlay.config.anchor_direction,
x, y, w, h
);
Some((x, y, w, h))
})
.collect()
}
pub fn close_top(&mut self) {
if let Some(handle) = self
.overlays
.values()
.filter(|o| o.state.is_open())
.max_by_key(|o| o.config.z_priority)
.map(|o| o.handle)
{
self.close(handle);
}
}
pub fn close_all_of(&mut self, kind: OverlayKind) {
let handles: Vec<_> = self
.overlays
.values()
.filter(|o| o.config.kind == kind && o.is_visible())
.map(|o| o.handle)
.collect();
for handle in handles {
self.close(handle);
}
}
pub fn close_all(&mut self) {
let handles: Vec<_> = self
.overlays
.values()
.filter(|o| o.is_visible())
.map(|o| o.handle)
.collect();
for handle in handles {
self.close(handle);
}
}
pub fn cleanup(&mut self) {
self.overlays.retain(|_, o| o.state != OverlayState::Closed);
}
pub fn handle_escape(&mut self) -> bool {
if let Some(handle) = self
.overlays
.values()
.filter(|o| o.state.is_open() && o.config.dismiss_on_escape)
.max_by_key(|o| o.config.z_priority)
.map(|o| o.handle)
{
if let Some(overlay) = self.overlays.get_mut(&handle) {
if overlay.transition(overlay_events::ESCAPE) {
self.mark_animation_dirty();
return true;
}
}
}
false
}
pub fn has_blocking_overlay(&self) -> bool {
self.overlays.values().any(|o| {
o.is_visible()
&& matches!(o.config.kind, OverlayKind::Modal | OverlayKind::Dialog)
&& o.config.backdrop.is_some()
})
}
pub fn has_dismissable_overlay(&self) -> bool {
self.overlays.values().any(|o| {
o.state.is_open()
&& (o.config.dismiss_on_click_outside
|| o.config
.backdrop
.as_ref()
.map(|b| b.dismiss_on_click)
.unwrap_or(false))
})
}
pub fn handle_backdrop_click(&mut self) -> bool {
if let Some(handle) = self
.overlays
.values()
.filter(|o| {
o.state.is_open()
&& (o.config.dismiss_on_click_outside
|| o.config
.backdrop
.as_ref()
.map(|b| b.dismiss_on_click)
.unwrap_or(false))
})
.max_by_key(|o| o.config.z_priority)
.map(|o| o.handle)
{
if let Some(overlay) = self.overlays.get_mut(&handle) {
if overlay.transition(overlay_events::BACKDROP_CLICK) {
self.mark_animation_dirty();
return true;
}
}
}
false
}
pub fn is_backdrop_click(&self, x: f32, y: f32) -> bool {
if let Some(overlay) = self
.overlays
.values()
.filter(|o| {
o.state.is_open()
&& (o.config.dismiss_on_click_outside
|| o.config
.backdrop
.as_ref()
.map(|b| b.dismiss_on_click)
.unwrap_or(false))
})
.max_by_key(|o| o.config.z_priority)
{
let (content_w, content_h) = overlay.cached_size.unwrap_or_else(|| {
overlay.config.size.unwrap_or((400.0, 300.0))
});
let (vp_w, vp_h) = self.viewport;
let (content_x, content_y) = match &overlay.config.position {
OverlayPosition::Centered => {
((vp_w - content_w) / 2.0, (vp_h - content_h) / 2.0)
}
OverlayPosition::AtPoint { x: px, y: py } => {
(*px, *py + overlay.scroll_offset_y)
}
OverlayPosition::Corner(corner) => {
let margin = 16.0;
match corner {
Corner::TopLeft => (margin, margin),
Corner::TopRight => (vp_w - content_w - margin, margin),
Corner::BottomLeft => (margin, vp_h - content_h - margin),
Corner::BottomRight => {
(vp_w - content_w - margin, vp_h - content_h - margin)
}
}
}
OverlayPosition::RelativeToAnchor {
offset_x, offset_y, ..
} => {
(*offset_x, *offset_y)
}
OverlayPosition::Edge(side) => {
match side {
EdgeSide::Left => (0.0, 0.0),
EdgeSide::Right => (vp_w - content_w, 0.0),
EdgeSide::Top => (0.0, 0.0),
EdgeSide::Bottom => (0.0, vp_h - content_h),
}
}
};
let in_content = x >= content_x
&& x <= content_x + content_w
&& y >= content_y
&& y <= content_y + content_h;
!in_content
} else {
false
}
}
pub fn handle_click_at(&mut self, x: f32, y: f32) -> bool {
if self.is_backdrop_click(x, y) {
self.handle_backdrop_click()
} else {
false
}
}
pub fn overlays_sorted(&self) -> Vec<&ActiveOverlay> {
let mut overlays: Vec<_> = self.overlays.values().collect();
overlays.sort_by_key(|o| o.config.z_priority);
overlays
}
pub fn has_visible_overlays(&self) -> bool {
self.overlays.values().any(|o| o.is_visible())
}
pub fn has_animating_overlays(&self) -> bool {
self.overlays.values().any(|o| o.state.is_animating())
}
pub fn overlay_count(&self) -> usize {
self.overlays.len()
}
pub fn build_overlay_tree(&self) -> Option<RenderTree> {
if !self.has_visible_overlays() {
return None;
}
let (width, height) = self.viewport;
if width <= 0.0 || height <= 0.0 {
tracing::debug!("build_overlay_tree: invalid viewport");
return None;
}
let mut root = stack().w(width).h(height);
for overlay in self.overlays_sorted() {
if overlay.is_visible() {
tracing::debug!(
"build_overlay_tree: adding overlay {:?}",
overlay.config.kind
);
root = root.child(self.build_single_overlay(overlay, width, height));
}
}
tracing::debug!("build_overlay_tree: building render tree");
let mut tree = RenderTree::from_element(&root);
tree.set_scale_factor(self.scale_factor);
tree.compute_layout(width, height);
Some(tree)
}
}
pub const OVERLAY_LAYER_ID: &str = "__blinc_overlay_layer__";
impl OverlayManagerInner {
pub fn build_overlay_layer(&self) -> Div {
let (width, height) = self.viewport;
let has_visible = self.has_visible_overlays();
let overlay_count = self.overlays.len();
tracing::debug!(
"build_overlay_layer: viewport={}x{}, has_visible={}, overlay_count={}",
width,
height,
has_visible,
overlay_count
);
let (layer_w, layer_h) = if has_visible && width > 0.0 && height > 0.0 {
(width, height)
} else {
(0.0, 0.0)
};
tracing::debug!("build_overlay_layer: layer size={}x{}", layer_w, layer_h);
let mut layer = div()
.id(OVERLAY_LAYER_ID)
.w(layer_w)
.h(layer_h)
.absolute()
.left(0.0)
.top(0.0)
.stack_layer()
.pointer_events_none();
if has_visible && width > 0.0 && height > 0.0 {
let mut toasts_by_corner: std::collections::HashMap<Corner, Vec<&ActiveOverlay>> =
std::collections::HashMap::new();
let mut non_toasts: Vec<&ActiveOverlay> = Vec::new();
for overlay in self.overlays_sorted() {
if overlay.is_visible() {
if overlay.config.kind == OverlayKind::Toast {
if let OverlayPosition::Corner(corner) = overlay.config.position {
toasts_by_corner.entry(corner).or_default().push(overlay);
} else {
non_toasts.push(overlay);
}
} else {
non_toasts.push(overlay);
}
}
}
for overlay in non_toasts {
layer = layer.child(self.build_single_overlay(overlay, width, height));
}
for (corner, toasts) in toasts_by_corner {
layer = layer.child(self.build_toast_stack(&toasts, corner, width, height));
}
}
layer
}
fn build_toast_stack(
&self,
toasts: &[&ActiveOverlay],
corner: Corner,
vp_width: f32,
vp_height: f32,
) -> Div {
let margin = 16.0;
let mut container = div()
.absolute()
.w(vp_width)
.h(vp_height)
.pointer_events_none()
.p(margin);
let (container, reverse) = match corner {
Corner::TopLeft => (container.items_start().justify_start(), false),
Corner::TopRight => (container.items_end().justify_start(), false),
Corner::BottomLeft => (container.items_start().justify_end(), true),
Corner::BottomRight => (container.items_end().justify_end(), true),
};
let mut toast_stack = div().flex_col().gap(self.toast_gap);
let toasts_ordered: Vec<_> = if reverse {
toasts.iter().rev().collect()
} else {
toasts.iter().collect()
};
for toast in toasts_ordered {
let content = toast.build_content();
let content = if let Some((w, h)) = toast.config.size {
content.w(w).h(h)
} else {
content
};
let content = if toast.config.dismiss_on_hover_leave {
let overlay_handle = toast.handle;
let has_close_delay = toast.config.close_delay_ms.is_some();
content.on_hover_leave(move |_| {
if let Some(ctx) = crate::overlay_state::OverlayContext::try_get() {
let mgr = ctx.overlay_manager();
let mut inner = mgr.lock().unwrap();
if has_close_delay {
inner.hover_leave(overlay_handle);
} else {
inner.close(overlay_handle);
}
}
})
} else {
content
};
toast_stack = toast_stack.child(content);
}
container.child(toast_stack)
}
pub fn build_overlay_content(&self) -> Div {
self.build_overlay_layer()
}
fn build_single_overlay(&self, overlay: &ActiveOverlay, vp_width: f32, vp_height: f32) -> Div {
let content = overlay.build_content();
let content = if let Some((w, h)) = overlay.config.size {
content.w(w).h(h)
} else {
content
};
let content = if overlay.config.dismiss_on_hover_leave {
let overlay_handle = overlay.handle;
let has_close_delay = overlay.config.close_delay_ms.is_some();
content.on_hover_leave(move |_| {
tracing::debug!("OVERLAY dismiss_on_hover_leave handler fired");
if let Some(ctx) = crate::overlay_state::OverlayContext::try_get() {
let mgr = ctx.overlay_manager();
let mut inner = mgr.lock().unwrap();
if has_close_delay {
tracing::debug!("OVERLAY: calling hover_leave (has_close_delay=true)");
inner.hover_leave(overlay_handle);
} else {
inner.close(overlay_handle);
}
}
})
} else {
content
};
if let Some(ref backdrop_config) = overlay.config.backdrop {
let backdrop_color = backdrop_config.color;
let enter_duration = overlay.config.animation.enter.duration_ms();
let exit_duration = overlay.config.animation.exit.duration_ms();
let backdrop_motion_key = format!("overlay_backdrop_{}", overlay.handle.0);
let backdrop_div = if backdrop_config.dismiss_on_click {
let overlay_handle = overlay.handle;
div()
.absolute()
.left(0.0)
.top(0.0)
.w(vp_width)
.h(vp_height)
.bg(backdrop_color)
.on_click(move |_| {
println!("Backdrop clicked! Dismissing overlay {:?}", overlay_handle);
if let Some(ctx) = crate::overlay_state::OverlayContext::try_get() {
ctx.overlay_manager().lock().unwrap().close(overlay_handle);
}
})
} else {
div()
.absolute()
.left(0.0)
.top(0.0)
.w(vp_width)
.h(vp_height)
.bg(backdrop_color)
};
let animated_backdrop = crate::motion::motion_derived(&backdrop_motion_key)
.fade_in(enter_duration)
.fade_out(exit_duration)
.child(backdrop_div);
div().w(vp_width).h(vp_height).child(
stack()
.w(vp_width)
.h(vp_height)
.child(animated_backdrop)
.child(self.position_content(overlay, content, vp_width, vp_height)),
)
} else {
self.position_content(overlay, content, vp_width, vp_height)
}
}
fn position_content(
&self,
overlay: &ActiveOverlay,
content: Div,
vp_width: f32,
vp_height: f32,
) -> Div {
match &overlay.config.position {
OverlayPosition::Centered => {
div()
.w(vp_width)
.h(vp_height)
.pointer_events_none()
.items_center()
.justify_center()
.child(content)
}
OverlayPosition::AtPoint { x, y } => {
let wrapper_id = format!("overlay_scroll_{}", overlay.handle.id());
div()
.id(&wrapper_id)
.absolute()
.left(*x)
.top(*y)
.child(content)
}
OverlayPosition::Corner(corner) => {
let margin = 16.0;
self.position_in_corner(content, *corner, vp_width, vp_height, margin)
}
OverlayPosition::RelativeToAnchor {
offset_x, offset_y, ..
} => {
div()
.w(vp_width)
.h(vp_height)
.pointer_events_none()
.child(content.ml(*offset_x).mt(*offset_y))
}
OverlayPosition::Edge(side) => {
let container = div().w(vp_width).h(vp_height).pointer_events_none();
match side {
EdgeSide::Left => container
.flex_row()
.items_start()
.justify_start()
.child(content),
EdgeSide::Right => container
.flex_row()
.items_start()
.justify_end()
.child(content),
EdgeSide::Top => container
.flex_col()
.items_start()
.justify_start()
.child(content),
EdgeSide::Bottom => container
.flex_col()
.items_start()
.justify_end()
.child(content),
}
}
}
}
fn position_in_corner(
&self,
content: Div,
corner: Corner,
vp_width: f32,
vp_height: f32,
margin: f32,
) -> Div {
let container = div().w(vp_width).h(vp_height).pointer_events_none();
match corner {
Corner::TopLeft => container
.items_start()
.justify_start()
.child(content.m(margin)),
Corner::TopRight => container
.items_end()
.justify_start()
.child(content.m(margin)),
Corner::BottomLeft => container
.items_start()
.justify_end()
.child(content.m(margin)),
Corner::BottomRight => container.items_end().justify_end().child(content.m(margin)),
}
}
pub fn layout_toasts(&self) -> Vec<(OverlayHandle, f32, f32)> {
let (vp_width, vp_height) = self.viewport;
let toasts: Vec<_> = self
.overlays
.values()
.filter(|o| o.config.kind == OverlayKind::Toast && o.is_visible())
.collect();
let margin = 16.0;
let mut positions = Vec::new();
let mut y_offset = margin;
for (i, toast) in toasts.iter().take(self.max_toasts).enumerate() {
let estimated_height = toast.cached_size.map(|(_, h)| h).unwrap_or(60.0);
let (x, y) = match self.toast_corner {
Corner::TopLeft => (margin, y_offset),
Corner::TopRight => (vp_width - margin - 300.0, y_offset), Corner::BottomLeft => (margin, vp_height - y_offset - estimated_height),
Corner::BottomRight => (
vp_width - margin - 300.0,
vp_height - y_offset - estimated_height,
),
};
positions.push((toast.handle, x, y));
y_offset += estimated_height + self.toast_gap;
}
positions
}
}
impl Default for OverlayManagerInner {
fn default() -> Self {
Self::new()
}
}
pub type OverlayManager = Arc<Mutex<OverlayManagerInner>>;
pub fn overlay_manager() -> OverlayManager {
Arc::new(Mutex::new(OverlayManagerInner::new()))
}
pub trait OverlayManagerExt {
fn modal(&self) -> ModalBuilder;
fn dialog(&self) -> DialogBuilder;
fn context_menu(&self) -> ContextMenuBuilder;
fn toast(&self) -> ToastBuilder;
fn dropdown(&self) -> DropdownBuilder;
fn hover_card(&self) -> DropdownBuilder;
fn close(&self, handle: OverlayHandle);
fn close_immediate(&self, handle: OverlayHandle);
fn cancel_close(&self, handle: OverlayHandle);
fn hover_leave(&self, handle: OverlayHandle);
fn hover_enter(&self, handle: OverlayHandle);
fn is_pending_close(&self, handle: OverlayHandle) -> bool;
fn is_closing(&self, handle: OverlayHandle) -> bool;
fn close_top(&self);
fn close_all_of(&self, kind: OverlayKind);
fn close_all(&self);
fn handle_escape(&self) -> bool;
fn handle_backdrop_click(&self) -> bool;
fn handle_click_at(&self, x: f32, y: f32) -> bool;
fn set_viewport(&self, width: f32, height: f32);
fn set_viewport_with_scale(&self, width: f32, height: f32, scale_factor: f32);
fn build_overlay_tree(&self) -> Option<RenderTree>;
fn build_overlay_layer(&self) -> Div;
fn has_blocking_overlay(&self) -> bool;
fn has_dismissable_overlay(&self) -> bool;
fn has_visible_overlays(&self) -> bool;
fn has_animating_overlays(&self) -> bool;
fn is_visible(&self, handle: OverlayHandle) -> bool;
fn update(&self, current_time_ms: u64);
fn take_dirty(&self) -> bool;
fn is_dirty(&self) -> bool;
fn take_animation_dirty(&self) -> bool;
fn needs_redraw(&self) -> bool;
fn set_content_size(&self, handle: OverlayHandle, width: f32, height: f32);
fn handle_scroll(&self, delta_y: f32) -> bool;
fn get_scroll_offsets(&self) -> Vec<(String, f32)>;
fn mark_content_dirty(&self);
fn request_redraw(&self);
fn get_visible_overlay_bounds(&self) -> Vec<(f32, f32, f32, f32)>;
}
impl OverlayManagerExt for OverlayManager {
fn modal(&self) -> ModalBuilder {
ModalBuilder::new(Arc::clone(self))
}
fn dialog(&self) -> DialogBuilder {
DialogBuilder::new(Arc::clone(self))
}
fn context_menu(&self) -> ContextMenuBuilder {
ContextMenuBuilder::new(Arc::clone(self))
}
fn toast(&self) -> ToastBuilder {
ToastBuilder::new(Arc::clone(self))
}
fn dropdown(&self) -> DropdownBuilder {
DropdownBuilder::new(Arc::clone(self))
}
fn hover_card(&self) -> DropdownBuilder {
DropdownBuilder::new_hover_card(Arc::clone(self))
}
fn close(&self, handle: OverlayHandle) {
self.lock().unwrap().close(handle);
}
fn close_immediate(&self, handle: OverlayHandle) {
self.lock().unwrap().close_immediate(handle);
}
fn cancel_close(&self, handle: OverlayHandle) {
self.lock().unwrap().cancel_close(handle);
}
fn hover_leave(&self, handle: OverlayHandle) {
self.lock().unwrap().hover_leave(handle);
}
fn hover_enter(&self, handle: OverlayHandle) {
self.lock().unwrap().hover_enter(handle);
}
fn is_pending_close(&self, handle: OverlayHandle) -> bool {
self.lock().unwrap().is_pending_close(handle)
}
fn is_closing(&self, handle: OverlayHandle) -> bool {
self.lock().unwrap().is_closing(handle)
}
fn close_top(&self) {
self.lock().unwrap().close_top();
}
fn close_all_of(&self, kind: OverlayKind) {
self.lock().unwrap().close_all_of(kind);
}
fn close_all(&self) {
self.lock().unwrap().close_all();
}
fn handle_escape(&self) -> bool {
self.lock().unwrap().handle_escape()
}
fn handle_backdrop_click(&self) -> bool {
self.lock().unwrap().handle_backdrop_click()
}
fn handle_click_at(&self, x: f32, y: f32) -> bool {
self.lock().unwrap().handle_click_at(x, y)
}
fn set_viewport(&self, width: f32, height: f32) {
self.lock().unwrap().set_viewport(width, height);
}
fn set_viewport_with_scale(&self, width: f32, height: f32, scale_factor: f32) {
self.lock()
.unwrap()
.set_viewport_with_scale(width, height, scale_factor);
}
fn build_overlay_tree(&self) -> Option<RenderTree> {
self.lock().unwrap().build_overlay_tree()
}
fn build_overlay_layer(&self) -> Div {
self.lock().unwrap().build_overlay_layer()
}
fn has_blocking_overlay(&self) -> bool {
self.lock().unwrap().has_blocking_overlay()
}
fn has_dismissable_overlay(&self) -> bool {
self.lock().unwrap().has_dismissable_overlay()
}
fn has_visible_overlays(&self) -> bool {
self.lock().unwrap().has_visible_overlays()
}
fn has_animating_overlays(&self) -> bool {
self.lock().unwrap().has_animating_overlays()
}
fn is_visible(&self, handle: OverlayHandle) -> bool {
self.lock()
.unwrap()
.overlays
.get(&handle)
.map(|o| o.is_visible())
.unwrap_or(false)
}
fn update(&self, current_time_ms: u64) {
self.lock().unwrap().update(current_time_ms);
}
fn take_dirty(&self) -> bool {
self.lock().unwrap().take_dirty()
}
fn is_dirty(&self) -> bool {
self.lock().unwrap().is_dirty()
}
fn take_animation_dirty(&self) -> bool {
self.lock().unwrap().take_animation_dirty()
}
fn needs_redraw(&self) -> bool {
self.lock().unwrap().needs_redraw()
}
fn set_content_size(&self, handle: OverlayHandle, width: f32, height: f32) {
self.lock().unwrap().set_content_size(handle, width, height);
}
fn handle_scroll(&self, delta_y: f32) -> bool {
self.lock().unwrap().handle_scroll(delta_y)
}
fn get_scroll_offsets(&self) -> Vec<(String, f32)> {
self.lock().unwrap().get_scroll_offsets()
}
fn mark_content_dirty(&self) {
self.lock().unwrap().mark_dirty();
}
fn request_redraw(&self) {
self.lock().unwrap().mark_animation_dirty();
}
fn get_visible_overlay_bounds(&self) -> Vec<(f32, f32, f32, f32)> {
self.lock().unwrap().get_visible_overlay_bounds()
}
}
pub struct ModalBuilder {
manager: OverlayManager,
config: OverlayConfig,
content: Option<Box<dyn Fn() -> Div + Send + Sync>>,
}
impl ModalBuilder {
fn new(manager: OverlayManager) -> Self {
Self {
manager,
config: OverlayConfig::modal(),
content: None,
}
}
pub fn content<F>(mut self, f: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.content = Some(Box::new(f));
self
}
pub fn size(mut self, width: f32, height: f32) -> Self {
self.config.size = Some((width, height));
self
}
pub fn backdrop(mut self, config: BackdropConfig) -> Self {
self.config.backdrop = Some(config);
self
}
pub fn no_backdrop(mut self) -> Self {
self.config.backdrop = None;
self
}
pub fn dismiss_on_escape(mut self, dismiss: bool) -> Self {
self.config.dismiss_on_escape = dismiss;
self
}
pub fn animation(mut self, animation: OverlayAnimation) -> Self {
self.config.animation = animation;
self
}
pub fn motion_key(mut self, key: impl Into<String>) -> Self {
self.config.motion_key = Some(key.into());
self
}
pub fn edge_position(mut self, side: EdgeSide) -> Self {
self.config.position = OverlayPosition::Edge(side);
self
}
pub fn show(self) -> OverlayHandle {
let content = self.content.unwrap_or_else(|| Box::new(div));
self.manager.lock().unwrap().add(self.config, content)
}
}
pub struct DialogBuilder {
manager: OverlayManager,
config: OverlayConfig,
content: Option<Box<dyn Fn() -> Div + Send + Sync>>,
}
impl DialogBuilder {
fn new(manager: OverlayManager) -> Self {
Self {
manager,
config: OverlayConfig::dialog(),
content: None,
}
}
pub fn content<F>(mut self, f: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.content = Some(Box::new(f));
self
}
pub fn size(mut self, width: f32, height: f32) -> Self {
self.config.size = Some((width, height));
self
}
pub fn show(self) -> OverlayHandle {
let content = self.content.unwrap_or_else(|| Box::new(div));
self.manager.lock().unwrap().add(self.config, content)
}
}
pub struct ContextMenuBuilder {
manager: OverlayManager,
config: OverlayConfig,
content: Option<Box<dyn Fn() -> Div + Send + Sync>>,
}
impl ContextMenuBuilder {
fn new(manager: OverlayManager) -> Self {
Self {
manager,
config: OverlayConfig::context_menu(),
content: None,
}
}
pub fn at(mut self, x: f32, y: f32) -> Self {
self.config.position = OverlayPosition::AtPoint { x, y };
self
}
pub fn content<F>(mut self, f: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.content = Some(Box::new(f));
self
}
pub fn show(self) -> OverlayHandle {
let content = self.content.unwrap_or_else(|| Box::new(div));
self.manager.lock().unwrap().add(self.config, content)
}
}
pub struct ToastBuilder {
manager: OverlayManager,
config: OverlayConfig,
content: Option<Box<dyn Fn() -> Div + Send + Sync>>,
}
impl ToastBuilder {
fn new(manager: OverlayManager) -> Self {
Self {
manager,
config: OverlayConfig::toast(),
content: None,
}
}
pub fn duration_ms(mut self, ms: u32) -> Self {
self.config.auto_dismiss_ms = Some(ms);
self
}
pub fn corner(mut self, corner: Corner) -> Self {
self.config.position = OverlayPosition::Corner(corner);
self
}
pub fn content<F>(mut self, f: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.content = Some(Box::new(f));
self
}
pub fn motion_key(mut self, key: impl Into<String>) -> Self {
self.config.motion_key = Some(key.into());
self
}
pub fn show(self) -> OverlayHandle {
let content = self.content.unwrap_or_else(|| Box::new(div));
self.manager.lock().unwrap().add(self.config, content)
}
}
pub struct DropdownBuilder {
manager: OverlayManager,
config: OverlayConfig,
content: Option<Box<dyn Fn() -> Div + Send + Sync>>,
on_close: Option<OnCloseCallback>,
}
impl DropdownBuilder {
fn new(manager: OverlayManager) -> Self {
Self {
manager,
config: OverlayConfig::dropdown(),
content: None,
on_close: None,
}
}
fn new_hover_card(manager: OverlayManager) -> Self {
Self {
manager,
config: OverlayConfig::hover_card(),
content: None,
on_close: None,
}
}
pub fn at(mut self, x: f32, y: f32) -> Self {
self.config.position = OverlayPosition::AtPoint { x, y };
self
}
pub fn anchor(mut self, node: LayoutNodeId) -> Self {
self.config.position = OverlayPosition::RelativeToAnchor {
anchor: node,
offset_x: 0.0,
offset_y: 0.0,
};
self
}
pub fn offset(mut self, x: f32, y: f32) -> Self {
if let OverlayPosition::RelativeToAnchor {
offset_x, offset_y, ..
} = &mut self.config.position
{
*offset_x = x;
*offset_y = y;
}
self
}
pub fn dismiss_on_escape(mut self, dismiss: bool) -> Self {
self.config.dismiss_on_escape = dismiss;
self
}
pub fn dismiss_on_hover_leave(mut self, dismiss: bool) -> Self {
self.config.dismiss_on_hover_leave = dismiss;
if dismiss {
self.config.backdrop = None;
}
self
}
pub fn dismiss_on_click_outside(mut self, dismiss: bool) -> Self {
self.config.dismiss_on_click_outside = dismiss;
self
}
pub fn follows_scroll(mut self, follows: bool) -> Self {
self.config.follows_scroll = follows;
self
}
pub fn auto_dismiss(mut self, ms: Option<u32>) -> Self {
self.config.auto_dismiss_ms = ms;
self
}
pub fn close_delay(mut self, ms: Option<u32>) -> Self {
self.config.close_delay_ms = ms;
self
}
pub fn size(mut self, width: f32, height: f32) -> Self {
self.config.size = Some((width, height));
self
}
pub fn content<F>(mut self, f: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.content = Some(Box::new(f));
self
}
pub fn on_close<F>(mut self, f: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_close = Some(Arc::new(f));
self
}
pub fn motion_key(mut self, key: impl Into<String>) -> Self {
self.config.motion_key = Some(key.into());
self
}
pub fn anchor_direction(mut self, direction: AnchorDirection) -> Self {
self.config.anchor_direction = direction;
self
}
pub fn animation(mut self, animation: OverlayAnimation) -> Self {
self.config.animation = animation;
self
}
pub fn show(self) -> OverlayHandle {
let content = self.content.unwrap_or_else(|| Box::new(div));
self.manager
.lock()
.unwrap()
.add_with_close_callback(self.config, content, self.on_close)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_overlay_state_transitions() {
use overlay_events::*;
let mut state = OverlayState::Closed;
state = state.on_event(OPEN).unwrap();
assert_eq!(state, OverlayState::Opening);
state = state.on_event(ANIMATION_COMPLETE).unwrap();
assert_eq!(state, OverlayState::Open);
state = state.on_event(CLOSE).unwrap();
assert_eq!(state, OverlayState::Closing);
state = state.on_event(ANIMATION_COMPLETE).unwrap();
assert_eq!(state, OverlayState::Closed);
}
#[test]
fn test_overlay_manager_basic() {
let mgr = overlay_manager();
let handle = mgr.lock().unwrap().add(OverlayConfig::modal(), div);
assert!(mgr.lock().unwrap().has_visible_overlays());
mgr.close(handle);
assert!(mgr.lock().unwrap().has_visible_overlays());
}
#[test]
fn test_overlay_escape() {
let mgr = overlay_manager();
let _handle = {
let mut m = mgr.lock().unwrap();
let h = m.add(OverlayConfig::modal(), div);
if let Some(o) = m.overlays.get_mut(&h) {
o.state = OverlayState::Open;
}
h
};
assert!(mgr.handle_escape());
}
#[test]
fn test_overlay_config_defaults() {
let modal = OverlayConfig::modal();
assert!(modal.backdrop.is_some());
assert!(modal.dismiss_on_escape);
assert!(modal.focus_trap);
let toast = OverlayConfig::toast();
assert!(toast.backdrop.is_none());
assert!(!toast.dismiss_on_escape);
assert!(toast.auto_dismiss_ms.is_some());
let context = OverlayConfig::context_menu();
assert!(context.backdrop.is_none());
assert!(context.dismiss_on_escape);
}
}