use dioxus::prelude::*;
use freya_core::platform::CursorIcon;
use freya_elements::{
self as dioxus_elements,
Code,
KeyboardEvent,
};
use freya_hooks::{
use_applied_theme,
use_focus,
use_platform,
MenuContainerTheme,
MenuContainerThemeWith,
MenuItemTheme,
MenuItemThemeWith,
};
#[cfg_attr(feature = "docs",
doc = embed_doc_image::embed_image!("menu", "images/gallery_menu.png")
)]
#[component]
pub fn Menu(children: Element, onclose: Option<EventHandler<()>>) -> Element {
use_context_provider(|| Signal::new(ROOT_MENU.0));
use_context_provider::<Signal<Vec<MenuId>>>(|| Signal::new(vec![ROOT_MENU]));
use_context_provider(|| ROOT_MENU);
rsx!(
rect {
margin: "2 0",
onglobalclick: move |_| {
if let Some(onclose) = &onclose {
onclose.call(());
}
},
onglobalkeydown: move |ev| {
if ev.data.code == Code::Escape {
if let Some(onclose) = &onclose {
onclose.call(());
}
}
},
MenuContainer {
{children}
}
}
)
}
#[derive(Clone, Copy, PartialEq)]
struct MenuId(usize);
static ROOT_MENU: MenuId = MenuId(0);
fn close_menus_until(menus: &mut Signal<Vec<MenuId>>, until_to: MenuId) {
loop {
let last_menu_id = menus.read().last().cloned();
if let Some(last_menu_id) = last_menu_id {
if last_menu_id != until_to {
menus.write().pop();
} else {
break;
}
} else {
break;
}
}
}
fn push_menu(menus: &mut Signal<Vec<MenuId>>, menu_id: MenuId) {
let last_menu_id = menus.read().last().cloned();
if let Some(last_menu_id) = last_menu_id {
if last_menu_id != menu_id {
menus.write().push(menu_id)
}
} else {
menus.write().push(menu_id)
}
}
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum MenuItemStatus {
#[default]
Idle,
Hovering,
}
#[allow(non_snake_case)]
#[component]
pub fn MenuItem(
children: Element,
theme: Option<MenuItemThemeWith>,
onpress: Option<EventHandler<()>>,
onmouseenter: Option<EventHandler<()>>,
) -> Element {
let mut focus = use_focus();
let mut status = use_signal(MenuItemStatus::default);
let platform = use_platform();
let a11y_id = focus.attribute();
let MenuItemTheme {
hover_background,
corner_radius,
font_theme,
} = use_applied_theme!(&theme, menu_item);
use_drop(move || {
if *status.read() == MenuItemStatus::Hovering {
platform.set_cursor(CursorIcon::default());
}
});
let onclick = move |_| {
focus.request_focus();
if let Some(onpress) = &onpress {
onpress.call(())
}
};
let onkeydown = move |ev: KeyboardEvent| {
if focus.validate_keydown(&ev) {
if let Some(onpress) = &onpress {
onpress.call(())
}
}
};
let onmouseenter = move |_| {
platform.set_cursor(CursorIcon::Pointer);
status.set(MenuItemStatus::Hovering);
if let Some(onmouseenter) = &onmouseenter {
onmouseenter.call(());
}
};
let onmouseleave = move |_| {
platform.set_cursor(CursorIcon::default());
status.set(MenuItemStatus::default());
};
let background = match *status.read() {
_ if focus.is_focused_with_keyboard() => &hover_background,
MenuItemStatus::Hovering => &hover_background,
MenuItemStatus::Idle => "transparent",
};
rsx!(
rect {
onclick,
onkeydown,
onmouseenter,
onmouseleave,
a11y_id,
min_width: "110",
width: "fill-min",
padding: "6 12",
margin: "2",
a11y_role: "button",
color: "{font_theme.color}",
corner_radius: "{corner_radius}",
background: "{background}",
text_align: "start",
main_align: "center",
{children}
}
)
}
#[allow(non_snake_case)]
#[component]
pub fn SubMenu(
menu: Element,
children: Element,
) -> Element {
let parent_menu_id = use_context::<MenuId>();
let mut menus = use_context::<Signal<Vec<MenuId>>>();
let mut menus_ids_generator = use_context::<Signal<usize>>();
let submenu_id = use_hook(|| {
menus_ids_generator += 1;
provide_context(MenuId(*menus_ids_generator.peek()))
});
let show_submenu = menus.read().contains(&submenu_id);
rsx!(
MenuItem {
onmouseenter: move |_| {
close_menus_until(&mut menus, parent_menu_id);
push_menu(&mut menus, submenu_id);
},
onpress: move |_| {
close_menus_until(&mut menus, parent_menu_id);
push_menu(&mut menus, submenu_id);
},
{children}
if show_submenu {
rect {
position_top: "-12",
position_right: "-20",
position: "absolute",
width: "0",
height: "0",
rect {
width: "100v",
MenuContainer {
{menu}
}
}
}
}
}
)
}
#[allow(non_snake_case)]
#[component]
pub fn MenuButton(
children: Element,
onpress: Option<EventHandler<()>>,
) -> Element {
let mut menus = use_context::<Signal<Vec<MenuId>>>();
let parent_menu_id = use_context::<MenuId>();
rsx!(
MenuItem {
onmouseenter: move |_| close_menus_until(&mut menus, parent_menu_id),
onpress: move |_| {
if let Some(onpress) = &onpress {
onpress.call(())
}
},
{children}
}
)
}
#[allow(non_snake_case)]
#[component]
pub fn MenuContainer(
children: Element,
theme: Option<MenuContainerThemeWith>,
) -> Element {
let MenuContainerTheme {
background,
padding,
shadow,
border_fill,
corner_radius,
} = use_applied_theme!(&theme, menu_container);
rsx!(
rect {
background: "{background}",
corner_radius: "{corner_radius}",
shadow: "{shadow}",
padding: "{padding}",
content: "fit",
border: "1 inner {border_fill}",
{children}
}
)
}
#[cfg(test)]
mod test {
use dioxus::prelude::use_signal;
use freya::prelude::*;
use freya_testing::prelude::*;
#[tokio::test]
pub async fn menu() {
fn menu_app() -> Element {
let mut show_menu = use_signal(|| false);
rsx!(
Body {
Button {
onpress: move |_| show_menu.toggle(),
label { "Open Menu" }
}
if *show_menu.read() {
Menu {
onclose: move |_| show_menu.set(false),
MenuButton {
label {
"Open"
}
}
MenuButton {
label {
"Save"
}
}
SubMenu {
menu: rsx!(
MenuButton {
label {
"Option 1"
}
}
SubMenu {
menu: rsx!(
MenuButton {
label {
"Option 3"
}
}
),
label {
"More Options"
}
}
),
label {
"Options"
}
}
MenuButton {
label {
"Close"
}
}
}
}
}
)
}
let mut utils = launch_test(menu_app);
utils.wait_for_update().await;
let start_size = utils.sdom().get().layout().size();
assert_eq!(utils.sdom().get().layout().size(), 5);
utils.click_cursor((15., 15.)).await;
assert_eq!(
utils
.root()
.get(0)
.get(1)
.get(0)
.get(0)
.get(0)
.get(0)
.text(),
Some("Open")
);
assert!(utils.sdom().get().layout().size() > start_size);
utils.click_cursor((15., 60.)).await;
assert_eq!(utils.sdom().get().layout().size(), start_size);
utils.click_cursor((15., 15.)).await;
let one_submenu_opened = utils.sdom().get().layout().size();
assert!(one_submenu_opened > start_size);
utils.move_cursor((15., 130.)).await;
assert_eq!(
utils
.root()
.get(0)
.get(1)
.get(0)
.get(2)
.get(1)
.get(0)
.get(0)
.get(0)
.get(0)
.get(0)
.text(),
Some("Option 1")
);
assert!(utils.sdom().get().layout().size() > one_submenu_opened);
utils.move_cursor((15., 90.)).await;
assert_eq!(utils.sdom().get().layout().size(), one_submenu_opened);
utils.click_cursor((333., 333.)).await;
assert_eq!(utils.sdom().get().layout().size(), start_size);
}
}