The bug
A MouseDown (i.e. the start of a click or a drag-to-select) that lands in a screen's own padding/border gutter crashes the whole app with an AssertionError.
When the pointer is over the screen background rather than a child widget, Compositor.get_widget_and_offset_at() returns the Screen itself as the selection target. If the click also falls inside the screen's left gutter, the compositor takes its if x < 0: return widget, Offset(0, y) branch, so it returns the screen with a non-None offset. Screen._forward_event then enters the text-selection branch and runs:
content_widget = select_widget # the Screen
assert isinstance(content_widget.parent, Widget) # 💥
A Screen's .parent is the App, which is not a Widget, so the assertion fails and the app crashes.
The trigger is any Screen with horizontal/vertical padding (or a border) — extremely common — combined with a click or drag near the edge. It is independent of OS and terminal.
Minimal repro
Runs without modification. Click (or drag) starting in column 0 — the 1-cell left padding (select from half of the screen and drag to the left border):
from textual.app import App, ComposeResult
from textual.widgets import Static
class CrashApp(App):
# Padding on the Screen creates a 1-cell gutter at column 0 that belongs to
# the Screen itself — no child widget renders there.
CSS = "Screen { padding: 0 1; }"
def compose(self) -> ComposeResult:
yield Static("Click or drag-select starting at the very left edge (column 0).")
if __name__ == "__main__":
CrashApp().run()
Traceback
Traceback (most recent call last):
File ".../site-packages/textual/app.py", line 4082, in on_event
self.screen._forward_event(event)
File ".../site-packages/textual/screen.py", line 1896, in _forward_event
assert isinstance(content_widget.parent, Widget)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError
Probable cause / suggested fix
The assert in Screen._forward_event encodes the invariant "a selectable target always has a Widget parent", which is false for a Screen (its parent is the App). This looks like a sibling of the already-fixed #5634 / PR #5641, which added an if y < 0: return None, None guard in the same get_widget_and_offset_at; the x < 0 path was not given the equivalent treatment.
A minimal fix is to fold the parent check into the existing branch condition and let a screen-target fall through to the ay selection path:
if select_offset is not None and isinstance(select_widget.parent, Widget):
content_widget = select_widget
content_offset = select_offset
container = content_widget.parent
else:
content_widget = None
container = select_widget # the Screen — has .region / .scroll_offset
content_offset = None
Happy to open a PR if this approach looks right.
textual diagnose
Versions
| Name |
Value |
| Textual |
8.2.7 |
| Rich |
14.3.4 |
Python
| Name |
Value |
| Version |
3.12.12 |
| Implementation |
CPython |
| Compiler |
Clang 21.1.4 |
Operating System
| Name |
Value |
| System |
Darwin |
| Release |
25.5.0 |
Terminal
|-----------|----------------|
| Terminal | vscode |
| TERM | xterm-256color |
| COLORTERM | truecolor |
(Also reproduces in macOS Terminal.app and other terminals — the crash is in event handling, not rendering.)
The bug
A
MouseDown(i.e. the start of a click or a drag-to-select) that lands in a screen's own padding/border gutter crashes the whole app with anAssertionError.When the pointer is over the screen background rather than a child widget,
Compositor.get_widget_and_offset_at()returns theScreenitself as the selection target. If the click also falls inside the screen's left gutter, the compositor takes itsif x < 0: return widget, Offset(0, y)branch, so it returns the screen with a non-Noneoffset.Screen._forward_eventthen enters the text-selection branch and runs:A
Screen's.parentis theApp, which is not aWidget, so the assertion fails and the app crashes.The trigger is any
Screenwith horizontal/vertical padding (or a border) — extremely common — combined with a click or drag near the edge. It is independent of OS and terminal.Minimal repro
Runs without modification. Click (or drag) starting in column 0 — the 1-cell left padding (select from half of the screen and drag to the left border):
Traceback
Probable cause / suggested fix
The
assertinScreen._forward_eventencodes the invariant "a selectable target always has aWidgetparent", which is false for aScreen(its parent is theApp). This looks like a sibling of the already-fixed #5634 / PR #5641, which added anif y < 0: return None, Noneguard in the sameget_widget_and_offset_at; thex < 0path was not given the equivalent treatment.A minimal fix is to fold the parent check into the existing branch condition and let a screen-target fall through to the ay selection path:
Happy to open a PR if this approach looks right.
textual diagnoseVersions
Python
Operating System
Terminal
|-----------|----------------|
| Terminal | vscode |
| TERM | xterm-256color |
| COLORTERM | truecolor |