Merge pull request #2030 from Textualize/add-containers

Add containers
This commit is contained in:
Rodrigo Girão Serrão
2023-03-15 10:37:01 +00:00
committed by GitHub
49 changed files with 707 additions and 120 deletions

View File

@@ -11,6 +11,21 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Dropped "loading-indicator--dot" component style from LoadingIndicator https://github.com/Textualize/textual/pull/2050
## Unreleased
### Changed
- Breaking change: changed default behaviour of `Vertical` (see `VerticalScroll`) https://github.com/Textualize/textual/issues/1957
- The default `overflow` style for `Horizontal` was changed to `hidden hidden` https://github.com/Textualize/textual/issues/1957
### Added
- Added `HorizontalScroll` https://github.com/Textualize/textual/issues/1957
- Added `Center` https://github.com/Textualize/textual/issues/1957
- Added `Middle` https://github.com/Textualize/textual/issues/1957
- Added `VerticalScroll` (mimicking the old behaviour of `Vertical`) https://github.com/Textualize/textual/issues/1957
## [0.15.1] - 2023-03-14
### Fixed
@@ -22,6 +37,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed
- Fixed container not resizing when a widget is removed https://github.com/Textualize/textual/issues/2007
- Fixes issue where the horizontal scrollbar would be incorrectly enabled https://github.com/Textualize/textual/pull/2024
## [0.15.0] - 2023-03-13
### Changed
- Fixed container not resizing when a widget is removed https://github.com/Textualize/textual/issues/2007
- Fixed issue where the horizontal scrollbar would be incorrectly enabled https://github.com/Textualize/textual/pull/2024
- Fixed `Pilot.click` not correctly creating the mouse events https://github.com/Textualize/textual/issues/2022

View File

@@ -1,9 +1,9 @@
from random import randint
import time
from random import randint
from textual.app import App, ComposeResult
from textual.color import Color
from textual.containers import Grid, Vertical
from textual.containers import Grid, VerticalScroll
from textual.widget import Widget
from textual.widgets import Footer, Label
@@ -28,7 +28,7 @@ class MyApp(App[None]):
def compose(self) -> ComposeResult:
yield Grid(
ColourChanger(),
Vertical(id="log"),
VerticalScroll(id="log"),
)
yield Footer()

View File

@@ -1,10 +1,10 @@
import asyncio
from random import randint
import time
from random import randint
from textual.app import App, ComposeResult
from textual.color import Color
from textual.containers import Grid, Vertical
from textual.containers import Grid, VerticalScroll
from textual.widget import Widget
from textual.widgets import Footer, Label
@@ -29,7 +29,7 @@ class MyApp(App[None]):
def compose(self) -> ComposeResult:
yield Grid(
ColourChanger(),
Vertical(id="log"),
VerticalScroll(id="log"),
)
yield Footer()

View File

@@ -1,10 +1,10 @@
import asyncio
from random import randint
import time
from random import randint
from textual.app import App, ComposeResult
from textual.color import Color
from textual.containers import Grid, Vertical
from textual.containers import Grid, VerticalScroll
from textual.widget import Widget
from textual.widgets import Footer, Label
@@ -29,7 +29,7 @@ class MyApp(App[None]):
def compose(self) -> ComposeResult:
yield Grid(
ColourChanger(),
Vertical(id="log"),
VerticalScroll(id="log"),
)
yield Footer()

View File

@@ -6,9 +6,10 @@ except ImportError:
raise ImportError("Please install httpx with 'pip install httpx' ")
from rich.json import JSON
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import Static, Input
from textual.containers import VerticalScroll
from textual.widgets import Input, Static
class DictionaryApp(App):
@@ -18,7 +19,7 @@ class DictionaryApp(App):
def compose(self) -> ComposeResult:
yield Input(placeholder="Search for a word")
yield Vertical(Static(id="results"), id="results-container")
yield VerticalScroll(Static(id="results"), id="results-container")
async def on_input_changed(self, message: Input.Changed) -> None:
"""A coroutine to handle a text changed message."""

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.containers import Container, Horizontal, VerticalScroll
from textual.widgets import Header, Static
@@ -9,7 +9,7 @@ class CombiningLayoutsExample(App):
def compose(self) -> ComposeResult:
yield Header()
with Container(id="app-grid"):
with Vertical(id="left-pane"):
with VerticalScroll(id="left-pane"):
for number in range(15):
yield Static(f"Vertical layout, child {number}")
with Horizontal(id="top-right"):

View File

@@ -1,6 +1,6 @@
from textual.app import App
from textual.containers import Vertical
from textual.widgets import Placeholder, Label, Static
from textual.containers import VerticalScroll
from textual.widgets import Label, Placeholder, Static
class Ruler(Static):
@@ -11,7 +11,7 @@ class Ruler(Static):
class HeightComparisonApp(App):
def compose(self):
yield Vertical(
yield VerticalScroll(
Placeholder(id="cells"), # (1)!
Placeholder(id="percent"),
Placeholder(id="w"),

View File

@@ -1,11 +1,11 @@
from textual.app import App
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Placeholder
class MaxWidthApp(App):
def compose(self):
yield Vertical(
yield VerticalScroll(
Placeholder("max-width: 50h", id="p1"),
Placeholder("max-width: 999", id="p2"),
Placeholder("max-width: 50%", id="p3"),

View File

@@ -1,4 +1,4 @@
Vertical {
VerticalScroll {
height: 100%;
width: 100%;
overflow-x: auto;
@@ -10,7 +10,8 @@ Placeholder {
}
#p1 {
min-width: 25%; /* (1)! */
min-width: 25%;
/* (1)! */
}
#p2 {

View File

@@ -1,11 +1,11 @@
from textual.app import App
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Placeholder
class MinWidthApp(App):
def compose(self):
yield Vertical(
yield VerticalScroll(
Placeholder("min-width: 25%", id="p1"),
Placeholder("min-width: 75%", id="p2"),
Placeholder("min-width: 100", id="p3"),

View File

@@ -3,7 +3,7 @@ Screen {
color: black;
}
Vertical {
VerticalScroll {
width: 1fr;
}
@@ -13,7 +13,7 @@ Static {
border: green wide;
color: white 90%;
height: auto;
}
}
#right {
overflow-y: hidden;

View File

@@ -1,6 +1,6 @@
from textual.app import App
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Static
from textual.containers import Horizontal, Vertical
TEXT = """I must not fear.
Fear is the mind-killer.
@@ -14,8 +14,8 @@ Where the fear has gone there will be nothing. Only I will remain."""
class OverflowApp(App):
def compose(self):
yield Horizontal(
Vertical(Static(TEXT), Static(TEXT), Static(TEXT), id="left"),
Vertical(Static(TEXT), Static(TEXT), Static(TEXT), id="right"),
VerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id="left"),
VerticalScroll(Static(TEXT), Static(TEXT), Static(TEXT), id="right"),
)

View File

@@ -1,11 +1,11 @@
from textual.app import App
from textual.containers import Horizontal, Vertical
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Placeholder
class VisibilityContainersApp(App):
def compose(self):
yield Vertical(
yield VerticalScroll(
Horizontal(
Placeholder(),
Placeholder(),

View File

@@ -2,7 +2,7 @@ Button {
margin: 1 2;
}
Horizontal > Vertical {
Horizontal > VerticalScroll {
width: 24;
}

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Button, Static
@@ -8,7 +8,7 @@ class ButtonsApp(App[str]):
def compose(self) -> ComposeResult:
yield Horizontal(
Vertical(
VerticalScroll(
Static("Standard Buttons", classes="header"),
Button("Default"),
Button("Primary!", variant="primary"),
@@ -16,7 +16,7 @@ class ButtonsApp(App[str]):
Button.warning("Warning!"),
Button.error("Error!"),
),
Vertical(
VerticalScroll(
Static("Disabled Buttons", classes="header"),
Button("Default", disabled=True),
Button("Primary!", variant="primary", disabled=True),

View File

@@ -2,7 +2,7 @@ Screen {
align: center middle;
}
Vertical {
VerticalScroll {
width: auto;
height: auto;
border: solid $primary;

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Checkbox
@@ -7,7 +7,7 @@ class CheckboxApp(App[None]):
CSS_PATH = "checkbox.css"
def compose(self) -> ComposeResult:
with Vertical():
with VerticalScroll():
yield Checkbox("Arrakis :sweat:")
yield Checkbox("Caladan")
yield Checkbox("Chusuk")

View File

@@ -1,14 +1,13 @@
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.containers import Container, Horizontal, VerticalScroll
from textual.widgets import Placeholder
class PlaceholderApp(App):
CSS_PATH = "placeholder.css"
def compose(self) -> ComposeResult:
yield Vertical(
yield VerticalScroll(
Container(
Placeholder("This is a custom label for p1.", id="p1"),
Placeholder("Placeholder p2 here!", id="p2"),

View File

@@ -1,4 +1,4 @@
Vertical {
VerticalScroll {
align: center middle;
}

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Label, RadioButton, RadioSet
@@ -7,7 +7,7 @@ class RadioSetChangedApp(App[None]):
CSS_PATH = "radio_set_changed.css"
def compose(self) -> ComposeResult:
with Vertical():
with VerticalScroll():
with Horizontal():
with RadioSet():
yield RadioButton("Battlestar Galactica")

View File

@@ -134,8 +134,8 @@ exceeds the available horizontal space in the parent container.
## Utility containers
Textual comes with several "container" widgets.
These are [Vertical][textual.containers.Vertical], [Horizontal][textual.containers.Horizontal], and [Grid][textual.containers.Grid] which have the corresponding layout.
Textual comes with [several "container" widgets][textual.containers].
Among them, we have [Vertical][textual.containers.Vertical], [Horizontal][textual.containers.Horizontal], and [Grid][textual.containers.Grid] which have the corresponding layout.
The example below shows how we can combine these containers to create a simple 2x2 grid.
Inside a single `Horizontal` container, we place two `Vertical` containers.
@@ -163,8 +163,8 @@ However, Textual comes with a more powerful mechanism for achieving this known a
## Composing with context managers
In the previous section we've show how you add children to a container (such as `Horizontal` and `Vertical`) using positional arguments.
It's fine to do it this way, but Textual offers a simplified syntax using [context managers](https://docs.python.org/3/reference/datamodel.html#context-managers) which is generally easier to write and edit.
In the previous section, we've shown how you add children to a container (such as `Horizontal` and `Vertical`) using positional arguments.
It's fine to do it this way, but Textual offers a simplified syntax using [context managers](https://docs.python.org/3/reference/datamodel.html#context-managers), which is generally easier to write and edit.
When composing a widget, you can introduce a container using Python's `with` statement.
Any widgets yielded within that block are added as a child of the container.
@@ -202,7 +202,7 @@ Let's update the [utility containers](#utility-containers) example to use the co
```{.textual path="docs/examples/guide/layout/utility_containers_using_with.py"}
```
Note how the end result is the same, but the code with context managers is a little easer to read. It is up to you which method you want to use, and you can mix context managers with positional arguments if you like!
Note how the end result is the same, but the code with context managers is a little easier to read. It is up to you which method you want to use, and you can mix context managers with positional arguments if you like!
## Grid

View File

@@ -61,8 +61,8 @@ Open the CSS file tab to see the comments that explain how each height is comput
1. This sets the height to 2 lines.
2. This sets the height to 12.5% of the space made available by the container. The container is 24 lines tall, so 12.5% of 24 is 3.
3. This sets the height to 5% of the width of the direct container, which is the `Vertical` container. Because it expands to fit all of the terminal, the width of the `Vertical` is 80 and 5% of 80 is 4.
4. This sets the height to 12.5% of the height of the direct container, which is the `Vertical` container. Because it expands to fit all of the terminal, the height of the `Vertical` is 24 and 12.5% of 24 is 3.
3. This sets the height to 5% of the width of the direct container, which is the `VerticalScroll` container. Because it expands to fit all of the terminal, the width of the `VerticalScroll` is 80 and 5% of 80 is 4.
4. This sets the height to 12.5% of the height of the direct container, which is the `VerticalScroll` container. Because it expands to fit all of the terminal, the height of the `VerticalScroll` is 24 and 12.5% of 24 is 3.
5. This sets the height to 6.25% of the viewport width, which is 80. 6.25% of 80 is 5.
6. This sets the height to 12.5% of the viewport height, which is 24. 12.5% of 24 is 3.
7. This sets the height of the placeholder to be the optimal size that fits the content without scrolling.

View File

@@ -25,7 +25,7 @@ The default setting for containers is `overflow: auto auto`.
!!! warning
Some built-in containers like `Horizontal` and `Vertical` override these defaults.
Some built-in containers like `Horizontal` and `VerticalScroll` override these defaults.
## Example

View File

@@ -14,7 +14,7 @@ from rich.traceback import Traceback
from textual import events
from textual.app import App, ComposeResult
from textual.containers import Container, Vertical
from textual.containers import Container, VerticalScroll
from textual.reactive import var
from textual.widgets import DirectoryTree, Footer, Header, Static
@@ -40,7 +40,7 @@ class CodeBrowser(App):
yield Header()
with Container():
yield DirectoryTree(path, id="tree-view")
with Vertical(id="code-view"):
with VerticalScroll(id="code-view"):
yield Static(id="code", expand=True)
yield Footer()

View File

@@ -192,11 +192,10 @@ def align_lines(
style: Background style.
size: Size of container.
horizontal: Horizontal alignment.
vertical: Vertical alignment
vertical: Vertical alignment.
Returns:
Aligned lines.
"""
width, height = size

View File

@@ -1,6 +1,6 @@
from textual.app import App, ComposeResult
from textual.constants import BORDERS
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Button, Label
TEXT = """I must not fear.
@@ -12,7 +12,7 @@ And when it has gone past, I will turn the inner eye to see its path.
Where the fear has gone there will be nothing. Only I will remain."""
class BorderButtons(Vertical):
class BorderButtons(VerticalScroll):
DEFAULT_CSS = """
BorderButtons {
dock: left;

View File

@@ -1,11 +1,11 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.containers import Horizontal, VerticalScroll
from textual.design import ColorSystem
from textual.widget import Widget
from textual.widgets import Button, Footer, Label, Static
class ColorButtons(Vertical):
class ColorButtons(VerticalScroll):
def compose(self) -> ComposeResult:
for border in ColorSystem.COLOR_NAMES:
if border:
@@ -20,15 +20,15 @@ class ColorItem(Horizontal):
pass
class ColorGroup(Vertical):
class ColorGroup(VerticalScroll):
pass
class Content(Vertical):
class Content(VerticalScroll):
pass
class ColorsView(Vertical):
class ColorsView(VerticalScroll):
def compose(self) -> ComposeResult:
LEVELS = [
"darken-3",

View File

@@ -5,7 +5,7 @@ from rich.console import RenderableType
from textual._easing import EASING
from textual.app import App, ComposeResult
from textual.cli.previews.borders import TEXT
from textual.containers import Container, Horizontal, Vertical
from textual.containers import Container, Horizontal, VerticalScroll
from textual.reactive import reactive, var
from textual.scrollbar import ScrollBarRender
from textual.widget import Widget
@@ -73,7 +73,7 @@ class EasingApp(App):
)
yield EasingButtons()
with Vertical():
with VerticalScroll():
with Horizontal(id="inputs"):
yield Label("Animation Duration:", id="label")
yield duration_input

View File

@@ -14,11 +14,23 @@ class Container(Widget):
class Vertical(Widget):
"""A container widget which aligns children vertically."""
"""A container which arranges children vertically."""
DEFAULT_CSS = """
Vertical {
height: 1fr;
width: 1fr;
layout: vertical;
overflow: hidden hidden;
}
"""
class VerticalScroll(Widget):
"""A container which arranges children vertically, with an automatic vertical scrollbar."""
DEFAULT_CSS = """
VerticalScroll {
width: 1fr;
layout: vertical;
overflow-y: auto;
}
@@ -26,25 +38,61 @@ class Vertical(Widget):
class Horizontal(Widget):
"""A container widget which aligns children horizontally."""
"""A container which arranges children horizontally."""
DEFAULT_CSS = """
Horizontal {
height: 1fr;
layout: horizontal;
overflow-x: hidden;
overflow: hidden hidden;
}
"""
class HorizontalScroll(Widget):
"""A container which arranges children horizontally, with an automatic horizontal scrollbar."""
DEFAULT_CSS = """
HorizontalScroll {
height: 1fr;
layout: horizontal;
overflow-x: auto;
}
"""
class Center(Widget):
"""A container which centers children horizontally."""
DEFAULT_CSS = """
Center {
align-horizontal: center;
height: auto;
width: 1fr;
}
"""
class Middle(Widget):
"""A container which aligns children vertically in the middle."""
DEFAULT_CSS = """
Middle {
align-vertical: middle;
width: auto;
height: 1fr;
}
"""
class Grid(Widget):
"""A container widget with grid alignment."""
"""A container with grid alignment."""
DEFAULT_CSS = """
Grid {
height: 1fr;
layout: grid;
}
}
"""
@@ -52,7 +100,7 @@ class Content(Widget, can_focus=True, can_focus_children=False):
"""A container for content such as text."""
DEFAULT_CSS = """
Vertical {
VerticalScroll {
height: 1fr;
layout: vertical;
overflow-y: auto;

View File

@@ -4,7 +4,7 @@ from typing import ClassVar, Optional
from textual.await_remove import AwaitRemove
from textual.binding import Binding, BindingType
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.geometry import clamp
from textual.message import Message
from textual.reactive import reactive
@@ -12,7 +12,7 @@ from textual.widget import AwaitMount, Widget
from textual.widgets._list_item import ListItem
class ListView(Vertical, can_focus=True, can_focus_children=False):
class ListView(VerticalScroll, can_focus=True, can_focus_children=False):
"""A vertical list view widget.
Displays a vertical list of `ListItem`s which can be highlighted and

View File

@@ -10,7 +10,7 @@ from rich.text import Text
from typing_extensions import TypeAlias
from ..app import ComposeResult
from ..containers import Horizontal, Vertical
from ..containers import Horizontal, VerticalScroll
from ..message import Message
from ..reactive import reactive, var
from ..widget import Widget
@@ -266,7 +266,7 @@ class MarkdownBulletList(MarkdownList):
width: 1fr;
}
MarkdownBulletList Vertical {
MarkdownBulletList VerticalScroll {
height: auto;
width: 1fr;
}
@@ -277,7 +277,7 @@ class MarkdownBulletList(MarkdownList):
if isinstance(block, MarkdownListItem):
bullet = MarkdownBullet()
bullet.symbol = block.bullet
yield Horizontal(bullet, Vertical(*block._blocks))
yield Horizontal(bullet, VerticalScroll(*block._blocks))
self._blocks.clear()
@@ -295,7 +295,7 @@ class MarkdownOrderedList(MarkdownList):
width: 1fr;
}
MarkdownOrderedList Vertical {
MarkdownOrderedList VerticalScroll {
height: auto;
width: 1fr;
}
@@ -311,7 +311,7 @@ class MarkdownOrderedList(MarkdownList):
if isinstance(block, MarkdownListItem):
bullet = MarkdownBullet()
bullet.symbol = block.bullet.rjust(symbol_size + 1)
yield Horizontal(bullet, Vertical(*block._blocks))
yield Horizontal(bullet, VerticalScroll(*block._blocks))
self._blocks.clear()
@@ -410,7 +410,7 @@ class MarkdownListItem(MarkdownBlock):
height: auto;
}
MarkdownListItem > Vertical {
MarkdownListItem > VerticalScroll {
width: 1fr;
height: auto;
}
@@ -761,7 +761,7 @@ class MarkdownTableOfContents(Widget, can_focus_children=True):
)
class MarkdownViewer(Vertical, can_focus=True, can_focus_children=True):
class MarkdownViewer(VerticalScroll, can_focus=True, can_focus_children=True):
"""A Markdown viewer widget."""
DEFAULT_CSS = """

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,29 @@
"""
App to test alignment containers.
"""
from textual.app import App, ComposeResult
from textual.containers import Center, Middle
from textual.widgets import Button
class AlignContainersApp(App[None]):
CSS = """
Center {
tint: $primary 10%;
}
Middle {
tint: $secondary 10%;
}
"""
def compose(self) -> ComposeResult:
with Center():
yield Button.success("center")
with Middle():
yield Button.error("middle")
app = AlignContainersApp()
if __name__ == "__main__":
app.run()

View File

@@ -1,10 +1,9 @@
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Header, Footer, Label, Input
class InputWidthAutoApp(App[None]):
CSS = """
Input.auto {
width: auto;

View File

@@ -2,6 +2,7 @@ from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.widgets import Button
class WidgetDisableTestApp(App[None]):
CSS = """
Horizontal {
@@ -28,5 +29,6 @@ class WidgetDisableTestApp(App[None]):
yield Button(variant="warning")
yield Button(variant="error")
if __name__ == "__main__":
WidgetDisableTestApp().run()

View File

@@ -1,7 +1,7 @@
from rich.text import Text
from textual.app import App, ComposeResult, RenderResult
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Header, Footer
from textual.widget import Widget
@@ -32,7 +32,7 @@ class Tester(Widget, can_focus=True):
class StyleBugApp(App[None]):
def compose(self) -> ComposeResult:
yield Header()
with Vertical():
with VerticalScroll():
for n in range(40):
yield Tester(n)
yield Footer()

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Static
@@ -8,7 +8,6 @@ class StaticText(Static):
class FRApp(App):
CSS = """
StaticText {
height: 1fr;
@@ -39,7 +38,7 @@ class FRApp(App):
"""
def compose(self) -> ComposeResult:
yield Vertical(
yield VerticalScroll(
StaticText("HEADER", id="header"),
Horizontal(
StaticText("foo", id="foo"),

View File

@@ -1,7 +1,7 @@
from textual.app import App, ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Label
from textual.containers import Vertical, Container
from textual.containers import VerticalScroll, Container
class Overlay(Container):
@@ -9,12 +9,12 @@ class Overlay(Container):
yield Label("This should float over the top")
class Body1(Vertical):
class Body1(VerticalScroll):
def compose(self) -> ComposeResult:
yield Label("My God! It's full of stars! " * 300)
class Body2(Vertical):
class Body2(VerticalScroll):
def compose(self) -> ComposeResult:
yield Label("My God! It's full of stars! " * 300)
@@ -36,7 +36,6 @@ class Bad(Screen):
class Layers(App[None]):
CSS = """
Screen {
layers: base higher;

View File

@@ -0,0 +1,56 @@
"""
App to test layout containers.
"""
from typing import Iterable
from textual.app import App, ComposeResult
from textual.containers import (
Grid,
Horizontal,
HorizontalScroll,
Vertical,
VerticalScroll,
)
from textual.widget import Widget
from textual.widgets import Button, Input, Label
def sub_compose() -> Iterable[Widget]:
yield Button.success("Accept")
yield Button.error("Decline")
yield Input()
yield Label("\n\n".join([str(n * 1_000_000) for n in range(10)]))
class MyApp(App[None]):
CSS = """
Grid {
grid-size: 2 2;
grid-rows: 1fr;
grid-columns: 1fr;
}
Grid > Widget {
width: 100%;
height: 100%;
}
Input {
width: 80;
}
"""
def compose(self) -> ComposeResult:
with Grid():
with Horizontal():
yield from sub_compose()
with HorizontalScroll():
yield from sub_compose()
with Vertical():
yield from sub_compose()
with VerticalScroll():
yield from sub_compose()
app = MyApp()
if __name__ == "__main__":
app.run()

View File

@@ -1,7 +1,7 @@
from rich.text import Text
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widget import Widget
from textual.widgets import TextLog
@@ -21,32 +21,32 @@ class ScrollViewApp(App):
Screen {
align: center middle;
}
TextLog {
width:13;
height:10;
height:10;
}
Vertical{
VerticalScroll {
width:13;
height: 10;
overflow: scroll;
overflow-x: auto;
}
MyWidget {
width:13;
height:auto;
}
"""
def compose(self) -> ComposeResult:
yield TextLog()
yield Vertical(MyWidget())
yield VerticalScroll(MyWidget())
def on_ready(self) -> None:
self.query_one(TextLog).write("\n".join(f"{n} 0123456789" for n in range(20)))
self.query_one(Vertical).scroll_end(animate=False)
self.query_one(VerticalScroll).scroll_end(animate=False)
if __name__ == "__main__":

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Static
@@ -42,8 +42,8 @@ class NestedAutoApp(App[None]):
def compose(self) -> ComposeResult:
self._static = Static("", id="my-static")
yield Vertical(
Vertical(
yield VerticalScroll(
VerticalScroll(
self._static,
id="my-static-wrapper",
),

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Label, Static
@@ -18,7 +18,6 @@ class Box(Static):
class OffsetsApp(App):
CSS = """
#box1 {

View File

@@ -1,5 +1,5 @@
from textual.app import App
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import Static
@@ -10,7 +10,7 @@ class Visibility(App):
Screen {
layout: horizontal;
}
Vertical {
VerticalScroll {
width: 1fr;
border: solid red;
}
@@ -30,13 +30,12 @@ class Visibility(App):
"""
def compose(self):
yield Vertical(
yield VerticalScroll(
Static("foo"),
Static("float", classes="float"),
id="container1",
)
yield Vertical(
yield VerticalScroll(
Static("bar"),
Static("float", classes="float"),
id="container2",

View File

@@ -47,6 +47,14 @@ def test_dock_layout_sidebar(snap_compare):
assert snap_compare(LAYOUT_EXAMPLES_DIR / "dock_layout2_sidebar.py")
def test_layout_containers(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "layout_containers.py")
def test_alignment_containers(snap_compare):
assert snap_compare(SNAPSHOT_APPS_DIR / "alignment_containers.py")
# --- Widgets - rendering and basic interactions ---
# Each widget should have a canonical example that is display in the docs.
# When adding a new widget, ideally we should also create a snapshot test

100
tests/test_containers.py Normal file
View File

@@ -0,0 +1,100 @@
"""Test basic functioning of some containers."""
from textual.app import App, ComposeResult
from textual.containers import (
Center,
Horizontal,
HorizontalScroll,
Middle,
Vertical,
VerticalScroll,
)
from textual.widgets import Label
async def test_horizontal_vs_horizontalscroll_scrolling():
"""Check the default scrollbar behaviours for `Horizontal` and `HorizontalScroll`."""
class HorizontalsApp(App[None]):
CSS = """
Screen {
layout: vertical;
}
"""
def compose(self) -> ComposeResult:
with Horizontal():
for _ in range(10):
yield Label("How is life going? " * 3 + " | ")
with HorizontalScroll():
for _ in range(10):
yield Label("How is life going? " * 3 + " | ")
WIDTH = 80
HEIGHT = 24
app = HorizontalsApp()
async with app.run_test(size=(WIDTH, HEIGHT)):
horizontal = app.query_one(Horizontal)
horizontal_scroll = app.query_one(HorizontalScroll)
assert horizontal.size.height == horizontal_scroll.size.height
assert horizontal.scrollbars_enabled == (False, False)
assert horizontal_scroll.scrollbars_enabled == (False, True)
async def test_vertical_vs_verticalscroll_scrolling():
"""Check the default scrollbar behaviours for `Vertical` and `VerticalScroll`."""
class VerticalsApp(App[None]):
CSS = """
Screen {
layout: horizontal;
}
"""
def compose(self) -> ComposeResult:
with Vertical():
for _ in range(10):
yield Label("How is life going?\n" * 3 + "\n\n")
with VerticalScroll():
for _ in range(10):
yield Label("How is life going?\n" * 3 + "\n\n")
WIDTH = 80
HEIGHT = 24
app = VerticalsApp()
async with app.run_test(size=(WIDTH, HEIGHT)):
vertical = app.query_one(Vertical)
vertical_scroll = app.query_one(VerticalScroll)
assert vertical.size.width == vertical_scroll.size.width
assert vertical.scrollbars_enabled == (False, False)
assert vertical_scroll.scrollbars_enabled == (True, False)
async def test_center_container():
"""Check the size of the container `Center`."""
class CenterApp(App[None]):
def compose(self) -> ComposeResult:
with Center():
yield Label("<>\n<>\n<>")
app = CenterApp()
async with app.run_test():
center = app.query_one(Center)
assert center.size.width == app.size.width
assert center.size.height == 3
async def test_middle_container():
"""Check the size of the container `Middle`."""
class MiddleApp(App[None]):
def compose(self) -> ComposeResult:
with Middle():
yield Label("1234")
app = MiddleApp()
async with app.run_test():
middle = app.query_one(Middle)
assert middle.size.width == 4
assert middle.size.height == app.size.height

View File

@@ -1,7 +1,7 @@
"""Test Widget.disabled."""
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widgets import (
Button,
DataTable,
@@ -21,7 +21,7 @@ class DisableApp(App[None]):
def compose(self) -> ComposeResult:
"""Compose the child widgets."""
yield Vertical(
yield VerticalScroll(
Button(),
DataTable(),
DirectoryTree("."),
@@ -56,7 +56,7 @@ async def test_enabled_widgets_have_enabled_pseudo_class() -> None:
async def test_all_individually_disabled() -> None:
"""Post-disable all widgets should report being disabled."""
async with DisableApp().run_test() as pilot:
for node in pilot.app.screen.query("Vertical > *"):
for node in pilot.app.screen.query("VerticalScroll > *"):
node.disabled = True
assert all(
node.disabled for node in pilot.app.screen.query("#test-container > *")
@@ -77,7 +77,7 @@ async def test_disabled_widgets_have_disabled_pseudo_class() -> None:
async def test_disable_via_container() -> None:
"""All child widgets should appear (to CSS) as disabled by a container being disabled."""
async with DisableApp().run_test() as pilot:
pilot.app.screen.query_one("#test-container", Vertical).disabled = True
pilot.app.screen.query_one("#test-container", VerticalScroll).disabled = True
assert all(
node.has_pseudo_class("disabled") and not node.has_pseudo_class("enabled")
for node in pilot.app.screen.query("#test-container > *")

View File

@@ -151,11 +151,11 @@ def test_focus_next_and_previous_with_type_selector_without_self():
screen = app.screen
from textual.containers import Horizontal, Vertical
from textual.containers import Horizontal, VerticalScroll
from textual.widgets import Button, Input, Switch
screen._add_children(
Vertical(
VerticalScroll(
Horizontal(
Input(id="w3"),
Switch(id="w4"),

View File

@@ -1,19 +1,18 @@
"""Regression test for #1616 https://github.com/Textualize/textual/issues/1616"""
import pytest
from textual.app import App
from textual.containers import Vertical
from textual.containers import VerticalScroll
async def test_overflow_change_updates_virtual_size_appropriately():
class MyApp(App):
def compose(self):
yield Vertical()
yield VerticalScroll()
app = MyApp()
async with app.run_test() as pilot:
vertical = app.query_one(Vertical)
vertical = app.query_one(VerticalScroll)
height = vertical.virtual_size.height

View File

@@ -1,7 +1,7 @@
"""See https://github.com/Textualize/textual/issues/1355 as the motivation for these tests."""
from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.containers import VerticalScroll
from textual.widget import Widget
@@ -18,7 +18,7 @@ class VisibleTester(App[None]):
"""
def compose(self) -> ComposeResult:
yield Vertical(
yield VerticalScroll(
Widget(id="keep"), Widget(id="hide-via-code"), Widget(id="hide-via-css")
)