use std::cell::OnceCell;
use std::ops::{Deref, DerefMut};
use std::sync::Arc;
use blinc_core::context_state::BlincContextState;
use blinc_core::reactive::{ReactiveGraph, computed};
use blinc_core::{Signal, State};
use blinc_layout::InstanceKey;
use blinc_layout::div::{Div, ElementBuilder, ElementTypeId};
use blinc_layout::element::CursorStyle;
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, ThemeState};
use crate::reactive_props::clone_reactive;
use blinc_layout::binding::{IntoReactive, Reactive};
#[derive(Clone)]
enum NavMenuItem {
Link {
label: String,
on_click: Arc<dyn Fn() + Send + Sync>,
},
Trigger {
label: String,
content: Arc<dyn Fn() -> Div + Send + Sync>,
},
}
pub struct NavigationMenu {
inner: Div,
}
impl NavigationMenu {
fn from_builder(builder: &NavigationMenuBuilder) -> Self {
let key_base = builder.key.get().to_string();
let items = builder.items.clone();
let min_content_width = builder.min_content_width;
let active_menu: Signal<Option<usize>> =
BlincContextState::get().use_signal_keyed(&format!("{}_active", key_base), || None);
let overlay_handle_state: Signal<Option<u64>> =
BlincContextState::get().use_signal_keyed(&format!("{}_handle", key_base), || None);
let mut nav = div()
.class("cn-nav-menu")
.flex_row()
.items_center()
.h_fit()
.gap(1.0);
for (idx, item) in items.iter().enumerate() {
let interaction = Interaction::keyed(&format!("{}_{}_btn", key_base, idx));
let label = match item {
NavMenuItem::Link { label, .. } | NavMenuItem::Trigger { label, .. } => {
label.clone()
}
};
let is_trigger = matches!(item, NavMenuItem::Trigger { .. });
let active = active_menu;
let open = computed(move |g: &ReactiveGraph| {
is_trigger && g.get(active).flatten() == Some(idx)
});
let mut entry = div()
.class("cn-nav-link")
.class("tabindex=0")
.class_when("cn-nav-link--active", &open)
.flex_row()
.items_center()
.h_fit()
.flex_shrink_0()
.gap(1.0)
.px(3.0)
.py(2.0)
.track(&interaction)
.child(
text(&label)
.size(14.0)
.medium()
.no_wrap()
.no_cursor()
.pointer_events_none(),
);
match item {
NavMenuItem::Link { on_click, .. } => {
let on_click = on_click.clone();
let active_menu_for_click = active_menu;
let overlay_handle_for_click = overlay_handle_state;
entry = entry.on_click(move |_| {
if let Some(handle_id) = overlay_handle_for_click.get() {
OverlayHandle::from_raw(handle_id).close();
}
active_menu_for_click.set(None);
on_click();
});
}
NavMenuItem::Trigger { content, .. } => {
let content = content.clone();
let menu_key = format!("{}_menu_{}", key_base, idx);
let active_menu_for_hover = active_menu;
let overlay_handle_for_hover = overlay_handle_state;
let overlay_handle_for_leave = overlay_handle_state;
let chevron = r#"<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>"#;
entry = entry
.child(
div()
.pointer_events_none()
.child(svg(chevron).size(12.0, 12.0)),
)
.on_hover_enter(move |ctx| {
if active_menu_for_hover.get() == Some(idx) {
if let Some(handle_id) = overlay_handle_for_hover.get() {
let handle = OverlayHandle::from_raw(handle_id);
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);
}
}
}
return;
}
if let Some(handle_id) = overlay_handle_for_hover.get() {
OverlayHandle::from_raw(handle_id).close();
}
let x = ctx.bounds_x;
let y = ctx.bounds_y + ctx.bounds_height + 4.0;
let handle = show_navigation_dropdown(
x,
y,
content.clone(),
min_content_width,
active_menu_for_hover,
overlay_handle_for_hover,
menu_key.clone(),
);
overlay_handle_for_hover.set(Some(handle.raw()));
active_menu_for_hover.set(Some(idx));
})
.on_hover_leave(move |_| {
if let Some(handle_id) = overlay_handle_for_leave.get() {
let handle = OverlayHandle::from_raw(handle_id);
if let Ok(mut stack) = overlay_stack().lock() {
stack.handle_mouse_leave(handle);
}
}
});
}
}
nav = nav.child(entry);
}
for c in &builder.classes {
nav = nav.class(c);
}
if let Some(ref id) = builder.user_id {
nav = nav.id(id);
}
Self { inner: nav }
}
}
#[allow(clippy::too_many_arguments)]
fn show_navigation_dropdown(
x: f32,
y: f32,
content: Arc<dyn Fn() -> Div + Send + Sync>,
min_width: f32,
active_menu_state: Signal<Option<usize>>,
overlay_handle_state: Signal<Option<u64>>,
_key: String,
) -> OverlayHandle {
let active_menu_for_close = active_menu_state;
let overlay_handle_for_close = overlay_handle_state;
let next_handle_id = overlay_stack()
.lock()
.ok()
.map(|s| s.peek_next_handle_id())
.unwrap_or(0);
let dropdown_handle = OverlayHandle::from_raw(next_handle_id);
let handle = OverlayBuilder::tooltip()
.at(x, y)
.size(720.0, 480.0)
.anchor_direction(AnchorDirection::Bottom)
.dismissable_by_mouse_leave(true, 300)
.dismissable_by_escape(true)
.on_close(move |_reason| {
active_menu_for_close.set(None);
overlay_handle_for_close.set(None);
})
.content(move || {
let user_content = content();
div()
.class("cn-nav-menu-content")
.min_w(min_width / 4.0)
.overflow_clip()
.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 == dropdown_handle && e.exiting);
if exiting {
stack.revive(dropdown_handle);
} else {
stack.handle_mouse_enter(dropdown_handle);
}
}
})
.on_hover_leave(move |_| {
if let Ok(mut stack) = overlay_stack().lock() {
stack.handle_mouse_leave(dropdown_handle);
}
})
})
.show();
debug_assert_eq!(
handle.raw(),
next_handle_id,
"peek_next_handle_id was stale — concurrent push?"
);
handle
}
impl Deref for NavigationMenu {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for NavigationMenu {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for NavigationMenu {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
fn layout_style(&self) -> Option<&taffy::Style> {
ElementBuilder::layout_style(&self.inner)
}
fn element_type_id(&self) -> ElementTypeId {
ElementBuilder::element_type_id(&self.inner)
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.inner.element_classes()
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
}
pub struct NavigationMenuBuilder {
key: InstanceKey,
items: Vec<NavMenuItem>,
min_content_width: f32,
classes: Vec<std::sync::Arc<str>>,
user_id: Option<String>,
built: OnceCell<NavigationMenu>,
}
impl NavigationMenuBuilder {
#[track_caller]
pub fn new() -> Self {
Self {
key: InstanceKey::new("nav_menu"),
items: Vec::new(),
min_content_width: 50.0, classes: Vec::new(),
user_id: None,
built: OnceCell::new(),
}
}
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) -> &NavigationMenu {
::blinc_layout::build_once::build_once(&self.built, || NavigationMenu::from_builder(self))
}
pub fn item<F>(mut self, label: impl Into<String>, on_click: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.items.push(NavMenuItem::Link {
label: label.into(),
on_click: Arc::new(on_click),
});
self
}
pub fn trigger<F>(mut self, label: impl Into<String>, content: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.items.push(NavMenuItem::Trigger {
label: label.into(),
content: Arc::new(content),
});
self
}
pub fn min_content_width(mut self, width: f32) -> Self {
self.min_content_width = width;
self
}
}
impl Default for NavigationMenuBuilder {
fn default() -> Self {
Self::new()
}
}
impl ElementBuilder for NavigationMenuBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
self.get_or_build().event_handlers()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn element_type_id(&self) -> ElementTypeId {
self.get_or_build().element_type_id()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.get_or_build().element_classes()
}
fn element_id(&self) -> Option<&str> {
self.get_or_build().element_id()
}
}
pub struct NavigationLink {
inner: Div,
}
impl NavigationLink {
fn from_builder(builder: &NavigationLinkBuilder) -> Self {
let label = builder.label.clone();
let description = builder.description.as_ref().map(clone_reactive);
let on_click = builder.on_click.clone();
let mut link = div()
.class("cn-nav-link")
.class("tabindex=0")
.flex_col()
.w_full()
.gap(1.0)
.px(3.0)
.py(2.0)
.child(
text(&label)
.size(14.0)
.medium()
.no_cursor()
.pointer_events_none(),
);
if let Some(ref desc) = description {
link = link.child(
blinc_layout::text::Text::bound(clone_reactive(desc))
.class("cn-nav-link-description")
.size(12.0)
.no_cursor()
.pointer_events_none(),
);
}
link = link.on_click(move |_| {
if let Some(ref cb) = on_click {
cb();
}
});
for c in &builder.classes {
link = link.class(c);
}
if let Some(ref id) = builder.user_id {
link = link.id(id);
}
Self { inner: link }
}
}
impl Deref for NavigationLink {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for NavigationLink {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for NavigationLink {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
fn layout_style(&self) -> Option<&taffy::Style> {
ElementBuilder::layout_style(&self.inner)
}
fn element_type_id(&self) -> ElementTypeId {
ElementBuilder::element_type_id(&self.inner)
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.inner.element_classes()
}
fn element_id(&self) -> Option<&str> {
self.inner.element_id()
}
}
pub struct NavigationLinkBuilder {
label: String,
description: Option<Reactive<String>>,
on_click: Option<Arc<dyn Fn() + Send + Sync>>,
classes: Vec<std::sync::Arc<str>>,
user_id: Option<String>,
built: OnceCell<NavigationLink>,
}
impl NavigationLinkBuilder {
#[track_caller]
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
description: None,
on_click: None,
classes: Vec::new(),
user_id: None,
built: OnceCell::new(),
}
}
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) -> &NavigationLink {
::blinc_layout::build_once::build_once(&self.built, || NavigationLink::from_builder(self))
}
pub fn description(mut self, desc: impl IntoReactive<String>) -> Self {
self.description = Some(desc.into_reactive());
self
}
pub fn on_click<F>(mut self, handler: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_click = Some(Arc::new(handler));
self
}
}
impl ElementBuilder for NavigationLinkBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
self.get_or_build().event_handlers()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn element_type_id(&self) -> ElementTypeId {
self.get_or_build().element_type_id()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.get_or_build().element_classes()
}
fn element_id(&self) -> Option<&str> {
self.get_or_build().element_id()
}
}
#[track_caller]
pub fn navigation_menu() -> NavigationMenuBuilder {
NavigationMenuBuilder::new()
}
#[track_caller]
pub fn navigation_link(label: impl Into<String>) -> NavigationLinkBuilder {
NavigationLinkBuilder::new(label)
}
#[cfg(test)]
mod tests {
use super::*;
fn init_theme() {
let _ = ThemeState::try_get().unwrap_or_else(|| {
ThemeState::init_default();
ThemeState::get()
});
}
#[test]
fn test_navigation_menu_basic() {
init_theme();
let _ = navigation_menu()
.item("Home", || {})
.trigger("Products", div);
}
#[test]
fn test_navigation_link() {
init_theme();
let _ = navigation_link("Test")
.description("A test link")
.on_click(|| {});
}
}