use std::cell::OnceCell;
use std::sync::Arc;
use blinc_core::context_state::BlincContextState;
use blinc_core::{Signal, State};
use blinc_layout::div::ElementTypeId;
use blinc_layout::element::RenderProps;
use blinc_layout::overlay_state::overlay_stack;
use blinc_layout::prelude::*;
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_layout::widgets::overlay::AnchorDirection;
use blinc_layout::widgets::overlay_stack::{OverlayBuilder, OverlayHandle};
use blinc_theme::{ColorToken, RadiusToken, SpacingToken, ThemeState};
use blinc_layout::InstanceKey;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum HoverCardSide {
Top,
#[default]
Bottom,
Right,
Left,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum HoverCardAlign {
#[default]
Start,
Center,
End,
}
type ContentBuilderFn = Arc<dyn Fn() -> Div + Send + Sync>;
type TriggerBuilderFn = Arc<dyn Fn() -> Div + Send + Sync>;
pub struct HoverCardBuilder {
trigger: TriggerBuilderFn,
content: Option<ContentBuilderFn>,
side: HoverCardSide,
align: HoverCardAlign,
open_delay_ms: u32,
close_delay_ms: u32,
offset: f32,
key: InstanceKey,
classes: Vec<std::sync::Arc<str>>,
user_id: Option<String>,
built: OnceCell<HoverCard>,
}
impl std::fmt::Debug for HoverCardBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HoverCardBuilder")
.field("side", &self.side)
.field("align", &self.align)
.field("open_delay_ms", &self.open_delay_ms)
.field("close_delay_ms", &self.close_delay_ms)
.field("offset", &self.offset)
.finish()
}
}
impl HoverCardBuilder {
pub fn with_key<F>(trigger_fn: F, key: InstanceKey) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
Self {
trigger: Arc::new(trigger_fn),
content: None,
side: HoverCardSide::Bottom,
align: HoverCardAlign::Start,
open_delay_ms: 500, close_delay_ms: 300, offset: 8.0,
key,
classes: Vec::new(),
user_id: None,
built: OnceCell::new(),
}
}
pub fn content<F>(mut self, content_fn: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.content = Some(Arc::new(content_fn));
self
}
pub fn side(mut self, side: HoverCardSide) -> Self {
self.side = side;
self
}
pub fn align(mut self, align: HoverCardAlign) -> Self {
self.align = align;
self
}
pub fn open_delay_ms(mut self, delay: u32) -> Self {
self.open_delay_ms = delay;
self
}
pub fn close_delay_ms(mut self, delay: u32) -> Self {
self.close_delay_ms = delay;
self
}
pub fn offset(mut self, offset: f32) -> Self {
self.offset = offset;
self
}
pub fn class(mut self, name: impl AsRef<str>) -> Self {
self.classes.push(blinc_core::intern::intern(name.as_ref()));
self
}
pub fn id(mut self, id: &str) -> Self {
self.user_id = Some(id.to_string());
self
}
fn get_or_build(&self) -> &HoverCard {
::blinc_layout::build_once::build_once(&self.built, || self.build_component())
}
fn build_component(&self) -> HoverCard {
let overlay_handle_state: Signal<Option<u64>> =
BlincContextState::get().use_signal_keyed(&self.key.derive("handle"), || None);
let side = self.side;
let align = self.align;
let offset = self.offset;
let close_delay_ms = self.close_delay_ms;
let content_builder = self.content.clone();
let trigger_builder = self.trigger.clone();
let stored_for_enter = overlay_handle_state;
let stored_for_leave = overlay_handle_state;
let stored_for_open = overlay_handle_state;
let trigger_content = (trigger_builder)();
let trigger = div()
.w_fit()
.child(trigger_content)
.on_hover_enter(move |ctx| {
if let Some(raw) = stored_for_enter.get() {
let handle = OverlayHandle::from_raw(raw);
if handle.is_live() {
if let Ok(mut stack) = overlay_stack().lock() {
stack.handle_mouse_enter(handle);
}
return;
}
if handle.is_exiting() {
if let Ok(mut stack) = overlay_stack().lock() {
stack.revive(handle);
}
return;
}
stored_for_enter.set(None);
}
let Some(ref content_fn) = content_builder else {
return;
};
let (x, y) = calculate_hover_card_position(
ctx.bounds_x,
ctx.bounds_y,
ctx.bounds_width,
ctx.bounds_height,
side,
align,
offset,
);
let content_fn = Arc::clone(content_fn);
let stored_close = stored_for_open;
let handle =
build_hover_card_overlay(x, y, side, content_fn, close_delay_ms, stored_close);
stored_for_open.set(Some(handle.raw()));
})
.on_hover_leave(move |_| {
if let Some(raw) = stored_for_leave.get() {
let handle = OverlayHandle::from_raw(raw);
if handle.is_live() {
if let Ok(mut stack) = overlay_stack().lock() {
stack.handle_mouse_leave(handle);
}
}
}
});
let mut inner = trigger;
for c in &self.classes {
inner = inner.class(c);
}
if let Some(ref id) = self.user_id {
inner = inner.id(id);
}
HoverCard { inner }
}
}
fn calculate_hover_card_position(
trigger_x: f32,
trigger_y: f32,
trigger_w: f32,
trigger_h: f32,
side: HoverCardSide,
align: HoverCardAlign,
offset: f32,
) -> (f32, f32) {
let card_width_estimate = 280.0;
match side {
HoverCardSide::Top => {
let y = trigger_y - (offset * 8.0);
let x = match align {
HoverCardAlign::Start => trigger_x,
HoverCardAlign::Center => trigger_x + (trigger_w - card_width_estimate) / 2.0,
HoverCardAlign::End => trigger_x + trigger_w - card_width_estimate,
};
(x.max(0.0), y.max(0.0))
}
HoverCardSide::Bottom => {
let y = trigger_y + trigger_h + offset;
let x = match align {
HoverCardAlign::Start => trigger_x,
HoverCardAlign::Center => trigger_x + (trigger_w - card_width_estimate) / 2.0,
HoverCardAlign::End => trigger_x + trigger_w - card_width_estimate,
};
(x.max(0.0), y)
}
HoverCardSide::Right => {
let x = trigger_x + trigger_w + offset;
let y = match align {
HoverCardAlign::Start => trigger_y,
HoverCardAlign::Center => trigger_y,
HoverCardAlign::End => trigger_y,
};
(x, y)
}
HoverCardSide::Left => {
let x = trigger_x - card_width_estimate - offset;
let y = match align {
HoverCardAlign::Start => trigger_y,
HoverCardAlign::Center => trigger_y,
HoverCardAlign::End => trigger_y,
};
(x.max(0.0), y)
}
}
}
fn build_hover_card_overlay(
x: f32,
y: f32,
side: HoverCardSide,
content_fn: ContentBuilderFn,
close_delay_ms: u32,
overlay_handle_state: Signal<Option<u64>>,
) -> OverlayHandle {
let padding = ThemeState::get().spacing_value(SpacingToken::Space4);
let anchor_dir = match side {
HoverCardSide::Top => AnchorDirection::Top,
HoverCardSide::Bottom => AnchorDirection::Bottom,
HoverCardSide::Left => AnchorDirection::Left,
HoverCardSide::Right => AnchorDirection::Right,
};
let next_handle_id = overlay_stack()
.lock()
.ok()
.map(|s| s.peek_next_handle_id())
.unwrap_or(0);
let stored_close = overlay_handle_state;
let next_id_for_card = next_handle_id;
let handle = OverlayBuilder::tooltip()
.at(x, y)
.size(360.0, 400.0)
.anchor_direction(anchor_dir)
.dismissable_by_mouse_leave(true, close_delay_ms)
.on_close(move |_reason| {
stored_close.set(None);
})
.content(move || {
let user_content = (content_fn)();
let handle = OverlayHandle::from_raw(next_id_for_card);
div()
.class("cn-hover-card-content")
.flex_col()
.p_px(padding)
.min_w(200.0)
.max_w(320.0)
.child(user_content)
.on_hover_enter(move |_| {
if let Ok(mut stack) = overlay_stack().lock() {
let exiting = stack
.iter_bottom_up()
.any(|e| e.handle == handle && e.exiting);
if exiting {
stack.revive(handle);
} else {
stack.handle_mouse_enter(handle);
}
}
})
.on_hover_leave(move |_| {
if let Ok(mut stack) = overlay_stack().lock() {
stack.handle_mouse_leave(handle);
}
})
})
.show();
debug_assert_eq!(
handle.raw(),
next_handle_id,
"peek_next_handle_id was stale — concurrent push?"
);
handle
}
pub struct HoverCard {
inner: Div,
}
impl std::ops::Deref for HoverCard {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for HoverCard {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for HoverCardBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.get_or_build().inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().inner.children_builders()
}
fn element_type_id(&self) -> ElementTypeId {
self.get_or_build().inner.element_type_id()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.get_or_build().inner)
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().inner.layout_style()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.get_or_build().inner.element_classes()
}
fn element_id(&self) -> Option<&str> {
self.get_or_build().inner.element_id()
}
}
impl ElementBuilder for HoverCard {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> ElementTypeId {
self.inner.element_type_id()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.inner.element_classes()
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
}
#[track_caller]
pub fn hover_card<F>(trigger_fn: F) -> HoverCardBuilder
where
F: Fn() -> Div + Send + Sync + 'static,
{
let key = InstanceKey::new("hover_card");
HoverCardBuilder::with_key(trigger_fn, key)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hover_card_position_bottom() {
let (x, y) = calculate_hover_card_position(
100.0,
50.0,
80.0,
30.0,
HoverCardSide::Bottom,
HoverCardAlign::Start,
8.0,
);
assert_eq!(x, 100.0);
assert_eq!(y, 50.0 + 30.0 + 8.0); }
#[test]
fn test_hover_card_position_right() {
let (x, y) = calculate_hover_card_position(
100.0,
50.0,
80.0,
30.0,
HoverCardSide::Right,
HoverCardAlign::Start,
8.0,
);
assert_eq!(x, 100.0 + 80.0 + 8.0); assert_eq!(y, 50.0);
}
}