Skip to content

Commit 82a0beb

Browse files
committed
merge
1 parent 7cceaeb commit 82a0beb

File tree

3 files changed

+111
-0
lines changed

3 files changed

+111
-0
lines changed

src/textual/app.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,26 @@ class MyApp(App[None]):
482482
SUSPENDED_SCREEN_CLASS: ClassVar[str] = ""
483483
"""Class to apply to suspended screens, or empty string for no class."""
484484

485+
HORIZONTAL_BREAKPOINTS: ClassVar[list[tuple[int, str]]] | None = []
486+
"""List of horizontal breakpoints for responsive classes.
487+
488+
A breakpoint consists of a tuple containing the width where the class is set, and the classname to set.
489+
490+
Example:
491+
```python
492+
# Up to 80 cells wide, the app has the class "-normal"
493+
# 80 - 119 cells wide, the app has the class "-wide"
494+
# 120 cells or wider, the app has the class "-very-wide"
495+
[(0, "-normal"), (80, "-wide"), (120, "-very-wide")]
496+
```
497+
498+
"""
499+
VERTICAL_BREAKPOINTS: ClassVar[list[tuple[int, str]]] | None = []
500+
"""List of vertical breakpoints for responsive classes.
501+
502+
Contents are the same as `HORIZONTAL_BREAKPOINTS`, but the integer is compared to the height, rather than the width.
503+
"""
504+
485505
_PSEUDO_CLASSES: ClassVar[dict[str, Callable[[App[Any]], bool]]] = {
486506
"focus": lambda app: app.app_focus,
487507
"blur": lambda app: not app.app_focus,

src/textual/screen.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,11 @@ class Screen(Generic[ScreenResultType], Widget):
196196
you can set the [sub_title][textual.screen.Screen.sub_title] attribute.
197197
"""
198198

199+
HORIZONTAL_BREAKPOINTS: ClassVar[list[tuple[int, str]]] | None = None
200+
"""Horizontal breakpoints, will override [App.HORIZONTAL_BREAKPOINTS][textual.app.App.HORIZONTAL_BREAKPOINTS] if not `None`."""
201+
VERTICAL_BREAKPOINTS: ClassVar[list[tuple[int, str]]] | None = None
202+
"""Vertical breakpoints, will override [App.VERTICAL_BREAKPOINTS][textual.app.App.VERTICAL_BREAKPOINTS] if not `None`."""
203+
199204
focused: Reactive[Widget | None] = Reactive(None)
200205
"""The focused [widget][textual.widget.Widget] or `None` for no focus.
201206
To set focus, do not update this value directly. Use [set_focus][textual.screen.Screen.set_focus] instead."""
@@ -1354,6 +1359,41 @@ async def _on_resize(self, event: events.Resize) -> None:
13541359
for screen in self.app._background_screens:
13551360
screen._screen_resized(event.size)
13561361

1362+
horizontal_breakpoints = (
1363+
self.app.HORIZONTAL_BREAKPOINTS
1364+
if self.HORIZONTAL_BREAKPOINTS is None
1365+
else self.HORIZONTAL_BREAKPOINTS
1366+
) or []
1367+
1368+
vertical_breakpoints = (
1369+
self.app.VERTICAL_BREAKPOINTS
1370+
if self.VERTICAL_BREAKPOINTS is None
1371+
else self.VERTICAL_BREAKPOINTS
1372+
) or []
1373+
1374+
width, height = event.size
1375+
if horizontal_breakpoints:
1376+
self._set_breakpoints(width, horizontal_breakpoints)
1377+
if vertical_breakpoints:
1378+
self._set_breakpoints(height, horizontal_breakpoints)
1379+
1380+
def _set_breakpoints(
1381+
self, dimension: int, breakpoints: list[tuple[int, str]]
1382+
) -> None:
1383+
"""Set horizontal or vertical breakpoints.
1384+
1385+
Args:
1386+
dimension: Either the width or the height.
1387+
breakpoints: A list of breakpoints.
1388+
1389+
"""
1390+
class_names = [class_name for _breakpoint, class_name in breakpoints]
1391+
self.remove_class(*class_names)
1392+
for breakpoint, class_name in sorted(breakpoints, reverse=True):
1393+
if dimension >= breakpoint:
1394+
self.add_class(class_name)
1395+
return
1396+
13571397
def _update_tooltip(self, widget: Widget) -> None:
13581398
"""Update the content of the tooltip."""
13591399
try:

tests/snapshot_tests/test_snapshots.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4023,6 +4023,57 @@ def compose(self) -> ComposeResult:
40234023
assert snap_compare(TintApp())
40244024

40254025

4026+
@pytest.mark.parametrize(
4027+
"size",
4028+
[
4029+
(30, 40),
4030+
(40, 40),
4031+
(80, 80),
4032+
(130, 80),
4033+
],
4034+
)
4035+
def test_breakpoints(snap_compare, size):
4036+
"""Test HORIZONTAL_BREAKPOINTS
4037+
4038+
You should see four terminals of different sizes with a grid of placeholders.
4039+
The first should have a single column, then two columns, then 4, then 6.
4040+
4041+
"""
4042+
4043+
class BreakpointApp(App):
4044+
4045+
HORIZONTAL_BREAKPOINTS = [
4046+
(0, "-narrow"),
4047+
(40, "-normal"),
4048+
(80, "-wide"),
4049+
(120, "-very-wide"),
4050+
]
4051+
4052+
CSS = """
4053+
Screen {
4054+
&.-narrow {
4055+
Grid { grid-size: 1; }
4056+
}
4057+
&.-normal {
4058+
Grid { grid-size: 2; }
4059+
}
4060+
&.-wide {
4061+
Grid { grid-size: 4; }
4062+
}
4063+
&.-very-wide {
4064+
Grid { grid-size: 6; }
4065+
}
4066+
}
4067+
"""
4068+
4069+
def compose(self) -> ComposeResult:
4070+
with Grid():
4071+
for n in range(16):
4072+
yield Placeholder(f"Placeholder {n+1}")
4073+
4074+
assert snap_compare(BreakpointApp(), terminal_size=size)
4075+
4076+
40264077
def test_compact(snap_compare):
40274078
"""Test compact styles.
40284079

0 commit comments

Comments
 (0)