Skip to content

Instantly share code, notes, and snippets.

@ddkasa
Last active November 18, 2024 09:13
Show Gist options
  • Save ddkasa/4c22a9ddff79f731732c8b91cf60021c to your computer and use it in GitHub Desktop.
Save ddkasa/4c22a9ddff79f731732c8b91cf60021c to your computer and use it in GitHub Desktop.
Textual Bug Report MRE - Offset Widgets Popping In And Out While Scrolling
Adjustable {
background: $panel;
align-vertical: middle;
&.horizontal {
border-left: double $panel-lighten-2;
border-right: double $panel-lighten-2;
height: 100%;
min-width: 6;
&.size_start {
outline: none;
outline-left: double $secondary;
&.hovered {
outline-left: double $primary;
}
}
&.size_end {
outline: none;
outline-right: double $secondary;
&.hovered {
outline-right: double $primary;
}
}
}
&.hovered {
outline: thick $primary;
Label {
text-style: underline;
}
}
&.moving {
outline: thick $secondary !important;
}
}
ScrollableContainer {
height: auto;
Timeline {
padding: 0;
background: $panel-darken-1;
border: hkey white;
width: 100%;
height: 100%;
&.horizontal {
layout: horizontal;
height: 13;
hatch: vertical white 5%;
}
}
}
from __future__ import annotations
from dataclasses import dataclass
from textual import on
from textual.app import App, ComposeResult
from textual.containers import Center, ScrollableContainer
from textual.events import MouseDown, MouseEvent, MouseMove, MouseUp, Resize
from textual.geometry import Offset, Size
from textual.message import Message
from textual.widget import Widget
from textual.widgets import Static, Label
from textual.events import Enter, Leave
class Adjustable(Widget):
HORZ_MARGIN: int = 2
DEFAULT_CLASSES = "horizontal"
@dataclass
class Resize(Message):
adjustable: Adjustable
size: Size
delta: int
def __init__(
self,
content: Widget,
host: Timeline,
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)
self.host = host
self.content = content
self.clicked: Offset | None = Offset(0, 0)
self.styles.width = 10
self.styles.height = 10
def compose(self) -> ComposeResult:
yield self.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(event)
async def is_focused(self, event: MouseEvent) -> None:
self.clicked = event.offset
self.moving = self.is_moving(event.offset)
if self.moving:
self.add_class("focus")
else:
self.add_edge(event.offset)
def add_edge(self, offset: Offset) -> None:
if offset.x < self.HORZ_MARGIN:
self.add_class("size_start")
else:
self.add_class("size_end")
async def is_unfocused(self, event: MouseUp) -> None:
self.clicked = None
self.remove_class("focus", "moving", "size_start", "size_end")
async def on_mouse_move(self, event: MouseMove) -> None:
await self.adjust(event)
async def adjust(self, event: MouseMove) -> None:
if self.clicked is not None and event.button != 0:
if hasattr(self, "moving") and self.moving:
self.add_class("moving")
await self._move(event)
else:
await self.resize(event)
def is_moving(self, offset: Offset) -> bool:
return bool(self.HORZ_MARGIN < offset.x < (self.size.width - self.HORZ_MARGIN))
async def _horz_resize(self, event: MouseMove) -> int:
delta = event.delta_x
if self.clicked and self.clicked.x < self.HORZ_MARGIN:
self.offset += Offset(delta, 0)
delta *= -1
self.styles.width = self.styles.width.value + delta
return delta
async def resize(self, event: MouseMove) -> None:
with self.prevent(Resize):
delta = await self._horz_resize(event)
self.refresh()
self.post_message(Adjustable.Resize(self, self.size, delta))
async def _move(self, event: MouseMove) -> None:
if event.delta:
delta = Offset(event.delta.x)
self.offset = self.offset + delta
class Timeline(Static):
DEFAULT_CLASSES = "horizontal"
def __init__(
self,
*children: tuple[str, str],
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)
self.styles.width = 86400 // (60 / 4)
self.adjustables = [
Adjustable(Center(Label(c)), self, id=i) for c, i in children
]
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
async def on_enter(self, event: Enter) -> None:
if not self.app.mouse_captured:
self.is_entered = True
self.add_class("hovered")
async def on_leave(self, event: Leave) -> None:
self.is_entered = False
self.remove_class("hovered")
class BugReportApp(App):
CSS_PATH = "app.tcss"
def compose(self) -> ComposeResult:
with ScrollableContainer():
yield Timeline(
("Test Content", "id-" + str(1)),
("Test Content", "id-" + str(2)),
("Test Content", "id-" + str(3)),
)
if __name__ == "__main__":
app = BugReportApp()
app.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment