Skip to content

Instantly share code, notes, and snippets.

@ddkasa
Created February 16, 2025 12:10
Show Gist options
  • Save ddkasa/deb79a185053f88822c1b99ceaf035cf to your computer and use it in GitHub Desktop.
Save ddkasa/deb79a185053f88822c1b99ceaf035cf to your computer and use it in GitHub Desktop.
Textual Collapsible Panel
"""Collapsible Panel Module"""
from __future__ import annotations
from typing import ClassVar
from rich.text import Text
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container
from textual.events import Leave
from textual.events import MouseDown
from textual.events import MouseMove
from textual.geometry import Offset
from textual.reactive import var
from textual.widget import Widget
from textual.widgets import Button
from textual.widgets import Tabs
class CollapsibleWidget(Widget):
DEFAULT_CSS = """
CollapsibleWidget {
box-sizing: content-box;
layout: vertical;
border-right: double $panel-lighten-1;
margin-right: 0;
padding: 1 2;
width: 45;
overflow-y: auto;
scrollbar-color: auto 0%;
scrollbar-background: $background 0%;
&.trigger_collapse {
border-right: double $secondary;
}
&.collapsed {
&Button {
max-width: 1;
}
#navigation {
grid-size: 1;
grid-rows: 4;
height: 100%;
border: none;
}
}
&.collapsing {
border-right: thick $primary;
opacity: 50%;
background-tint: $panel-lighten-3 50%;
}
& > #navigation {
border-bottom: inner $secondary;
layout: grid;
grid-size: 3;
grid-rows: 4;
grid-rows: 3;
grid-gutter: 0 2;
height: 8;
& > Button {
column-span: 1;
height: 3;
border: none;
}
Tabs {
column-span: 3;
content-align-horizontal: center;
}
Rule {
column-span: 3;
}
}
}
"""
BINDINGS: ClassVar = [
Binding(
"ctrl+grave_accent",
"toggle('collapsed')",
"Toggle",
tooltip="Toggle the filter panel.",
),
]
collapsed = var[bool](False, init=False)
"""Whether the widget collapsed or not."""
def __init__(
self,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
) -> None:
super().__init__(
name=name,
id=id,
classes=classes,
disabled=disabled,
)
def on_mouse_down(self, event: MouseDown) -> None:
if self._can_collapse(event.offset):
self.collapsed = not self.collapsed
def on_mouse_move(self, event: MouseMove) -> None:
self.set_class(self._can_collapse(event.offset), "trigger_collapse")
def on_leave(self, event: Leave) -> None:
self.remove_class("trigger_collapse")
def compose(self) -> ComposeResult:
with Container(id="navigation"):
yield Tabs("List", "Table", id="content-tabs")
yield Button(Text("New", no_wrap=True))
yield Button(Text("Refresh", no_wrap=True))
yield Button(Text("Reset", no_wrap=True), "warning")
def _watch_collapsed(self, value: bool) -> None:
self.set_class(value, "collapsed")
self.add_class("collapsing")
self.styles.animate(
"width",
value=8 if value else 50,
duration=0.2,
easing="out_elastic",
on_complete=lambda: self.remove_class("collapsing"),
)
def _can_collapse(self, offset: Offset) -> bool:
return bool(self.region.width - 3 < offset.x < self.region.width + 3)
class CollapseApp(App[None]):
def compose(self) -> ComposeResult:
yield CollapsibleWidget()
if __name__ == "__main__":
CollapseApp().run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment