""" Layout component for FastHTML applications. This component provides a responsive layout with fixed header/footer, optional collapsible left/right drawers, and a scrollable main content area. """ import logging from typing import Literal from fasthtml.common import * from myfasthtml.controls.BaseCommands import BaseCommands from myfasthtml.controls.Boundaries import Boundaries from myfasthtml.controls.UserProfile import UserProfile from myfasthtml.controls.helpers import mk from myfasthtml.core.commands import Command from myfasthtml.core.dbmanager import DbObject from myfasthtml.core.instances import SingleInstance from myfasthtml.core.utils import get_id from myfasthtml.icons.fluent import panel_left_expand20_regular as left_drawer_icon from myfasthtml.icons.fluent_p2 import panel_right_expand20_regular as right_drawer_icon logger = logging.getLogger("LayoutControl") class LayoutState(DbObject): def __init__(self, owner): super().__init__(owner) with self.initializing(): self.left_drawer_open: bool = True self.right_drawer_open: bool = True self.left_drawer_width: int = 250 self.right_drawer_width: int = 250 class Commands(BaseCommands): def toggle_drawer(self, side: Literal["left", "right"]): return Command("ToggleDrawer", f"Toggle {side} layout drawer", self._owner.toggle_drawer, side) def update_drawer_width(self, side: Literal["left", "right"]): """ Create a command to update drawer width. Args: side: Which drawer to update ("left" or "right") Returns: Command: Command object for updating drawer width """ return Command( f"UpdateDrawerWidth_{side}", f"Update {side} drawer width", self._owner.update_drawer_width, side ) class Layout(SingleInstance): """ A responsive layout component with header, footer, main content area, and optional collapsible side drawers. Attributes: app_name (str): Name of the application left_drawer (bool): Whether to include a left drawer right_drawer (bool): Whether to include a right drawer """ class Content: def __init__(self, owner): self._owner = owner self._content = {} self._groups = [] self._ids = set() def add_group(self, group, group_ft=None): group_ft = group_ft or Div(group, cls="mf-layout-group") if not group: group_ft = None self._groups.append((group, group_ft)) self._content[group] = [] def add(self, content, group=None): content_id = get_id(content) if content_id in self._ids: return if group not in self._content: self.add_group(group) self._content[group] = [] self._content[group].append(content) if content_id is not None: self._ids.add(content_id) def get_content(self): return self._content def get_groups(self): return self._groups def __init__(self, parent, app_name, _id=None): """ Initialize the Layout component. Args: app_name (str): Name of the application left_drawer (bool): Enable left drawer. Default is True. right_drawer (bool): Enable right drawer. Default is True. """ super().__init__(parent, _id=_id) self.app_name = app_name # Content storage self._main_content = None self._state = LayoutState(self) self._boundaries = Boundaries(self) self.commands = Commands(self) self.left_drawer = self.Content(self) self.right_drawer = self.Content(self) self.header_left = self.Content(self) self.header_right = self.Content(self) self.footer_left = self.Content(self) self.footer_right = self.Content(self) self._footer_content = None def set_footer(self, content): """ Set the footer content. Args: content: FastHTML component(s) or content for the footer """ self._footer_content = content def set_main(self, content): """ Set the main content area. Args: content: FastHTML component(s) or content for the main area """ self._main_content = content return self def toggle_drawer(self, side: Literal["left", "right"]): logger.debug(f"Toggle drawer: {side=}, {self._state.left_drawer_open=}") if side == "left": self._state.left_drawer_open = not self._state.left_drawer_open return self._mk_left_drawer_icon(), self._mk_left_drawer() elif side == "right": self._state.right_drawer_open = not self._state.right_drawer_open return self._mk_right_drawer_icon(), self._mk_right_drawer() else: raise ValueError("Invalid drawer side") def update_drawer_width(self, side: Literal["left", "right"], width: int): """ Update the width of a drawer. Args: side: Which drawer to update ("left" or "right") width: New width in pixels Returns: Div: Updated drawer component """ # Constrain width between min and max values width = max(150, min(600, width)) logger.debug(f"Update drawer width: {side=}, {width=}") if side == "left": self._state.left_drawer_width = width return self._mk_left_drawer() elif side == "right": self._state.right_drawer_width = width return self._mk_right_drawer() else: raise ValueError("Invalid drawer side") def _mk_header(self): """ Render the header component. Returns: Header: FastHTML Header component """ return Header( Div( # left self._mk_left_drawer_icon(), *self.header_left.get_content(), cls="flex gap-1" ), Div( # right *self.header_right.get_content()[None], UserProfile(self), cls="flex gap-1" ), cls="mf-layout-header" ) def _mk_footer(self): """ Render the footer component. Returns: Footer: FastHTML Footer component """ footer_content = self._footer_content if self._footer_content else "" return Footer( footer_content, cls="mf-layout-footer footer sm:footer-horizontal" ) def _mk_main(self): """ Render the main content area. Returns: Main: FastHTML Main component """ main_content = self._main_content if self._main_content else "" return Main( main_content, cls="mf-layout-main" ) def _mk_left_drawer(self): """ Render the left drawer if enabled. Returns: Div: FastHTML Div component for left drawer """ resizer = Div( cls="mf-resizer mf-resizer-left", data_command_id=self.commands.update_drawer_width("left").id, data_side="left" ) # Wrap content in scrollable container content_wrapper = Div( *[ ( Div(cls="divider") if index > 0 else None, group_ft, *[item for item in self.left_drawer.get_content()[group_name]] ) for index, (group_name, group_ft) in enumerate(self.left_drawer.get_groups()) ], cls="mf-layout-drawer-content" ) return Div( content_wrapper, resizer, id=f"{self._id}_ld", cls=f"mf-layout-drawer mf-layout-left-drawer {'collapsed' if not self._state.left_drawer_open else ''}", style=f"width: {self._state.left_drawer_width if self._state.left_drawer_open else 0}px;" ) def _mk_right_drawer(self): """ Render the right drawer if enabled. Returns: Div: FastHTML Div component for right drawer """ resizer = Div( cls="mf-resizer mf-resizer-right", data_command_id=self.commands.update_drawer_width("right").id, data_side="right" ) # Wrap content in scrollable container content_wrapper = Div( *self.right_drawer.get_content(), cls="mf-layout-drawer-content" ) return Div( resizer, content_wrapper, cls=f"mf-layout-drawer mf-layout-right-drawer {'collapsed' if not self._state.right_drawer_open else ''}", id=f"{self._id}_rd", style=f"width: {self._state.right_drawer_width if self._state.right_drawer_open else 0}px;" ) def _mk_left_drawer_icon(self): return mk.icon(right_drawer_icon if self._state.left_drawer_open else left_drawer_icon, id=f"{self._id}_ldi", command=self.commands.toggle_drawer("left")) def _mk_right_drawer_icon(self): return mk.icon(right_drawer_icon if self._state.left_drawer_open else left_drawer_icon, id=f"{self._id}_rdi", command=self.commands.toggle_drawer("right")) def render(self): """ Render the complete layout. Returns: Div: Complete layout as FastHTML Div component """ # Wrap everything in a container div return Div( self._mk_header(), self._mk_left_drawer(), self._mk_main(), self._mk_right_drawer(), self._mk_footer(), Script(f"initResizer('{self._id}');"), id=self._id, cls="mf-layout", ) def __ft__(self): """ FastHTML magic method for rendering. Returns: Div: The rendered layout """ return self.render()