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

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")
)