use crate::breakpoint::{
BreakpointConfig, DesktopSidebar, MobileSidebar, SheetSnap, ShellBreakpoint,
use_shell_breakpoint,
};
use crate::{ShellContext, use_shell_context};
use dioxus::prelude::*;
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ShellLayout {
#[default]
Horizontal,
Vertical,
Sidebar,
}
impl ShellLayout {
pub fn as_data_attr(&self) -> &'static str {
match self {
Self::Horizontal => "horizontal",
Self::Vertical => "vertical",
Self::Sidebar => "sidebar",
}
}
}
impl fmt::Display for ShellLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_data_attr())
}
}
#[derive(Clone, Copy)]
struct ShellSignals {
layout: Signal<ShellLayout>,
sidebar_visible: Signal<bool>,
sidebar_mobile_open: Signal<bool>,
mobile_sidebar: Signal<MobileSidebar>,
desktop_sidebar: Signal<DesktopSidebar>,
stack_depth: Signal<u32>,
modal_open: Signal<bool>,
search_active: Signal<bool>,
sheet_snap: Signal<SheetSnap>,
on_modal_change: Signal<Option<EventHandler<bool>>>,
on_search_change: Signal<Option<EventHandler<bool>>>,
}
fn use_shell_signals(
layout: ShellLayout,
mobile_sidebar: MobileSidebar,
desktop_sidebar: DesktopSidebar,
) -> ShellSignals {
ShellSignals {
layout: use_signal(|| layout),
sidebar_visible: use_signal(|| true),
sidebar_mobile_open: use_signal(|| false),
mobile_sidebar: use_signal(|| mobile_sidebar),
desktop_sidebar: use_signal(|| desktop_sidebar),
stack_depth: use_signal(|| 1u32),
modal_open: use_signal(|| false),
search_active: use_signal(|| false),
sheet_snap: use_signal(|| SheetSnap::Hidden),
on_modal_change: use_signal(|| None),
on_search_change: use_signal(|| None),
}
}
#[component]
pub fn AppShell(
children: Element,
#[props(default)]
sidebar: Option<Element>,
#[props(default)]
preview: Option<Element>,
#[props(default)]
footer: Option<Element>,
#[props(default)]
layout: ShellLayout,
#[props(default)]
mobile_sidebar: MobileSidebar,
#[props(default)]
desktop_sidebar: DesktopSidebar,
#[props(default)]
breakpoints: BreakpointConfig,
#[props(default)]
external_breakpoint: Option<ReadSignal<ShellBreakpoint>>,
#[props(default)]
class: Option<String>,
#[props(into, default = "complementary".to_string())]
sidebar_role: String,
#[props(into, default = "Preview".to_string())]
preview_label: String,
#[props(default)]
tabs: Option<Element>,
#[props(default)]
sheet: Option<Element>,
#[props(default)]
modal: Option<Element>,
#[props(default)]
fab: Option<Element>,
#[props(default)]
action_bar: Option<Element>,
#[props(default)]
search: Option<Element>,
#[props(default)]
modal_open: Option<ReadSignal<bool>>,
#[props(default)]
on_modal_change: Option<EventHandler<bool>>,
#[props(default)]
search_active: Option<ReadSignal<bool>>,
#[props(default)]
on_search_change: Option<EventHandler<bool>>,
#[props(default)]
additional_attributes: Vec<Attribute>,
) -> Element {
let signals = use_shell_signals(layout, mobile_sidebar, desktop_sidebar);
let runtime_bp = use_shell_breakpoint(breakpoints.compact_below, breakpoints.expanded_above);
let breakpoint = external_breakpoint.unwrap_or(runtime_bp);
if *signals.mobile_sidebar.peek() != mobile_sidebar {
let mut s = signals.mobile_sidebar;
s.set(mobile_sidebar);
}
if *signals.desktop_sidebar.peek() != desktop_sidebar {
let mut s = signals.desktop_sidebar;
s.set(desktop_sidebar);
}
use_effect(move || {
if let Some(controlled) = modal_open {
let mut s = signals.modal_open;
s.set(controlled());
}
});
use_effect(move || {
if let Some(controlled) = search_active {
let mut s = signals.search_active;
s.set(controlled());
}
});
{
let mut s = signals.on_modal_change;
s.set(on_modal_change);
}
{
let mut s = signals.on_search_change;
s.set(on_search_change);
}
let ctx = use_context_provider(|| ShellContext {
layout: signals.layout,
breakpoint,
sidebar_visible: signals.sidebar_visible,
sidebar_mobile_open: signals.sidebar_mobile_open,
mobile_sidebar: signals.mobile_sidebar.into(),
desktop_sidebar: signals.desktop_sidebar.into(),
stack_depth: signals.stack_depth,
modal_open: signals.modal_open,
search_active: signals.search_active,
sheet_snap: signals.sheet_snap,
on_modal_change: signals.on_modal_change,
on_search_change: signals.on_search_change,
});
let is_mobile = (breakpoint)().is_compact();
let mobile_sidebar_val = mobile_sidebar;
let desktop_sidebar_val = desktop_sidebar;
let has_sidebar = sidebar.is_some();
let sidebar_for_desktop = sidebar.clone();
let derived_columns: &'static str = if is_mobile || !has_sidebar || !(signals.sidebar_visible)()
{
"1"
} else {
"2"
};
rsx! {
div {
class: class.unwrap_or_default(),
"data-shell": "",
"data-shell-layout": (ctx.layout)().as_data_attr(),
"data-shell-breakpoint": (breakpoint)().as_str(),
"data-shell-sidebar-state": ctx.sidebar_state(),
"data-shell-columns": derived_columns,
"data-shell-display-mode": if is_mobile { "stack" } else { "side-by-side" },
"data-shell-stack-depth": (signals.stack_depth)().to_string(),
"data-shell-can-go-back": ((signals.stack_depth)() > 1).to_string(),
"data-shell-search-active": (signals.search_active)().to_string(),
"data-shell-modal-state": if (signals.modal_open)() { "presented" } else { "dismissed" },
..additional_attributes,
if sidebar_for_desktop.is_some() && !is_mobile {
div {
role: sidebar_role.as_str(),
"data-shell-sidebar": "",
"data-shell-sidebar-visible": (signals.sidebar_visible)().to_string(),
"data-shell-desktop-variant": match desktop_sidebar_val {
DesktopSidebar::Full => "full",
DesktopSidebar::Rail => "rail",
DesktopSidebar::Expandable => "expandable",
},
{sidebar_for_desktop}
}
}
if sidebar.is_some() && is_mobile && mobile_sidebar_val != MobileSidebar::Hidden {
div {
"data-shell-sidebar": "",
"data-shell-sidebar-mobile": "true",
"data-shell-sidebar-variant": match mobile_sidebar_val {
MobileSidebar::Drawer => "drawer",
MobileSidebar::Rail => "rail",
MobileSidebar::Hidden => "hidden",
},
"data-shell-sidebar-state": if (signals.sidebar_mobile_open)() { "open" } else { "closed" },
{sidebar}
}
}
div {
role: "main",
"data-shell-content": "",
{children}
}
if let Some(preview_el) = preview {
div {
role: "region",
"aria-label": preview_label.as_str(),
"data-shell-preview": "",
{preview_el}
}
}
if let Some(footer_el) = footer {
div {
role: "contentinfo",
"data-shell-footer": "",
{footer_el}
}
}
if let Some(tabs_el) = tabs {
div {
role: "navigation",
"data-shell-tabs": "",
{tabs_el}
}
}
if let Some(sheet_el) = sheet {
div {
role: "complementary",
"data-shell-sheet": "",
"data-shell-sheet-state": (signals.sheet_snap)().as_str(),
{sheet_el}
}
}
if let Some(fab_el) = fab {
div {
"data-shell-fab": "",
{fab_el}
}
}
if let Some(action_bar_el) = action_bar {
div {
role: "toolbar",
aria_label: "Actions",
"data-shell-action-bar": "",
{action_bar_el}
}
}
if let Some(search_el) = search {
div {
role: "search",
"data-shell-search": "",
"data-shell-search-active": (signals.search_active)().to_string(),
{search_el}
}
}
if let Some(modal_el) = modal {
div {
role: "dialog",
"aria-modal": "true",
"data-shell-modal": "",
"data-shell-modal-state": if (signals.modal_open)() { "presented" } else { "dismissed" },
{modal_el}
}
}
}
}
}
#[component]
pub fn MobileSidebarBackdrop(
#[props(default)]
class: Option<String>,
) -> Element {
let ctx = use_shell_context();
if ctx.is_mobile() && (ctx.sidebar_mobile_open)() {
rsx! {
div {
"data-shell-backdrop": "",
class: class.unwrap_or_default(),
onclick: move |_| {
let mut open = ctx.sidebar_mobile_open;
open.set(false);
},
}
}
} else {
rsx! {}
}
}