Files
MyFastHtml/src/myfasthtml/controls/Panel.py

287 lines
8.4 KiB
Python

import logging
from dataclasses import dataclass
from typing import Literal, Optional
from fasthtml.components import Div
from fasthtml.xtend import Script
from myfasthtml.controls.BaseCommands import BaseCommands
from myfasthtml.controls.helpers import mk
from myfasthtml.core.commands import Command
from myfasthtml.core.dbmanager import DbObject
from myfasthtml.core.instances import MultipleInstance
from myfasthtml.icons.fluent_p1 import more_horizontal20_regular
from myfasthtml.icons.fluent_p2 import subtract20_regular
logger = logging.getLogger("Panel")
class PanelIds:
def __init__(self, owner):
self._owner = owner
@property
def main(self):
return f"{self._owner.get_id()}_m"
@property
def right(self):
""" Right panel's content"""
return f"{self._owner.get_id()}_cr"
@property
def left(self):
""" Left panel's content"""
return f"{self._owner.get_id()}_cl"
def panel(self, side: Literal["left", "right"]):
return f"{self._owner.get_id()}_pl" if side == "left" else f"{self._owner.get_id()}_pr"
def content(self, side: Literal["left", "right"]):
return self.left if side == "left" else self.right
@dataclass
class PanelConf:
left: bool = False
right: bool = True
left_title: str = "Left"
right_title: str = "Right"
show_left_title: bool = True
show_right_title: bool = True
show_display_left: bool = True
show_display_right: bool = True
class PanelState(DbObject):
def __init__(self, owner, name=None):
super().__init__(owner, name=name)
with self.initializing():
self.left_visible: bool = True
self.right_visible: bool = True
self.left_width: int = 250
self.right_width: int = 250
class Commands(BaseCommands):
def set_side_visible(self, side: Literal["left", "right"], visible: bool = None):
return Command("SetVisiblePanelSide",
f"Open / Close {side} side panel",
self._owner,
self._owner.set_side_visible,
args=[side, visible]).htmx(target=f"#{self._owner.get_ids().panel(side)}")
def toggle_side(self, side: Literal["left", "right"]):
return Command("TogglePanelSide",
f"Toggle {side} side panel",
self._owner,
self._owner.toggle_side,
args=[side]).htmx(target=f"#{self._owner.get_ids().panel(side)}")
def update_side_width(self, side: Literal["left", "right"]):
"""
Create a command to update panel's side width.
Args:
side: Which panel's side to update ("left" or "right")
Returns:
Command: Command object for updating panel's side width
"""
return Command(f"UpdatePanelSideWidth_{side}",
f"Update {side} side panel width",
self._owner,
self._owner.update_side_width,
args=[side]).htmx(target=f"#{self._owner.get_ids().panel(side)}")
class Panel(MultipleInstance):
"""
Represents a user interface panel that supports customizable left, main, and right components.
The `Panel` class is used to create and manage a panel layout with optional left, main,
and right sections. It provides functionality to set the components of the panel, toggle
sides, and adjust the width of the sides dynamically. The class also handles rendering
the panel with appropriate HTML elements and JavaScript for interactivity.
"""
def __init__(self, parent, conf: Optional[PanelConf] = None, _id=None):
super().__init__(parent, _id=_id)
self.conf = conf or PanelConf()
self.commands = Commands(self)
self._state = PanelState(self)
self._main = None
self._right = None
self._left = None
self._ids = PanelIds(self)
def get_ids(self):
return self._ids
def update_side_width(self, side, width):
logger.debug(f"update_side_width {side=} {width=}")
if side == "left":
self._state.left_width = width
else:
self._state.right_width = width
return self._mk_panel(side)
def set_side_visible(self, side, visible):
if side == "left":
self._state.left_visible = visible
else:
self._state.right_visible = visible
return self._mk_panel(side), self._mk_show_icon(side)
def toggle_side(self, side):
current_visible = self._state.left_visible if side == "left" else self._state.right_visible
return self.set_side_visible(side, not current_visible)
def set_main(self, main):
self._main = main
return self
def set_right(self, right):
self._right = right
return Div(self._right, id=self._ids.right)
def set_left(self, left):
self._left = left
return Div(self._left, id=self._ids.left)
def set_title(self, side, title):
if side == "left":
self.conf.left_title = title
else:
self.conf.right_title = title
return self._mk_panel(side)
def _mk_panel(self, side: Literal["left", "right"]):
enabled = self.conf.left if side == "left" else self.conf.right
if not enabled:
return None
visible = self._state.left_visible if side == "left" else self._state.right_visible
content = self._right if side == "right" else self._left
show_title = self.conf.show_left_title if side == "left" else self.conf.show_right_title
title = self.conf.left_title if side == "left" else self.conf.right_title
resizer = Div(
cls=f"mf-resizer mf-resizer-{side}",
data_command_id=self.commands.update_side_width(side).id,
data_side=side
)
hide_icon = mk.icon(
subtract20_regular,
size=20,
command=self.commands.set_side_visible(side, False),
cls="mf-panel-hide-icon"
)
panel_cls = f"mf-panel-{side}"
if not visible:
panel_cls += " mf-hidden"
if show_title:
panel_cls += " mf-panel-with-title"
# Left panel: content then resizer (resizer on the right)
# Right panel: resizer then content (resizer on the left)
if show_title:
header = Div(
Div(title),
hide_icon,
cls="mf-panel-header"
)
body = Div(
header,
Div(content, id=self._ids.content(side), cls="mf-panel-content"),
cls="mf-panel-body"
)
if side == "left":
return Div(
body,
resizer,
cls=panel_cls,
style=f"width: {self._state.left_width}px;",
id=self._ids.panel(side)
)
else:
return Div(
resizer,
body,
cls=panel_cls,
style=f"width: {self._state.right_width}px;",
id=self._ids.panel(side)
)
else:
if side == "left":
return Div(
hide_icon,
Div(content, id=self._ids.content(side)),
resizer,
cls=panel_cls,
style=f"width: {self._state.left_width}px;",
id=self._ids.panel(side)
)
else:
return Div(
resizer,
hide_icon,
Div(content, id=self._ids.content(side)),
cls=panel_cls,
style=f"width: {self._state.left_width}px;",
id=self._ids.panel(side)
)
def _mk_main(self):
return Div(
self._mk_show_icon("left"),
Div(self._main, id=self._ids.main, cls="mf-panel-main"),
self._mk_show_icon("right"),
cls="mf-panel-main"
),
def _mk_show_icon(self, side: Literal["left", "right"]):
"""
Create show icon for a panel side if it's hidden.
Args:
side: Which panel side ("left" or "right")
Returns:
Div with icon if panel is hidden, None otherwise
"""
enabled = self.conf.left if side == "left" else self.conf.right
if not enabled:
return None
show_display = self.conf.show_display_left if side == "left" else self.conf.show_display_right
if not show_display:
return None
is_visible = self._state.left_visible if side == "left" else self._state.right_visible
icon_cls = "hidden" if is_visible else f"mf-panel-show-icon mf-panel-show-icon-{side}"
return mk.icon(
more_horizontal20_regular,
command=self.commands.set_side_visible(side, True),
cls=icon_cls,
id=f"{self._id}_show_{side}"
)
def render(self):
return Div(
self._mk_panel("left"),
self._mk_main(),
self._mk_panel("right"),
Script(f"initResizer('{self._id}');"),
cls="mf-panel",
id=self._id,
)
def __ft__(self):
return self.render()