from __future__ import annotations from dataclasses import dataclass from textual import on from textual.app import App, ComposeResult from textual.containers import ScrollableContainer from textual.events import MouseDown, MouseEvent, MouseMove, MouseUp, Resize from textual.geometry import Offset from textual.message import Message from textual.widget import Widget from textual.widgets import Label class Adjustable(Widget): @dataclass class Resize(Message): adjustable: Adjustable delta: int def __init__(self, id: str) -> None: super().__init__(id=id) self.clicked: Offset | None = None self.styles.width = 10 def compose(self) -> ComposeResult: yield Label("Test Content") async def on_mouse_down(self, event: MouseDown) -> None: if self.app.mouse_captured is None: self.capture_mouse() await self.is_focused(event) async def on_mouse_up(self, event: MouseUp) -> None: if self.app.mouse_captured: self.capture_mouse(False) await self.is_unfocused() async def is_focused(self, event: MouseEvent) -> None: self.clicked = event.offset self.moving = 2 < event.offset.x < (self.size.width - 2) async def is_unfocused(self) -> None: self.clicked = None async def on_mouse_move(self, event: MouseMove) -> None: if self.clicked is not None and event.button != 0: if hasattr(self, "moving") and self.moving: await self._move(event) else: await self._resize(event) async def _resize(self, event: MouseMove) -> None: delta = event.delta_x if self.clicked and self.clicked.x < 2: self.offset += Offset(delta, 0) delta *= -1 self.styles.width = self.styles.width.value + delta self.refresh() self.post_message(Adjustable.Resize(self, delta)) async def _move(self, event: MouseMove) -> None: if event.delta: self.offset = self.offset + Offset(event.delta.x) class Timeline(Widget): def __init__(self) -> None: super().__init__() self.styles.width = 5760 self.adjustables = ( Adjustable(id="adj-1"), Adjustable(id="adj-2"), Adjustable(id="adj-3"), ) def compose(self) -> ComposeResult: yield from self.adjustables @on(Adjustable.Resize) def _resize_compensation(self, message: Adjustable.Resize) -> None: offset = Offset(message.delta) for i in range(1, len(self.adjustables) + 1): adj = self.adjustables[-i] if adj.id == message.adjustable.id: break with adj.prevent(Adjustable.Resize, Resize): adj.offset = adj.offset - offset class BugReportApp(App): CSS = """ Adjustable { background: $panel; align-vertical: middle; height: 100%; min-width: 6; border: outer $secondary; } ScrollableContainer { height: auto; Timeline { background: $panel-darken-1; width: 100%; height: 100%; layout: horizontal; height: 13; } } """ def compose(self) -> ComposeResult: with ScrollableContainer(): yield Timeline() if __name__ == "__main__": app = BugReportApp() app.run()