Skip to content

Commit 5260e3f

Browse files
authored
Merge pull request #5779 from Textualize/breakpoints
Breakpoints
2 parents 7cceaeb + cb872e2 commit 5260e3f

File tree

9 files changed

+991
-0
lines changed

9 files changed

+991
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1515

1616
- Added `toggle_class` parameter to reactives https://github.com/Textualize/textual/pull/5778
1717
- Added `compact` parameter and reactive to `Button`, `Input`, `ToggleButton`, `RadioSet`, `OptionList`, `TextArea` https://github.com/Textualize/textual/pull/5778
18+
- Added `HORIZONTAL_BREAKPOINTS` and `VERTICAL_BREAKPOINTS` to `App` and `Screen` https://github.com/Textualize/textual/pull/5779
1819

1920
### Changed
2021

examples/breakpoints.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from textual.app import App, ComposeResult
2+
from textual.containers import Grid
3+
from textual.widgets import Footer, Markdown, Placeholder
4+
5+
HELP = """\
6+
## Breakpoints
7+
8+
A demonstration of how to make an app respond to the dimensions of the terminal.
9+
10+
Try resizing the terminal, then have a look at the source to see how it works!
11+
"""
12+
13+
14+
class BreakpointApp(App):
15+
16+
# A breakpoint consists of a width and a class name to set
17+
HORIZONTAL_BREAKPOINTS = [
18+
(0, "-narrow"),
19+
(40, "-normal"),
20+
(80, "-wide"),
21+
(120, "-very-wide"),
22+
]
23+
24+
CSS = """
25+
Screen {
26+
Placeholder { padding: 2; }
27+
Grid { grid-rows: auto; height: auto; }
28+
# Change the styles according to the breakpoint classes
29+
&.-narrow {
30+
Grid { grid-size: 1; }
31+
}
32+
&.-normal {
33+
Grid { grid-size: 2; }
34+
}
35+
&.-wide {
36+
Grid { grid-size: 4; }
37+
}
38+
&.-very-wide {
39+
Grid { grid-size: 6; }
40+
}
41+
}
42+
"""
43+
44+
def compose(self) -> ComposeResult:
45+
yield Markdown(HELP)
46+
with Grid():
47+
for n in range(16):
48+
yield Placeholder(f"Placeholder {n+1}")
49+
yield Footer()
50+
51+
52+
if __name__ == "__main__":
53+
BreakpointApp().run()

src/textual/app.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,31 @@ 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+
This allows for styles to be responsive to the dimensions of the terminal.
489+
For instance, you might want to show less information, or fewer columns on a narrow displays -- or more information when the terminal is sized wider than usual.
490+
491+
A breakpoint consists of a tuple containing the minimum width where the class should applied, and the name of the class to set.
492+
493+
Note that only one class name is set, and if you should avoid having more than one breakpoint set for the same size.
494+
495+
Example:
496+
```python
497+
# Up to 80 cells wide, the app has the class "-normal"
498+
# 80 - 119 cells wide, the app has the class "-wide"
499+
# 120 cells or wider, the app has the class "-very-wide"
500+
HORIZONTAL_BREAKPOINTS = [(0, "-normal"), (80, "-wide"), (120, "-very-wide")]
501+
```
502+
503+
"""
504+
VERTICAL_BREAKPOINTS: ClassVar[list[tuple[int, str]]] | None = []
505+
"""List of vertical breakpoints for responsive classes.
506+
507+
Contents are the same as [`HORIZONTAL_BREAKPOINTS`][textual.app.App.HORIZONTAL_BREAKPOINTS], but the integer is compared to the height, rather than the width.
508+
"""
509+
485510
_PSEUDO_CLASSES: ClassVar[dict[str, Callable[[App[Any]], bool]]] = {
486511
"focus": lambda app: app.app_focus,
487512
"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:
Lines changed: 145 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)