use crate::icon::{Icon, IconKind};
use dioxus::prelude::*;
#[derive(Clone, Copy, PartialEq)]
pub struct ContextMenuContext {
pub is_open: Signal<bool>,
pub coords: Signal<(f64, f64)>,
}
#[component]
pub fn ContextMenu(
#[props(default)]
class: String,
children: Element,
) -> Element {
let is_open = use_signal(|| false);
let coords = use_signal(|| (0.0, 0.0));
use_context_provider(|| ContextMenuContext { is_open, coords });
rsx! {
div {
class: "sd-context-menu-wrapper {class}",
{children}
}
}
}
#[component]
pub fn ContextMenuTrigger(
#[props(default)]
class: String,
children: Element,
) -> Element {
let mut ctx = use_context::<ContextMenuContext>();
rsx! {
div {
class: "sd-context-menu-trigger-wrapper {class}",
oncontextmenu: move |e| {
e.prevent_default();
let mouse_coords = e.client_coordinates();
ctx.coords.set((mouse_coords.x, mouse_coords.y));
ctx.is_open.set(true);
},
{children}
}
}
}
#[component]
pub fn ContextMenuContent(
#[props(default)]
class: String,
#[props(default)]
glow: Option<bool>,
children: Element,
) -> Element {
let mut ctx = use_context::<ContextMenuContext>();
let is_open = *ctx.is_open.read();
if !is_open {
return rsx! {};
}
let (x, y) = *ctx.coords.read();
let glow_class = if glow == Some(true) {
"sd-context-menu-content--glow"
} else {
""
};
#[cfg(feature = "theme")]
let theme_val = crate::theme::use_theme()();
#[cfg(feature = "theme")]
let base_override = theme_val.resolve_component_base("context_menu").unwrap_or("");
#[cfg(not(feature = "theme"))]
let base_override = "";
rsx! {
div {
class: "sd-context-menu-backdrop",
onclick: move |_| {
ctx.is_open.set(false);
},
oncontextmenu: move |e| {
e.prevent_default();
ctx.is_open.set(false);
},
}
div {
role: "menu",
tabindex: "-1",
class: "sd-context-menu-content {glow_class} {base_override} {class}",
style: "left: {x}px; top: {y}px;",
onkeydown: move |e: KeyboardEvent| {
if e.key() == Key::Escape {
ctx.is_open.set(false);
}
},
{children}
}
}
}
#[component]
pub fn ContextMenuItem(
label: String,
#[props(default)]
icon: Option<IconKind>,
#[props(default)]
shortcut: Option<String>,
#[props(default)]
disabled: bool,
#[props(default)]
danger: bool,
#[props(default)]
onclick: Option<EventHandler<()>>,
#[props(default)]
class: String,
) -> Element {
let mut ctx = use_context::<ContextMenuContext>();
let disabled_class = if disabled {
"sd-context-menu-item--disabled"
} else {
""
};
let danger_class = if danger {
"sd-context-menu-item--danger"
} else {
""
};
rsx! {
button {
r#type: "button",
role: "menuitem",
disabled: disabled,
class: "sd-context-menu-item {disabled_class} {danger_class} {class}",
onclick: move |_| {
if !disabled {
if let Some(h) = onclick {
h.call(());
}
ctx.is_open.set(false);
}
},
div { class: "flex items-center gap-2",
if let Some(ic) = icon {
Icon { kind: ic, size: 14 }
}
span { "{label}" }
}
if let Some(sc) = shortcut {
span { class: "sd-context-menu-shortcut", "{sc}" }
}
}
}
}
#[component]
pub fn ContextMenuSeparator() -> Element {
rsx! {
div { class: "sd-context-menu-separator" }
}
}
#[component]
pub fn ContextMenuCheckboxItem(
checked: bool,
label: String,
#[props(default)]
shortcut: Option<String>,
#[props(default)]
onchange: Option<EventHandler<bool>>,
#[props(default)]
class: String,
) -> Element {
let mut ctx = use_context::<ContextMenuContext>();
rsx! {
button {
r#type: "button",
role: "menuitemcheckbox",
"aria-checked": "{checked}",
class: "sd-context-menu-item {class}",
onclick: move |_| {
if let Some(h) = onchange {
h.call(!checked);
}
ctx.is_open.set(false);
},
div { class: "flex items-center gap-2",
span { class: "w-4 flex items-center justify-center text-purple-400",
if checked {
Icon { kind: IconKind::Check, size: 13 }
}
}
span { "{label}" }
}
if let Some(sc) = shortcut {
span { class: "sd-context-menu-shortcut", "{sc}" }
}
}
}
}