diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d3a6a573..fc686b822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking01.py b/docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking01.py index 6e0ab0949..9773da0c9 100644 --- a/docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking01.py +++ b/docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking01.py @@ -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() diff --git a/docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking02.py b/docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking02.py index 693bde821..0edefbeff 100644 --- a/docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking02.py +++ b/docs/blog/snippets/2022-12-07-responsive-app-background-task/blocking02.py @@ -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() diff --git a/docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py b/docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py index 2c96e5ed6..20f2daba8 100644 --- a/docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py +++ b/docs/blog/snippets/2022-12-07-responsive-app-background-task/nonblocking01.py @@ -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() diff --git a/docs/examples/events/dictionary.py b/docs/examples/events/dictionary.py index cf85c2bf6..51421ad39 100644 --- a/docs/examples/events/dictionary.py +++ b/docs/examples/events/dictionary.py @@ -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.""" diff --git a/docs/examples/guide/layout/combining_layouts.py b/docs/examples/guide/layout/combining_layouts.py index c52ecce0d..e608152cd 100644 --- a/docs/examples/guide/layout/combining_layouts.py +++ b/docs/examples/guide/layout/combining_layouts.py @@ -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"): diff --git a/docs/examples/styles/height_comparison.py b/docs/examples/styles/height_comparison.py index c679a68e6..41d8f0a76 100644 --- a/docs/examples/styles/height_comparison.py +++ b/docs/examples/styles/height_comparison.py @@ -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"), diff --git a/docs/examples/styles/max_width.py b/docs/examples/styles/max_width.py index 0ee440a92..7ea482dc7 100644 --- a/docs/examples/styles/max_width.py +++ b/docs/examples/styles/max_width.py @@ -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"), diff --git a/docs/examples/styles/min_width.css b/docs/examples/styles/min_width.css index 43dd8400a..ba2dd81c6 100644 --- a/docs/examples/styles/min_width.css +++ b/docs/examples/styles/min_width.css @@ -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 { diff --git a/docs/examples/styles/min_width.py b/docs/examples/styles/min_width.py index 25195c61d..b00881266 100644 --- a/docs/examples/styles/min_width.py +++ b/docs/examples/styles/min_width.py @@ -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"), diff --git a/docs/examples/styles/overflow.css b/docs/examples/styles/overflow.css index 3b68440c7..527429097 100644 --- a/docs/examples/styles/overflow.css +++ b/docs/examples/styles/overflow.css @@ -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; diff --git a/docs/examples/styles/overflow.py b/docs/examples/styles/overflow.py index b9a6c3383..debe0252d 100644 --- a/docs/examples/styles/overflow.py +++ b/docs/examples/styles/overflow.py @@ -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"), ) diff --git a/docs/examples/styles/visibility_containers.py b/docs/examples/styles/visibility_containers.py index 879b916fb..a94de145d 100644 --- a/docs/examples/styles/visibility_containers.py +++ b/docs/examples/styles/visibility_containers.py @@ -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(), diff --git a/docs/examples/widgets/button.css b/docs/examples/widgets/button.css index 5f1c906da..cb07fcaa1 100644 --- a/docs/examples/widgets/button.css +++ b/docs/examples/widgets/button.css @@ -2,7 +2,7 @@ Button { margin: 1 2; } -Horizontal > Vertical { +Horizontal > VerticalScroll { width: 24; } diff --git a/docs/examples/widgets/button.py b/docs/examples/widgets/button.py index 4c3509c32..09339ccb0 100644 --- a/docs/examples/widgets/button.py +++ b/docs/examples/widgets/button.py @@ -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), diff --git a/docs/examples/widgets/checkbox.css b/docs/examples/widgets/checkbox.css index 4e80b6685..b6e17c093 100644 --- a/docs/examples/widgets/checkbox.css +++ b/docs/examples/widgets/checkbox.css @@ -2,7 +2,7 @@ Screen { align: center middle; } -Vertical { +VerticalScroll { width: auto; height: auto; border: solid $primary; diff --git a/docs/examples/widgets/checkbox.py b/docs/examples/widgets/checkbox.py index 5ad6ca1ff..75eadda0c 100644 --- a/docs/examples/widgets/checkbox.py +++ b/docs/examples/widgets/checkbox.py @@ -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") diff --git a/docs/examples/widgets/placeholder.py b/docs/examples/widgets/placeholder.py index c09e9c412..9c7d6eb0e 100644 --- a/docs/examples/widgets/placeholder.py +++ b/docs/examples/widgets/placeholder.py @@ -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"), diff --git a/docs/examples/widgets/radio_set_changed.css b/docs/examples/widgets/radio_set_changed.css index 47583c1d8..fc1299294 100644 --- a/docs/examples/widgets/radio_set_changed.css +++ b/docs/examples/widgets/radio_set_changed.css @@ -1,4 +1,4 @@ -Vertical { +VerticalScroll { align: center middle; } diff --git a/docs/examples/widgets/radio_set_changed.py b/docs/examples/widgets/radio_set_changed.py index c360cb868..c817b6e6f 100644 --- a/docs/examples/widgets/radio_set_changed.py +++ b/docs/examples/widgets/radio_set_changed.py @@ -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") diff --git a/docs/guide/layout.md b/docs/guide/layout.md index ba142f9ad..3564208a9 100644 --- a/docs/guide/layout.md +++ b/docs/guide/layout.md @@ -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 diff --git a/docs/styles/height.md b/docs/styles/height.md index 15d5399a3..bd3fe3512 100644 --- a/docs/styles/height.md +++ b/docs/styles/height.md @@ -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. diff --git a/docs/styles/overflow.md b/docs/styles/overflow.md index 12edaac15..d3f0ece5f 100644 --- a/docs/styles/overflow.md +++ b/docs/styles/overflow.md @@ -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 diff --git a/examples/code_browser.py b/examples/code_browser.py index 4616be4f7..13fdf3c6d 100644 --- a/examples/code_browser.py +++ b/examples/code_browser.py @@ -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() diff --git a/src/textual/_segment_tools.py b/src/textual/_segment_tools.py index 5f9df333b..79f3bc077 100644 --- a/src/textual/_segment_tools.py +++ b/src/textual/_segment_tools.py @@ -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 diff --git a/src/textual/cli/previews/borders.py b/src/textual/cli/previews/borders.py index a33320a0a..09dbcaab4 100644 --- a/src/textual/cli/previews/borders.py +++ b/src/textual/cli/previews/borders.py @@ -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; diff --git a/src/textual/cli/previews/colors.py b/src/textual/cli/previews/colors.py index b9d8da3eb..d8028f7c5 100644 --- a/src/textual/cli/previews/colors.py +++ b/src/textual/cli/previews/colors.py @@ -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", diff --git a/src/textual/cli/previews/easing.py b/src/textual/cli/previews/easing.py index 70dc45758..7bca58efa 100644 --- a/src/textual/cli/previews/easing.py +++ b/src/textual/cli/previews/easing.py @@ -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 diff --git a/src/textual/containers.py b/src/textual/containers.py index bbd4c13d7..a2ddbf422 100644 --- a/src/textual/containers.py +++ b/src/textual/containers.py @@ -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; diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index 013c51ead..8f95d011d 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -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 diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 8e36b3e5e..36931fd59 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -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 = """ diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index c142d5d3d..f2855c724 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -1,3 +1,165 @@ +# name: test_alignment_containers + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AlignContainersApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + center + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + middle + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + + + + + ''' +# --- # name: test_auto_table ''' @@ -15027,6 +15189,172 @@ ''' # --- +# name: test_layout_containers + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MyApp + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + AcceptDeclineAcceptDecline + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + + + + + + + + + + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + AcceptAccept + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + DeclineDecline + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▆▆ + ▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔ + + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ + 00 + + 10000001000000 + + + + + ''' +# --- # name: test_line_api_scrollbars ''' diff --git a/tests/snapshot_tests/snapshot_apps/alignment_containers.py b/tests/snapshot_tests/snapshot_apps/alignment_containers.py new file mode 100644 index 000000000..c419f29e9 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/alignment_containers.py @@ -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() diff --git a/tests/snapshot_tests/snapshot_apps/auto_width_input.py b/tests/snapshot_tests/snapshot_apps/auto_width_input.py index 35cc3b2ec..e312e653e 100644 --- a/tests/snapshot_tests/snapshot_apps/auto_width_input.py +++ b/tests/snapshot_tests/snapshot_apps/auto_width_input.py @@ -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; diff --git a/tests/snapshot_tests/snapshot_apps/disable_widgets.py b/tests/snapshot_tests/snapshot_apps/disable_widgets.py index 6677fa5de..e6465516f 100644 --- a/tests/snapshot_tests/snapshot_apps/disable_widgets.py +++ b/tests/snapshot_tests/snapshot_apps/disable_widgets.py @@ -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() diff --git a/tests/snapshot_tests/snapshot_apps/focus_component_class.py b/tests/snapshot_tests/snapshot_apps/focus_component_class.py index 85becd79b..77ac46212 100644 --- a/tests/snapshot_tests/snapshot_apps/focus_component_class.py +++ b/tests/snapshot_tests/snapshot_apps/focus_component_class.py @@ -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() diff --git a/tests/snapshot_tests/snapshot_apps/fr_units.py b/tests/snapshot_tests/snapshot_apps/fr_units.py index d9bcddeb9..1ece79ebc 100644 --- a/tests/snapshot_tests/snapshot_apps/fr_units.py +++ b/tests/snapshot_tests/snapshot_apps/fr_units.py @@ -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"), diff --git a/tests/snapshot_tests/snapshot_apps/layer_order_independence.py b/tests/snapshot_tests/snapshot_apps/layer_order_independence.py index 2ad43e931..a3deec8ba 100644 --- a/tests/snapshot_tests/snapshot_apps/layer_order_independence.py +++ b/tests/snapshot_tests/snapshot_apps/layer_order_independence.py @@ -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; diff --git a/tests/snapshot_tests/snapshot_apps/layout_containers.py b/tests/snapshot_tests/snapshot_apps/layout_containers.py new file mode 100644 index 000000000..c06b6270c --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/layout_containers.py @@ -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() diff --git a/tests/snapshot_tests/snapshot_apps/line_api_scrollbars.py b/tests/snapshot_tests/snapshot_apps/line_api_scrollbars.py index 68a16a099..26ab2c985 100644 --- a/tests/snapshot_tests/snapshot_apps/line_api_scrollbars.py +++ b/tests/snapshot_tests/snapshot_apps/line_api_scrollbars.py @@ -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__": diff --git a/tests/snapshot_tests/snapshot_apps/nested_auto_heights.py b/tests/snapshot_tests/snapshot_apps/nested_auto_heights.py index 5df6c2960..4ed40f10e 100644 --- a/tests/snapshot_tests/snapshot_apps/nested_auto_heights.py +++ b/tests/snapshot_tests/snapshot_apps/nested_auto_heights.py @@ -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", ), diff --git a/tests/snapshot_tests/snapshot_apps/offsets.py b/tests/snapshot_tests/snapshot_apps/offsets.py index 12099b126..23d21bb38 100644 --- a/tests/snapshot_tests/snapshot_apps/offsets.py +++ b/tests/snapshot_tests/snapshot_apps/offsets.py @@ -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 { diff --git a/tests/snapshot_tests/snapshot_apps/visibility.py b/tests/snapshot_tests/snapshot_apps/visibility.py index a5ccccb7e..7dbfcf7b4 100644 --- a/tests/snapshot_tests/snapshot_apps/visibility.py +++ b/tests/snapshot_tests/snapshot_apps/visibility.py @@ -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", diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index d3196ae48..7769c4a62 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -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 diff --git a/tests/test_containers.py b/tests/test_containers.py new file mode 100644 index 000000000..22a4ad0ec --- /dev/null +++ b/tests/test_containers.py @@ -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 diff --git a/tests/test_disabled.py b/tests/test_disabled.py index 850fcf7c7..bc0692ad0 100644 --- a/tests/test_disabled.py +++ b/tests/test_disabled.py @@ -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 > *") diff --git a/tests/test_focus.py b/tests/test_focus.py index b4d9ce8c5..a03b9b53c 100644 --- a/tests/test_focus.py +++ b/tests/test_focus.py @@ -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"), diff --git a/tests/test_overflow_change.py b/tests/test_overflow_change.py index 236a15f8c..7f5d7eb46 100644 --- a/tests/test_overflow_change.py +++ b/tests/test_overflow_change.py @@ -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 diff --git a/tests/test_visibility_change.py b/tests/test_visibility_change.py index f79f18a6f..7006827ee 100644 --- a/tests/test_visibility_change.py +++ b/tests/test_visibility_change.py @@ -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") )