new align

This commit is contained in:
Will McGugan
2022-09-27 16:35:40 +01:00
parent fc4ec8ee50
commit d962dcd49c
80 changed files with 336 additions and 687 deletions

View File

@@ -6,9 +6,8 @@ except ImportError:
raise ImportError("Please install httpx with 'pip install httpx' ") raise ImportError("Please install httpx with 'pip install httpx' ")
from rich.json import JSON from rich.json import JSON
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Vertical from textual.containers import Vertical
from textual.widgets import Static, TextInput from textual.widgets import Static, TextInput

View File

@@ -1,6 +1,6 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container, Horizontal from textual.containers import Container, Horizontal
from textual.widgets import Header, Footer, Static, Button from textual.widgets import Button, Footer, Header, Static
QUESTION = "Do you want to learn about Textual CSS?" QUESTION = "Do you want to learn about Textual CSS?"

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container, Horizontal from textual.containers import Container, Horizontal
from textual.widgets import Header, Footer, Static, Button from textual.widgets import Header, Footer, Static, Button
QUESTION = "Do you want to learn about Textual CSS?" QUESTION = "Do you want to learn about Textual CSS?"

View File

@@ -1,6 +1,6 @@
from textual import events from textual import events
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container from textual.containers import Container
from textual.widgets import Static, TextLog from textual.widgets import Static, TextLog

View File

@@ -1,21 +0,0 @@
Screen {
layout: center;
}
#bottom {
width: 20;
height: 20;
background: red;
}
#middle {
width: 26;
height: 12;
background: green;
}
#top {
width: 32;
height: 6;
background: blue;
}

View File

@@ -1,16 +0,0 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
class CenterLayoutExample(App):
CSS_PATH = "center_layout.css"
def compose(self) -> ComposeResult:
yield Static("One", id="bottom")
yield Static("Two", id="middle")
yield Static("Three", id="top")
if __name__ == "__main__":
app = CenterLayoutExample()
app.run()

View File

@@ -1,4 +1,4 @@
from textual import layout from textual.containers import Container, Horizontal, Vertical
from textual.app import ComposeResult, App from textual.app import ComposeResult, App
from textual.widgets import Static, Header from textual.widgets import Static, Header
@@ -8,19 +8,19 @@ class CombiningLayoutsExample(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Header() yield Header()
yield layout.Container( yield Container(
layout.Vertical( Vertical(
*[Static(f"Vertical layout, child {number}") for number in range(15)], *[Static(f"Vertical layout, child {number}") for number in range(15)],
id="left-pane", id="left-pane",
), ),
layout.Horizontal( Horizontal(
Static("Horizontally"), Static("Horizontally"),
Static("Positioned"), Static("Positioned"),
Static("Children"), Static("Children"),
Static("Here"), Static("Here"),
id="top-right", id="top-right",
), ),
layout.Container( Container(
Static("This"), Static("This"),
Static("panel"), Static("panel"),
Static("is"), Static("is"),
@@ -31,14 +31,6 @@ class CombiningLayoutsExample(App):
id="app-grid", id="app-grid",
) )
async def on_key(self, event) -> None:
await self.dispatch_key(event)
def key_a(self):
print(self.stylesheet.variables["boost"])
print(self.stylesheet.variables["boost-lighten-1"])
print(self.stylesheet.variables["boost-lighten-2"])
if __name__ == "__main__": if __name__ == "__main__":
app = CombiningLayoutsExample() app = CombiningLayoutsExample()

View File

@@ -1,6 +1,6 @@
Screen { Screen {
layout: grid; layout: grid;
grid-size: 3; grid-size: 3 2;
} }
.box { .box {

View File

@@ -3,7 +3,7 @@ from textual.widgets import Static
class GridLayoutExample(App): class GridLayoutExample(App):
CSS_PATH = "grid_layout1.css" CSS_PATH = "grid_layout2.css"
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield Static("One", classes="box") yield Static("One", classes="box")

View File

@@ -1,5 +1,5 @@
Screen { Screen {
layout: center; align: center middle;
layers: below above; layers: below above;
} }

View File

@@ -1,36 +0,0 @@
Screen {
layout: center;
}
#parent {
layout: center;
background: #9e9e9e;
width: 60;
height: 20;
}
Box {
color: auto;
width: auto;
height: auto;
padding: 1 2;
}
#box1 {
background: $primary;
}
#box2 {
background: $secondary;
offset: 12 4;
}
#box3 {
background: lightseagreen;
offset: -12 -4;
}
#box4 {
background: darkred;
offset: -26 10;
}

View File

@@ -1,29 +0,0 @@
from rich.console import RenderableType
from textual import layout
from textual.app import App, ComposeResult
from textual.widgets import Static
class Box(Static):
def render(self) -> RenderableType:
x, y = self.styles.offset
return f"{self.id}: offset = ({x}, {y})"
class OffsetExample(App):
CSS_PATH = "offset.css"
def compose(self) -> ComposeResult:
yield layout.Container(
Box(id="box1"),
Box(id="box2"),
Box(id="box3"),
Box(id="box4"),
id="parent",
)
if __name__ == "__main__":
app = OffsetExample()
app.run()

View File

@@ -1,5 +1,5 @@
from textual import layout
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical
from textual.widgets import Static from textual.widgets import Static
@@ -7,13 +7,13 @@ class UtilityContainersExample(App):
CSS_PATH = "utility_containers.css" CSS_PATH = "utility_containers.css"
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield layout.Horizontal( yield Horizontal(
layout.Vertical( Vertical(
Static("One"), Static("One"),
Static("Two"), Static("Two"),
classes="column", classes="column",
), ),
layout.Vertical( Vertical(
Static("Three"), Static("Three"),
Static("Four"), Static("Four"),
classes="column", classes="column",

View File

@@ -1,5 +1,5 @@
Screen { Screen {
layout: center; align: center middle;
} }
FizzBuzz { FizzBuzz {

View File

@@ -1,5 +1,5 @@
Screen { Screen {
layout: center; align: center middle;
} }
FizzBuzz { FizzBuzz {

View File

@@ -1,3 +1,3 @@
Screen { Screen {
layout: center; align: center middle;
} }

View File

@@ -1,5 +1,5 @@
Screen { Screen {
layout: center; align: center middle;
} }
Hello { Hello {

View File

@@ -1,5 +1,5 @@
Screen { Screen {
layout: center; align: center middle;
} }
Hello { Hello {

View File

@@ -1,3 +1,3 @@
Screen { Screen {
layout: center; align: center middle;
} }

View File

@@ -1,5 +1,5 @@
Screen { Screen {
layout: center; align: center middle;
} }
Hello { Hello {

View File

@@ -0,0 +1,13 @@
Screen {
align: center middle;
}
.box {
width: 40;
height: 5;
margin: 1;
padding: 1;
background: green;
color: white 90%;
border: heavy white;
}

View File

@@ -0,0 +1,11 @@
from textual.app import App
from textual.widgets import Static
class AlignApp(App):
def compose(self):
yield Static("Vertical alignment with [b]Textual[/]", classes="box")
yield Static("Take note, browsers.", classes="box")
app = AlignApp(css_path="align.css")

View File

@@ -10,12 +10,6 @@
height: auto; height: auto;
} }
#center-layout {
layout: center;
background: darkslateblue;
height: 7;
}
Static { Static {
margin: 1; margin: 1;
width: 12; width: 12;

View File

@@ -1,27 +1,22 @@
from textual import layout
from textual.app import App from textual.app import App
from textual.widget import Widget from textual.containers import Container
from textual.widgets import Static from textual.widgets import Static
class LayoutApp(App): class LayoutApp(App):
def compose(self): def compose(self):
yield layout.Container( yield Container(
Static("Layout"), Static("Layout"),
Static("Is"), Static("Is"),
Static("Vertical"), Static("Vertical"),
id="vertical-layout", id="vertical-layout",
) )
yield layout.Container( yield Container(
Static("Layout"), Static("Layout"),
Static("Is"), Static("Is"),
Static("Horizontal"), Static("Horizontal"),
id="horizontal-layout", id="horizontal-layout",
) )
yield layout.Container(
Static("Center"),
id="center-layout",
)
app = LayoutApp(css_path="layout.css") app = LayoutApp(css_path="layout.css")

View File

@@ -1,6 +1,6 @@
from textual.app import App from textual.app import App
from textual.widgets import Static from textual.widgets import Static
from textual.layout import Horizontal, Vertical from textual.containers import Horizontal, Vertical
TEXT = """I must not fear. TEXT = """I must not fear.
Fear is the mind-killer. Fear is the mind-killer.

View File

@@ -1,5 +1,5 @@
from textual.app import App from textual.app import App
from textual import layout from textual.containers import Vertical
from textual.widgets import Static from textual.widgets import Static
TEXT = """I must not fear. TEXT = """I must not fear.
@@ -14,7 +14,7 @@ Where the fear has gone there will be nothing. Only I will remain.
class ScrollbarApp(App): class ScrollbarApp(App):
def compose(self): def compose(self):
yield layout.Vertical(Static(TEXT * 5), classes="panel") yield Vertical(Static(TEXT * 5), classes="panel")
app = ScrollbarApp(css_path="scrollbar_size.css") app = ScrollbarApp(css_path="scrollbar_size.css")

View File

@@ -1,5 +1,5 @@
from textual.app import App from textual.app import App
from textual import layout from textual.containers import Vertical
from textual.widgets import Static from textual.widgets import Static
TEXT = """I must not fear. TEXT = """I must not fear.
@@ -14,8 +14,8 @@ Where the fear has gone there will be nothing. Only I will remain.
class ScrollbarApp(App): class ScrollbarApp(App):
def compose(self): def compose(self):
yield layout.Vertical(Static(TEXT * 5), classes="panel1") yield Vertical(Static(TEXT * 5), classes="panel1")
yield layout.Vertical(Static(TEXT * 5), classes="panel2") yield Vertical(Static(TEXT * 5), classes="panel2")
app = ScrollbarApp(css_path="scrollbars.css") app = ScrollbarApp(css_path="scrollbars.css")

View File

@@ -1,22 +0,0 @@
from datetime import datetime
from textual.app import App
from textual.widget import Widget
class Clock(Widget):
def on_mount(self):
self.styles.content_align = ("center", "middle")
self.auto_refresh = 1.0
def render(self):
return datetime.now().strftime("%X")
class ClockApp(App):
def compose(self):
yield Clock()
app = ClockApp()
app.run()

View File

@@ -1,22 +0,0 @@
from datetime import datetime
from textual.app import App
from textual.widget import Widget
class Clock(Widget):
def on_mount(self):
self.styles.content_align = ("center", "middle")
self.auto_refresh = 1.0
def render(self):
return datetime.now().strftime("%c")
class ClockApp(App):
def compose(self):
yield Clock()
app = ClockApp()
app.run()

View File

@@ -1,7 +1,7 @@
from time import monotonic from time import monotonic
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container from textual.containers import Container
from textual.reactive import reactive from textual.reactive import reactive
from textual.widgets import Button, Header, Footer, Static from textual.widgets import Button, Header, Footer, Static

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container from textual.containers import Container
from textual.widgets import Button, Header, Footer, Static from textual.widgets import Button, Header, Footer, Static

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container from textual.containers import Container
from textual.widgets import Button, Header, Footer, Static from textual.widgets import Button, Header, Footer, Static

View File

@@ -1,5 +1,5 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container from textual.containers import Container
from textual.widgets import Button, Header, Footer, Static from textual.widgets import Button, Header, Footer, Static

View File

@@ -1,7 +1,7 @@
from time import monotonic from time import monotonic
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container from textual.containers import Container
from textual.reactive import reactive from textual.reactive import reactive
from textual.widgets import Button, Header, Footer, Static from textual.widgets import Button, Header, Footer, Static

View File

@@ -1,7 +1,7 @@
from time import monotonic from time import monotonic
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container from textual.containers import Container
from textual.reactive import reactive from textual.reactive import reactive
from textual.widgets import Button, Header, Footer, Static from textual.widgets import Button, Header, Footer, Static

View File

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

View File

@@ -1,17 +1,12 @@
# Textual CSS # Textual CSS
Textual uses CSS to apply style to widgets. If you have any exposure to web development you will have encountered CSS, but don't worry if you haven't: this section will get you up to speed. Textual uses CSS to apply style to widgets. If you have any exposure to web development you will have encountered CSS, but don't worry if you haven't: this chapter will get you up to speed.
## Stylesheets ## Stylesheets
CSS stands for _Cascading Stylesheets_. A stylesheet is a list of styles and rules about how those styles should be applied to a page. In the case of Textual, the stylesheet applies [styles](./styles.md) to widgets but otherwise it is the same idea. CSS stands for _Cascading Stylesheets_. A stylesheet is a list of styles and rules about how those styles should be applied to a web page. In the case of Textual, the stylesheet applies [styles](./styles.md) to widgets but otherwise it is the same idea.
!!! note When Textual loads CSS it sets attributes on your widgets' `style` object. The effect is the same as if you had set attributes in Python.
Depending on what you want to build with Textual, you may not need to learn Textual CSS at all. Widgets are packaged with CSS styles so apps may not need any additional CSS.
When Textual loads CSS it sets attributes of your widgets's `style` object. The effect is the same as if you had set attributes in Python.
CSS is typically stored in an external file with the extension `.css` alongside your Python code. CSS is typically stored in an external file with the extension `.css` alongside your Python code.
@@ -88,10 +83,11 @@ This doesn't look much like a tree yet. Let's add a header and a footer to this
=== "dom2.py" === "dom2.py"
```python ```python hl_lines="7 8"
--8<-- "docs/examples/guide/dom2.py" --8<-- "docs/examples/guide/dom2.py"
``` ```
=== "Output" === "Output"
```{.textual path="docs/examples/guide/dom2.py"} ```{.textual path="docs/examples/guide/dom2.py"}
@@ -105,7 +101,7 @@ With a header and a footer widget the DOM looks the this:
!!! note !!! note
We've simplified the above example somewhat. Both the Header and Footer widgets contain children of their own. When building an app with pre-built widgets you rarely need to know how they are constructed unless you plan on changing the styles for the individual components. We've simplified the above example somewhat. Both the Header and Footer widgets contain children of their own. When building an app with pre-built widgets you rarely need to know how they are constructed unless you plan on changing the styles of individual components.
Both Header and Footer are children of the Screen object. Both Header and Footer are children of the Screen object.
@@ -432,8 +428,3 @@ Let's say we define a variable `$success: lime;`.
Our `$border` variable could then be updated to `$border: wide $success;`, which will Our `$border` variable could then be updated to `$border: wide $success;`, which will
be translated to `$border: wide lime;`. be translated to `$border: wide lime;`.
Textual CSS ships with a number of builtin variables.
These can be used in CSS without any additional imports or declarations.
For more information on these builtin variables, see [this page](#).
[//]: # (TODO: Fill in the link above when builtin style variables are documented)

View File

@@ -40,7 +40,7 @@ If you hit ++ctrl+c++ Textual will exit application mode and return you to the c
## Events ## Events
Textual has an event system you can use to respond to key presses, mouse actions, and internal state changes. Event handlers are methods which are prefixed with `on_` followed by the name of the event. Textual has an event system you can use to respond to key presses, mouse actions, and internal state changes. Event handlers are methods prefixed with `on_` followed by the name of the event.
One such event is the *mount* event which is sent to an application after it enters application mode. You can respond to this event by defining a method called `on_mount`. One such event is the *mount* event which is sent to an application after it enters application mode. You can respond to this event by defining a method called `on_mount`.
@@ -59,15 +59,15 @@ The `on_mount` handler sets the `self.screen.styles.background` attribute to `"d
```{.textual path="docs/examples/app/event01.py" hl_lines="23-25"} ```{.textual path="docs/examples/app/event01.py" hl_lines="23-25"}
``` ```
The key event handler (`on_key`) specifies an `event` parameter which will receive a [Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list. The key event handler (`on_key`) has an `event` parameter which will receive a [Key][textual.events.Key] instance. Every event has an associated event object which will be passed to the handler method if it is present in the method's parameter list.
!!! note !!! note
It is unusual (but not unprecedented) for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it. It is unusual (but not unprecedented) for a method's parameters to affect how it is called. Textual accomplishes this by inspecting the method prior to calling it.
For some events, such as the key event, the event object contains additional information. In the case of [Key][textual.events.Key] it will contain the key that was pressed. For some events contains additional information. In the case of [Key][textual.events.Key] it will contain the key that was pressed.
The `on_key` method above uses the `key` attribute on the Key event to change the background color if any of the keys ++0++ to ++9++ are pressed. The `on_key` method above changes the background color if any of the keys from ++0++ to ++9++ are pressed.
### Async events ### Async events
@@ -81,7 +81,7 @@ Textual knows to *await* your event handlers if they are coroutines (i.e. prefix
## Widgets ## Widgets
Widgets are self-contained components responsible for generating the output for a portion of the screen and can respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface. Widgets are self-contained components responsible for generating the output for a portion of the screen. Widgets respond to events in much the same way as the App. Most apps that do anything interesting will contain at least one (and probably many) widgets which together form a User Interface.
Widgets can be as simple as a piece of text, a button, or a fully-fledge component like a text editor or file browser (which may contain widgets of their own). Widgets can be as simple as a piece of text, a button, or a fully-fledge component like a text editor or file browser (which may contain widgets of their own).
@@ -106,7 +106,7 @@ Notice the `on_button_pressed` method which handles the [Button.Pressed][textual
While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling [mount()][textual.widget.Widget.mount] which will add a new widget to the UI. While composing is the preferred way of adding widgets when your app starts it is sometimes necessary to add new widget(s) in response to events. You can do this by calling [mount()][textual.widget.Widget.mount] which will add a new widget to the UI.
Here's an app which adds the welcome widget in response to any key press: Here's an app which adds a welcome widget in response to any key press:
```python title="widgets02.py" ```python title="widgets02.py"
--8<-- "docs/examples/app/widgets02.py" --8<-- "docs/examples/app/widgets02.py"

View File

@@ -172,7 +172,7 @@ Let's look at an example which looks up word definitions from an [api](https://d
=== "dictionary.py" === "dictionary.py"
```python title="dictionary.py" hl_lines="28" ```python title="dictionary.py" hl_lines="27"
--8<-- "docs/examples/events/dictionary.py" --8<-- "docs/examples/events/dictionary.py"
``` ```
=== "dictionary.css" === "dictionary.css"

View File

@@ -188,7 +188,7 @@ Textual will send a [Enter](../events/enter.md) event to a widget when the mouse
### Click events ### Click events
There are three events associated with clicking a button on your mouse. When the button is initially pressed, Textual sends a [MouseDown](../events/mouse_down.md) event, followed by [MouseUp](../events/mouse_up.md) when the button is released. Textual then sends a final [Click](../events/mouse_click.md) event. There are three events associated with clicking a button on your mouse. When the button is initially pressed, Textual sends a [MouseDown](../events/mouse_down.md) event, followed by [MouseUp](../events/mouse_up.md) when the button is released. Textual then sends a final [Click](../events/click.md) event.
If you want your app to respond to a mouse click you should prefer the Click event (and not MouseDown or MouseUp). This is because a future version of Textual may support other pointing devices which don't have up and down states. If you want your app to respond to a mouse click you should prefer the Click event (and not MouseDown or MouseUp). This is because a future version of Textual may support other pointing devices which don't have up and down states.

View File

@@ -132,42 +132,10 @@ To enable horizontal scrolling, we can use the `overflow-x: auto;` declaration:
With `overflow-x: auto;`, Textual automatically adds a horizontal scrollbar since the width of the children With `overflow-x: auto;`, Textual automatically adds a horizontal scrollbar since the width of the children
exceeds the available horizontal space in the parent container. exceeds the available horizontal space in the parent container.
## Center
The `center` layout will place a widget directly in the center of the container.
<div class="excalidraw">
--8<-- "docs/images/layout/center.excalidraw.svg"
</div>
If there's more than one child widget inside a container using `center` layout,
the child widgets will be "stacked" on top of each other, as demonstrated below.
=== "Output"
```{.textual path="docs/examples/guide/layout/center_layout.py"}
```
=== "center_layout.py"
```python
--8<-- "docs/examples/guide/layout/center_layout.py"
```
=== "center_layout.css"
```sass hl_lines="2"
--8<-- "docs/examples/guide/layout/center_layout.css"
```
Widgets are drawn in the order they are yielded from `compose`.
The first yielded widget appears at the bottom, and widgets yielded after it are stacked on top.
## Utility containers ## Utility containers
Textual comes with several "container" widgets. Textual comes with several "container" widgets.
These are `layout.Vertical`, `layout.Horizontal`, and `layout.Center`. These are [Vertical][textual.containers.Vertical], [Horizontal][textual.containers.Horizontal], and [Grid][textual.containers.Grid] which have the corresponding layout.
Internally, these widgets contain some default CSS containing a `layout` declaration.
The example below shows how we can combine these containers to create a simple 2x2 grid. 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. Inside a single `Horizontal` container, we place two `Vertical` containers.
@@ -205,24 +173,11 @@ The diagram below hints at what can be achieved using `layout: grid`.
!!! note !!! note
Grid layouts in Textual have very little in common with browser-based CSS Grid. Grid layouts in Textual have little in common with browser-based CSS Grid.
To get started with grid layout, you define the number of columns and rows in your grid using the `grid-size` CSS property and set `layout: grid`. To get started with grid layout, define the number of columns and rows in your grid with the `grid-size` CSS property and set `layout: grid`. Widgets are inserted into the "cells" of the grid from left-to-right and top-to-bottom order.
When you yield widgets from the `compose` method, they're inserted into the "cells" of your grid in left-to-right, top-to-bottom order.
For example, `grid-size: 3 2;` defines a grid with 3 columns and 2 rows. The following example creates a 3 x 2 grid and adds six widgets to it
We can now yield 6 widgets from our `compose` method, and they'll be inserted into all available cells in the grid.
```{.textual path="docs/examples/guide/layout/grid_layout1.py"}
```
If we were to yield a seventh widget from our `compose` method, it would not be visible as the grid does not contain enough cells to accommodate it.
We can optionally omit the number of rows from `grid-size`, and Textual will create them "on-demand" based on the number of widgets yielded from `compose`.
Widgets will be inserted into the grid in the order they're yielded, and when all cells in a row become occupied, a new row will be created to accommodate the next widget.
Let's create a simple grid with three columns. In our CSS, we'll specify this using `grid-size: 3`.
Then, we'll yield six widgets from `compose`, in order to fully occupy two rows in the grid.
=== "Output" === "Output"
@@ -241,11 +196,26 @@ Then, we'll yield six widgets from `compose`, in order to fully occupy two rows
--8<-- "docs/examples/guide/layout/grid_layout1.css" --8<-- "docs/examples/guide/layout/grid_layout1.css"
``` ```
To further illustrate the "on-demand" nature of `layout: grid` when the number of rows is omitted, here's what happens when you modify the example
above to yield an additional widget (for a total of seven widgets).
```{.textual path="docs/examples/guide/layout/grid_layout2.py"} If we were to yield a seventh widget from our `compose` method, it would not be visible as the grid does not contain enough cells to accommodate it. We can tell Textual to add new rows on demand to fit the number of widgets, by omitting the number of rows from `grid-size`. The following example creates a grid with three columns, with rows created on demand:
```
=== "Output"
```{.textual path="docs/examples/guide/layout/grid_layout2.py"}
```
=== "grid_layout2.py"
```python
--8<-- "docs/examples/guide/layout/grid_layout2.py"
```
=== "grid_layout2.css"
```sass hl_lines="3"
--8<-- "docs/examples/guide/layout/grid_layout2.css"
```
Since we specified that our grid has three columns (`grid-size: 3`), and we've yielded seven widgets in total, Since we specified that our grid has three columns (`grid-size: 3`), and we've yielded seven widgets in total,
a third row has been created to accommodate the seventh widget. a third row has been created to accommodate the seventh widget.
@@ -258,9 +228,10 @@ customize it to create more complex layouts.
You can adjust the width of columns and the height of rows in your grid using the `grid-columns` and `grid-rows` properties. You can adjust the width of columns and the height of rows in your grid using the `grid-columns` and `grid-rows` properties.
These properties can take multiple values, letting you specify dimensions on a column-by-column or row-by-row basis. These properties can take multiple values, letting you specify dimensions on a column-by-column or row-by-row basis.
Continuing on from our earlier 2x3 example grid, let's adjust the width of the columns using `grid-columns`. Continuing on from our earlier 3x2 example grid, let's adjust the width of the columns using `grid-columns`.
We'll make the first column take up half of the screen width, with the other two columns sharing the remaining space equally. We'll make the first column take up half of the screen width, with the other two columns sharing the remaining space equally.
=== "Output" === "Output"
```{.textual path="docs/examples/guide/layout/grid_layout3_row_col_adjust.py"} ```{.textual path="docs/examples/guide/layout/grid_layout3_row_col_adjust.py"}
@@ -278,7 +249,8 @@ We'll make the first column take up half of the screen width, with the other two
--8<-- "docs/examples/guide/layout/grid_layout3_row_col_adjust.css" --8<-- "docs/examples/guide/layout/grid_layout3_row_col_adjust.css"
``` ```
Since our `grid-size` is three (meaning it has three columns), our `grid-columns` declaration has three space-separated values.
Since our `grid-size` is 3 (meaning it has three columns), our `grid-columns` declaration has three space-separated values.
Each of these values sets the width of a column. Each of these values sets the width of a column.
The first value refers to the left-most column, the second value refers to the next column, and so on. The first value refers to the left-most column, the second value refers to the next column, and so on.
In the example above, we've given the left-most column a width of `2fr` and the other columns widths of `1fr`. In the example above, we've given the left-most column a width of `2fr` and the other columns widths of `1fr`.
@@ -288,6 +260,7 @@ Similarly, we can adjust the height of a row using `grid-rows`.
In the following example, we use `%` units to adjust the first row of our grid to `25%` height, In the following example, we use `%` units to adjust the first row of our grid to `25%` height,
and the second row to `75%` height (while retaining the `grid-columns` change from above). and the second row to `75%` height (while retaining the `grid-columns` change from above).
=== "Output" === "Output"
```{.textual path="docs/examples/guide/layout/grid_layout4_row_col_adjust.py"} ```{.textual path="docs/examples/guide/layout/grid_layout4_row_col_adjust.py"}
@@ -305,26 +278,21 @@ and the second row to `75%` height (while retaining the `grid-columns` change fr
--8<-- "docs/examples/guide/layout/grid_layout4_row_col_adjust.css" --8<-- "docs/examples/guide/layout/grid_layout4_row_col_adjust.css"
``` ```
If you don't specify enough values in a `grid-columns` or `grid-rows` declaration, the values you _have_ provided will be "repeated". If you don't specify enough values in a `grid-columns` or `grid-rows` declaration, the values you _have_ provided will be "repeated".
For example, if your grid has four columns (i.e. `grid-size: 4;`), then `grid-columns: 2 4;` is equivalent to `grid-columns: 2 4 2 4;`. For example, if your grid has four columns (i.e. `grid-size: 4;`), then `grid-columns: 2 4;` is equivalent to `grid-columns: 2 4 2 4;`.
If it instead had three columns, then `grid-columns: 2 4;` would be equivalent to `grid-columns: 2 4 2;`. If it instead had three columns, then `grid-columns: 2 4;` would be equivalent to `grid-columns: 2 4 2;`.
### Cell spans ### Cell spans
You can adjust the number of rows and columns an individual cell spans across. Cells may _span_ multiple rows or columns, to create more interesting grid arrangements.
Let's return to our original, uniform, 2x3 grid to more clearly illustrate the effect of modifying the row spans and column spans of cells:
```{.textual path="docs/examples/guide/layout/grid_layout1.py"}
```
To make a single cell span multiple rows or columns in the grid, we need to be able to select it using CSS. To make a single cell span multiple rows or columns in the grid, we need to be able to select it using CSS.
To do this, we'll add an ID to the widget inside our `compose` method. To do this, we'll add an ID to the widget inside our `compose` method so we can set the `row-span` and `column-span` properties using CSS.
Then, we can set the `row-span` and `column-span` properties on this ID using CSS.
Let's add an ID of `#two` to the second widget yielded from `compose`, and give it a `column-span` of 2 in our CSS to make that widget span across two columns. Let's add an ID of `#two` to the second widget yielded from `compose`, and give it a `column-span` of 2 to make that widget span two columns.
We'll also add a slight tint using `tint: magenta 40%;` to draw attention to it. We'll also add a slight tint using `tint: magenta 40%;` to draw attention to it.
The relevant changes are highlighted in the Python and CSS files below.
=== "Output" === "Output"
@@ -333,7 +301,7 @@ The relevant changes are highlighted in the Python and CSS files below.
=== "grid_layout5_col_span.py" === "grid_layout5_col_span.py"
```python hl_lines="8" ```python
--8<-- "docs/examples/guide/layout/grid_layout5_col_span.py" --8<-- "docs/examples/guide/layout/grid_layout5_col_span.py"
``` ```
@@ -343,6 +311,8 @@ The relevant changes are highlighted in the Python and CSS files below.
--8<-- "docs/examples/guide/layout/grid_layout5_col_span.css" --8<-- "docs/examples/guide/layout/grid_layout5_col_span.css"
``` ```
Notice that the widget expands to fill columns to the _right_ of its original position. Notice that the widget expands to fill columns to the _right_ of its original position.
Since `#two` now spans two cells instead of one, all widgets that follow it are shifted along one cell in the grid to accommodate. Since `#two` now spans two cells instead of one, all widgets that follow it are shifted along one cell in the grid to accommodate.
As a result, the final widget wraps on to a new row at the bottom of the grid. As a result, the final widget wraps on to a new row at the bottom of the grid.
@@ -356,6 +326,7 @@ This can be used in conjunction with `column-span`, meaning one cell may span mu
The example below shows `row-span` in action. The example below shows `row-span` in action.
We again target widget `#two` in our CSS, and add a `row-span: 2;` declaration to it. We again target widget `#two` in our CSS, and add a `row-span: 2;` declaration to it.
=== "Output" === "Output"
```{.textual path="docs/examples/guide/layout/grid_layout6_row_span.py"} ```{.textual path="docs/examples/guide/layout/grid_layout6_row_span.py"}
@@ -373,6 +344,8 @@ We again target widget `#two` in our CSS, and add a `row-span: 2;` declaration t
--8<-- "docs/examples/guide/layout/grid_layout6_row_span.css" --8<-- "docs/examples/guide/layout/grid_layout6_row_span.css"
``` ```
Widget `#two` now spans two columns and two rows, covering a total of four cells. Widget `#two` now spans two columns and two rows, covering a total of four cells.
Notice how the other cells are moved to accommodate this change. Notice how the other cells are moved to accommodate this change.
The widget that previously occupied a single cell now occupies four cells, thus displacing three cells to a new row. The widget that previously occupied a single cell now occupies four cells, thus displacing three cells to a new row.
@@ -383,7 +356,7 @@ The spacing between cells in the grid can be adjusted using the `grid-gutter` CS
By default, cells have no gutter, meaning their edges touch each other. By default, cells have no gutter, meaning their edges touch each other.
Gutter is applied across every cell in the grid, so `grid-gutter` must be used on a widget with `layout: grid` (_not_ on a child/cell widget). Gutter is applied across every cell in the grid, so `grid-gutter` must be used on a widget with `layout: grid` (_not_ on a child/cell widget).
To better illustrate gutter, let's set our `Screen` background color to `lightgreen`, and the background color of the widgets we yield to `darkmagenta`. To illustrate gutter let's set our `Screen` background color to `lightgreen`, and the background color of the widgets we yield to `darkmagenta`.
Now if we add `grid-gutter: 1;` to our grid, one cell of spacing appears between the cells and reveals the light green background of the `Screen`. Now if we add `grid-gutter: 1;` to our grid, one cell of spacing appears between the cells and reveals the light green background of the `Screen`.
=== "Output" === "Output"
@@ -446,7 +419,7 @@ The code below shows a simple sidebar implementation.
If we run the app above and scroll down, the body text will scroll but the sidebar does not (note the position of the scrollbar in the output shown above). If we run the app above and scroll down, the body text will scroll but the sidebar does not (note the position of the scrollbar in the output shown above).
Docking multiple widgets to the same edge will result in overlap. Docking multiple widgets to the same edge will result in overlap.
Just like in the `center` layout, the first widget yielded from `compose` will appear on below widgets yielded after it. The first widget yielded from `compose` will appear below widgets yielded after it.
Let's dock a second sidebar, `#another-sidebar`, to the left of the screen. Let's dock a second sidebar, `#another-sidebar`, to the left of the screen.
This new sidebar is double the width of the one previous one, and has a `deeppink` background. This new sidebar is double the width of the one previous one, and has a `deeppink` background.
@@ -457,7 +430,7 @@ This new sidebar is double the width of the one previous one, and has a `deeppin
=== "dock_layout2_sidebar.py" === "dock_layout2_sidebar.py"
```python hl_lines="14" ```python hl_lines="16"
--8<-- "docs/examples/guide/layout/dock_layout2_sidebar.py" --8<-- "docs/examples/guide/layout/dock_layout2_sidebar.py"
``` ```
@@ -495,17 +468,15 @@ If we wished for the sidebar to appear below the header, it'd simply be a case o
## Layers ## Layers
The order which widgets are yielded isn't the only thing that affects the order in which they're painted. Textual has a concept of _layers_ which gives you finely grained control over the order widgets are place.
Textual also has the concept of _layers_, which you may be familiar with if you've ever used image editing software.
When drawing widgets, Textual will first draw on _lower_ layers, working its way up to higher layers. When drawing widgets, Textual will first draw on _lower_ layers, working its way up to higher layers.
As such, widgets on higher layers will be drawn on top of those on lower layers. As such, widgets on higher layers will be drawn on top of those on lower layers.
Layers take precedence over yield order.
Layer names need to be defined in advance, using a `layers` CSS declaration on a widget. Layer names are defined with a `layers` style on a container (parent) widget.
Descendants of this widget can then be assigned to one of these layers using a `layer` declaration. Descendants of this widget can then be assigned to one of these layers using a `layer` style.
The `layers` declaration takes a space-separated list of layer names. The `layers` style takes a space-separated list of layer names.
The leftmost name is the lowest layer, and the rightmost is the highest layer. The leftmost name is the lowest layer, and the rightmost is the highest layer.
Therefore, if you assign a descendant to the rightmost layer name, it'll be drawn on the top layer and will be visible above all other descendants. Therefore, if you assign a descendant to the rightmost layer name, it'll be drawn on the top layer and will be visible above all other descendants.
@@ -514,9 +485,8 @@ To add a widget to the topmost layer in this case, you'd add a declaration of `l
In the example below, `#box1` is yielded before `#box2`. In the example below, `#box1` is yielded before `#box2`.
Given our earlier discussion on yield order, you'd expect `#box2` to appear on top. Given our earlier discussion on yield order, you'd expect `#box2` to appear on top.
However, in this case, both `#box1` and `#box2` are assigned to layers. However, in this case, both `#box1` and `#box2` are assigned to layers which define the reverse order, so `#box1` is on top of `#box2`
From the `layers: below above;` declaration inside `layers.css`, we can see that the layer named `above` is on top of the `below` layer.
Since `#box1` is on the higher layer, it is drawn on top of `#box2`.
[//]: # (NOTE: the example below also appears in the layers and layer style reference) [//]: # (NOTE: the example below also appears in the layers and layer style reference)
@@ -552,46 +522,12 @@ The offset of a widget can be set using the `offset` CSS property.
* The first value defines the `x` (horizontal) offset. Positive values will shift the widget to the right. Negative values will shift the widget to the left. * The first value defines the `x` (horizontal) offset. Positive values will shift the widget to the right. Negative values will shift the widget to the left.
* The second value defines the `y` (vertical) offset. Positive values will shift the widget down. Negative values will shift the widget up. * The second value defines the `y` (vertical) offset. Positive values will shift the widget down. Negative values will shift the widget up.
For example, `offset: 4 -2;` will shift the target widget 4 terminal cells to the right, and 2 terminal cells up.
The example below illustrates `offset` further.
The `#parent` container has `layout: center`, meaning all four of the widgets we yield from `compose` have an origin in the center of it.
* We make no adjustments to the offset of `#box1` in the CSS - it remains in its original position, and thus has offset `(0, 0)`.
* We apply and offset of `12 4` to `#box2`, moving it to the right and down a little.
* In the case of `#box3` we apply and offset of `-12 -4`, which shifts it to the left and up.
* `#box4` at the bottom left of the screen illustrates clipping. A child widget will be clipped by its parent's region, meaning any part of the child which extends beyond the parent region will not be visible.
=== "Output"
```{.textual path="docs/examples/guide/layout/offset.py"}
```
=== "offset.py"
```python
--8<-- "docs/examples/guide/layout/offset.py"
```
=== "offset.css"
```sass hl_lines="25 30 35"
--8<-- "docs/examples/guide/layout/offset.css"
```
[//]: # (TODO Link the word animation below to animation docs) [//]: # (TODO Link the word animation below to animation docs)
Offset is commonly used with animation.
You may have a sidebar, for example, with its initial offset set such that it is hidden off to the left of the screen.
On pressing a button, the offset can be eased to `(0, 0)`, animating the sidebar in from the left, back to its origin position as defined by the layout.
## Putting it all together ## Putting it all together
The sections above show how the various layouts in Textual can be used to position widgets on screen. The sections above show how the various layouts in Textual can be used to position widgets on screen.
In a real application, you'll make use of several layouts. In a real application, you'll make use of several layouts.
You might choose to build the high-level structure of your app using `layout: grid;`, with individual widgets laying out their children using `horizontal` or `vertical` layouts.
If one of your widgets is particularly complex, perhaps it'll use `layout: grid;` itself.
The example below shows how an advanced layout can be built by combining the various techniques described on this page. The example below shows how an advanced layout can be built by combining the various techniques described on this page.
@@ -608,29 +544,8 @@ The example below shows how an advanced layout can be built by combining the var
=== "combining_layouts.css" === "combining_layouts.css"
```sass hl_lines="4" ```sass
--8<-- "docs/examples/guide/layout/combining_layouts.css" --8<-- "docs/examples/guide/layout/combining_layouts.css"
``` ```
At the top of the application we have a header. Textual layouts make it easy design build real-life applications with relatively little code.
This header is yielded from our `compose` method using `yield Header()`.
As mentioned earlier, `Header` is a builtin Textual widget which internally contains a `dock: top;`.
Since it's yielded directly from `compose`, it gets docked to the top of `Screen` (the terminal window).
The body of the application is contained within the widget `#app-grid` which uses a grid layout.
The cells of the grid have been given blue, pink, and green borders.
This grid consists of two columns (`grid-size: 2`).
The left pane (with the blue border) is the first cell within our grid.
It has ID `#left-pane`, and is set to span two rows using `row-span: 2;`.
The left pane `#left-pane` itself is a `layout.Vertical` container widget.
This widget internally contains some CSS which sets `layout: vertical`, resulting in vertically arranged children.
The next cell in the grid layout is `#top-right`, which has the pink-red border.
This grid cell makes use of a horizontal layout.
The final cell in our grid is located at the bottom right of the screen.
It has a green border, and this cell itself uses a grid layout.
As you can see, combining layouts lets you design complex apps with very little code!

View File

@@ -43,7 +43,9 @@ Widgets will occupy the full width of their container and as many lines as requi
Note how the combined height of the widget is three rows in the terminal. This is because a border adds two rows (and two columns). If you were to remove the line that sets the border style, the widget would occupy a single row. Note how the combined height of the widget is three rows in the terminal. This is because a border adds two rows (and two columns). If you were to remove the line that sets the border style, the widget would occupy a single row.
Widgets will wrap text by default. If you were to replace `"Textual"` with a long paragraph of text, the widget will expand downwards to fit. !!! information
Widgets will wrap text by default. If you were to replace `"Textual"` with a long paragraph of text, the widget will expand downwards to fit.
## Colors ## Colors
@@ -184,7 +186,7 @@ With the width set to `"50%"` and the height set to `"80%"`, the widget will kee
```{.textual path="docs/examples/guide/styles/dimensions03.py" columns="120" lines="40"} ```{.textual path="docs/examples/guide/styles/dimensions03.py" columns="120" lines="40"}
``` ```
Percentage units are useful for widgets that occupy a relative portion of the screen, but they can be problematic for some proportions. For instance, if we want to divide the screen into thirds, we would have to set a dimension to `33.3333333333%` which is awkward. Textual supports `fr` units which are often better than percentage-based units for these situations. Percentage units can be problematic for some relative values. For instance, if we want to divide the screen into thirds, we would have to set a dimension to `33.3333333333%` which is awkward. Textual supports `fr` units which are often better than percentage-based units for these situations.
When specifying `fr` units for a given dimension, Textual will divide the available space by the sum of the `fr` units on that dimension. That space will then be divided amongst the widgets as a proportion of their individual `fr` values. When specifying `fr` units for a given dimension, Textual will divide the available space by the sum of the `fr` units on that dimension. That space will then be divided amongst the widgets as a proportion of their individual `fr` values.
@@ -292,7 +294,9 @@ If you set `box_sizing` to `"content-box"` then space required for padding and b
</div> </div>
The following example creates two widgets which have a width of 30, a height of 6, and a border and padding of 1. The second widget has `box_sizing` set to `"content-box"`. The following example creates two widgets with a width of 30, a height of 6, and a border and padding of 1.
The first widget has the default `box_sizing` (`"border-box"`).
The second widget sets `box_sizing` to `"content-box"`.
```python title="box_sizing01.py" hl_lines="33" ```python title="box_sizing01.py" hl_lines="33"
--8<-- "docs/examples/guide/styles/box_sizing01.py" --8<-- "docs/examples/guide/styles/box_sizing01.py"
@@ -307,9 +311,9 @@ The padding and border of the first widget is subtracted from the height leaving
Margin is similar to padding in that it adds space, but unlike padding, [margin](../styles/margin.md) is outside of the widget's border. It is used to add space between widgets. Margin is similar to padding in that it adds space, but unlike padding, [margin](../styles/margin.md) is outside of the widget's border. It is used to add space between widgets.
The following example creates two widgets, each with a padding of 2. The following example creates two widgets, each with a margin of 2.
```python title="margin01.py" hl_lines="33" ```python title="margin01.py" hl_lines="26-27"
--8<-- "docs/examples/guide/styles/margin01.py" --8<-- "docs/examples/guide/styles/margin01.py"
``` ```

View File

@@ -0,0 +1 @@
::: textual.containers

68
docs/styles/align.md Normal file
View File

@@ -0,0 +1,68 @@
# Align
The `align` style aligns children within a container.
## Syntax
```
align: <HORIZONTAL> <VERTICAL>;
align-horizontal: <HORIZONTAL>;
align-vertical: <VERTICAL>;
```
### Values
#### `HORIZONTAL`
| Value | Description |
| ---------------- | -------------------------------------------------- |
| `left` (default) | Align content on the left of the horizontal axis |
| `center` | Align content in the center of the horizontal axis |
| `right` | Align content on the right of the horizontal axis |
#### `VERTICAL`
| Value | Description |
| --------------- | ------------------------------------------------ |
| `top` (default) | Align content at the top of the vertical axis |
| `middle` | Align content in the middle of the vertical axis |
| `bottom` | Align content at the bottom of the vertical axis |
## Example
=== "align.py"
```python
--8<-- "docs/examples/styles/align.py"
```
=== "align.css"
```scss hl_lines="2"
--8<-- "docs/examples/styles/align.css"
```
=== "Output"
```{.textual path="docs/examples/styles/align.py"}
```
## CSS
```sass
/* Align child widgets to the center. */
align: center middle;
/* Align child widget to th top right */
align: right top;
```
## Python
```python
# Align child widgets to the center
widget.styles.align = ("center", "middle")
# Align child widgets to the top right
widget.styles.align = ("right", "top")
```

View File

@@ -13,8 +13,7 @@ layout: [center|grid|horizontal|vertical];
### Values ### Values
| Value | Description | | Value | Description |
|----------------------|-------------------------------------------------------------------------------| | -------------------- | ----------------------------------------------------------------------------- |
| `center` | A single child widget will be placed in the center. |
| `grid` | Child widgets will be arranged in a grid. | | `grid` | Child widgets will be arranged in a grid. |
| `horizontal` | Child widgets will be arranged along the horizontal axis, from left to right. | | `horizontal` | Child widgets will be arranged along the horizontal axis, from left to right. |
| `vertical` (default) | Child widgets will be arranged along the vertical axis, from top to bottom. | | `vertical` (default) | Child widgets will be arranged along the vertical axis, from top to bottom. |

View File

@@ -7,7 +7,7 @@ from textual.app import App, ComposeResult
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer
from textual.layout import Container from textual.containers import Container
CODE = ''' CODE = '''
from __future__ import annotations from __future__ import annotations

View File

@@ -2,7 +2,7 @@ from decimal import Decimal
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual import events from textual import events
from textual.layout import Container from textual.containers import Container
from textual.reactive import var from textual.reactive import var
from textual.widgets import Button, Static from textual.widgets import Button, Static

View File

@@ -11,9 +11,8 @@ import sys
from rich.syntax import Syntax from rich.syntax import Syntax
from rich.traceback import Traceback from rich.traceback import Traceback
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container, Vertical from textual.containers import Container, Vertical
from textual.reactive import var from textual.reactive import var
from textual.widgets import DirectoryTree, Footer, Header, Static from textual.widgets import DirectoryTree, Footer, Header, Static

View File

@@ -11,7 +11,7 @@ except ImportError:
from rich.markdown import Markdown from rich.markdown import Markdown
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Vertical from textual.containers import Vertical
from textual.widgets import Static, TextInput from textual.widgets import Static, TextInput

View File

@@ -52,6 +52,7 @@ nav:
- "events/show.md" - "events/show.md"
- Styles: - Styles:
- "styles/index.md" - "styles/index.md"
- "styles/align.md"
- "styles/background.md" - "styles/background.md"
- "styles/border.md" - "styles/border.md"
- "styles/box_sizing.md" - "styles/box_sizing.md"
@@ -96,6 +97,7 @@ nav:
- "reference/app.md" - "reference/app.md"
- "reference/button.md" - "reference/button.md"
- "reference/color.md" - "reference/color.md"
- "reference/containers.md"
- "reference/dom_node.md" - "reference/dom_node.md"
- "reference/events.md" - "reference/events.md"
- "reference/geometry.md" - "reference/geometry.md"

View File

@@ -7,7 +7,7 @@ from textual.app import App, ComposeResult
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer
from textual.layout import Container from textual.containers import Container
CODE = ''' CODE = '''
from __future__ import annotations from __future__ import annotations

View File

@@ -1,11 +1,12 @@
from textual import layout, events from textual import events
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import Button from textual.widgets import Button
class ButtonsApp(App[str]): class ButtonsApp(App[str]):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield layout.Vertical( yield Vertical(
Button("default", id="foo"), Button("default", id="foo"),
Button.success("success", id="bar"), Button.success("success", id="bar"),
Button.warning("warning", id="baz"), Button.warning("warning", id="baz"),

View File

@@ -1,5 +1,5 @@
Screen { Screen {
layout: center; align: center middle;
background: darkslategrey; background: darkslategrey;
} }

View File

@@ -1,6 +1,6 @@
import random import random
from textual import layout from textual.containers import Horizontal, Vertical
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.widgets import Button, Static from textual.widgets import Button, Static
@@ -36,14 +36,14 @@ class AddRemoveApp(App):
self.count = 0 self.count = 0
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield layout.Vertical( yield Vertical(
layout.Horizontal( Horizontal(
Button("Add", variant="success", id="add"), Button("Add", variant="success", id="add"),
Button("Remove", variant="error", id="remove"), Button("Remove", variant="error", id="remove"),
Button("Remove random", variant="warning", id="remove_random"), Button("Remove random", variant="warning", id="remove_random"),
id="buttons", id="buttons",
), ),
layout.Vertical(id="items"), Vertical(id="items"),
) )
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:

14
sandbox/will/align.css Normal file
View File

@@ -0,0 +1,14 @@
Screen {
align: center middle;
}
Label {
width: 20;
height: 5;
background: blue;
color: white;
border: tall white;
margin: 1;
content-align: center middle;
}

19
sandbox/will/align.py Normal file
View File

@@ -0,0 +1,19 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
class Label(Static):
pass
class AlignApp(App):
CSS_PATH = "align.css"
def compose(self) -> ComposeResult:
yield Label("Hello")
yield Label("World!")
if __name__ == "__main__":
app = AlignApp()
app.run()

View File

@@ -112,7 +112,7 @@ Tweet {
border: wide $panel; border: wide $panel;
/* scrollbar-gutter: stable; */ /* scrollbar-gutter: stable; */
align-horizontal: center;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -124,7 +124,7 @@ Tweet {
padding: 0 2; padding: 0 2;
margin: 1 2; margin: 1 2;
height: 24; height: 24;
align-horizontal: center;
layout: vertical; layout: vertical;
} }
@@ -228,7 +228,7 @@ Error {
padding: 0; padding: 0;
text-style: bold; text-style: bold;
align-horizontal: center; content-align: center middle;
} }
Warning { Warning {
@@ -240,7 +240,7 @@ Warning {
border-bottom: tall $warning-darken-2; border-bottom: tall $warning-darken-2;
text-style: bold; text-style: bold;
align-horizontal: center; content-align: center middle;
} }
Success { Success {
@@ -256,7 +256,7 @@ Success {
text-style: bold ; text-style: bold ;
align-horizontal: center; content-align: center middle;
} }

View File

@@ -7,7 +7,7 @@ from textual.app import App, ComposeResult
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer
from textual.layout import Container, Vertical from textual.containers import Container, Vertical
CODE = ''' CODE = '''
from __future__ import annotations from __future__ import annotations

View File

@@ -2,7 +2,7 @@ from decimal import Decimal
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual import events from textual import events
from textual.layout import Container from textual.containers import Container
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.widgets import Button, Static from textual.widgets import Button, Static

View File

@@ -1,5 +1,5 @@
from textual.app import App from textual.app import App
from textual.layout import Vertical, Center from textual.containers import Vertical, Center
from textual.widgets import Static from textual.widgets import Static

View File

@@ -1,5 +1,5 @@
from textual.app import App from textual.app import App
from textual.layout import Container from textual.containers import Container
from textual.widgets import Header, Footer, Static from textual.widgets import Header, Footer, Static

View File

@@ -5,7 +5,7 @@ from rich.panel import Panel
from textual import events from textual import events
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.layout import Container, Horizontal, Vertical from textual.containers import Container, Horizontal, Vertical
from textual.widget import Widget from textual.widget import Widget

View File

@@ -1,5 +1,5 @@
Screen { Screen {
layout: center; align: center middle;
} }
#parent { #parent {

View File

@@ -1,14 +1,11 @@
from textual import layout
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.containers import Vertical
from textual.widgets import Static from textual.widgets import Static
class OffsetExample(App): class OffsetExample(App):
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
yield layout.Vertical( yield Vertical(Static("Child", id="child"), id="parent")
Static("Child", id="child"),
id="parent"
)
yield Static("Tag", id="tag") yield Static("Tag", id="tag")

View File

@@ -1,6 +1,6 @@
from textual.app import App from textual.app import App
from textual.layout import Container from textual.containers import Container
from textual.widgets import DirectoryTree from textual.widgets import DirectoryTree

View File

@@ -49,6 +49,7 @@ def arrange(
scroll_spacing = Spacing() scroll_spacing = Spacing()
null_spacing = Spacing() null_spacing = Spacing()
get_dock = attrgetter("styles.dock") get_dock = attrgetter("styles.dock")
styles = widget.styles
for widgets in dock_layers.values(): for widgets in dock_layers.values():
@@ -89,7 +90,7 @@ def arrange(
# Should not occur, mainly to keep Mypy happy # Should not occur, mainly to keep Mypy happy
raise AssertionError("invalid value for edge") # pragma: no-cover raise AssertionError("invalid value for edge") # pragma: no-cover
align_offset = dock_widget.styles.align_size( align_offset = dock_widget.styles._align_size(
(widget_width, widget_height), size (widget_width, widget_height), size
) )
dock_region = dock_region.shrink(margin).translate(align_offset) dock_region = dock_region.shrink(margin).translate(align_offset)
@@ -105,7 +106,17 @@ def arrange(
if arranged_layout_widgets: if arranged_layout_widgets:
scroll_spacing = scroll_spacing.grow_maximum(dock_spacing) scroll_spacing = scroll_spacing.grow_maximum(dock_spacing)
arrange_widgets.update(arranged_layout_widgets) arrange_widgets.update(arranged_layout_widgets)
placement_offset = region.offset placement_offset = region.offset
if styles.align_horizontal != "left" or styles.align_vertical != "top":
placement_size = Region.from_union(
[
placement.region.grow(placement.margin)
for placement in layout_placements
]
).size
placement_offset += styles._align_size(placement_size, size)
if placement_offset: if placement_offset:
layout_placements = [ layout_placements = [
_WidgetPlacement( _WidgetPlacement(

View File

@@ -1,7 +1,7 @@
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.constants import BORDERS from textual.constants import BORDERS
from textual.widgets import Button, Static from textual.widgets import Button, Static
from textual import layout from textual.containers import Vertical
TEXT = """I must not fear. TEXT = """I must not fear.
@@ -13,7 +13,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.""" Where the fear has gone there will be nothing. Only I will remain."""
class BorderButtons(layout.Vertical): class BorderButtons(Vertical):
DEFAULT_CSS = """ DEFAULT_CSS = """
BorderButtons { BorderButtons {
dock: left; dock: left;

View File

@@ -1,16 +1,14 @@
from __future__ import annotations from __future__ import annotations
from rich.console import RenderableType from rich.console import RenderableType
from textual import layout
from textual._easing import EASING from textual._easing import EASING
from textual.app import ComposeResult, App from textual.app import App, ComposeResult
from textual.cli.previews.borders import TEXT from textual.cli.previews.borders import TEXT
from textual.containers import Container, Horizontal, Vertical
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.scrollbar import ScrollBarRender from textual.scrollbar import ScrollBarRender
from textual.widget import Widget from textual.widget import Widget
from textual.widgets import Button, Static, Footer from textual.widgets import Button, Footer, Static, TextInput
from textual.widgets import TextInput
from textual.widgets._text_input import TextWidgetBase from textual.widgets._text_input import TextWidgetBase
VIRTUAL_SIZE = 100 VIRTUAL_SIZE = 100
@@ -78,13 +76,13 @@ class EasingApp(App):
) )
yield EasingButtons() yield EasingButtons()
yield layout.Vertical( yield Vertical(
layout.Horizontal( Horizontal(
Static("Animation Duration:", id="label"), duration_input, id="inputs" Static("Animation Duration:", id="label"), duration_input, id="inputs"
), ),
layout.Horizontal( Horizontal(
self.animated_bar, self.animated_bar,
layout.Container(self.opacity_widget, id="other"), Container(self.opacity_widget, id="other"),
), ),
Footer(), Footer(),
) )

View File

@@ -13,7 +13,7 @@ class Container(Widget):
class Vertical(Widget): class Vertical(Widget):
"""A container widget to align children vertically.""" """A container widget which aligns children vertically."""
DEFAULT_CSS = """ DEFAULT_CSS = """
Vertical { Vertical {
@@ -24,7 +24,7 @@ class Vertical(Widget):
class Horizontal(Widget): class Horizontal(Widget):
"""A container widget to align children horizontally.""" """A container widget which aligns children horizontally."""
DEFAULT_CSS = """ DEFAULT_CSS = """
Horizontal { Horizontal {
@@ -34,11 +34,11 @@ class Horizontal(Widget):
""" """
class Center(Widget): class Grid(Widget):
"""A container widget to align children in the center.""" """A container widget with grid alignment."""
DEFAULT_CSS = """ DEFAULT_CSS = """
Center { Grid {
layout: center; layout: grid;
} }
""" """

View File

@@ -449,13 +449,21 @@ class StylesBase(ABC):
styles.node = node styles.node = node
return styles return styles
def get_transition(self, key: str) -> Transition | None: def _get_transition(self, key: str) -> Transition | None:
"""Get a transition.
Args:
key (str): Transition key.
Returns:
Transition | None: Transition object or None it no transition exists.
"""
if key in self.ANIMATABLE: if key in self.ANIMATABLE:
return self.transitions.get(key, None) return self.transitions.get(key, None)
else: else:
return None return None
def align_width(self, width: int, parent_width: int) -> int: def _align_width(self, width: int, parent_width: int) -> int:
"""Align the width dimension. """Align the width dimension.
Args: Args:
@@ -474,7 +482,7 @@ class StylesBase(ABC):
offset_x = parent_width - width offset_x = parent_width - width
return offset_x return offset_x
def align_height(self, height: int, parent_height: int) -> int: def _align_height(self, height: int, parent_height: int) -> int:
"""Align the height dimensions """Align the height dimensions
Args: Args:
@@ -493,7 +501,7 @@ class StylesBase(ABC):
offset_y = parent_height - height offset_y = parent_height - height
return offset_y return offset_y
def align_size(self, child: tuple[int, int], parent: tuple[int, int]) -> Offset: def _align_size(self, child: tuple[int, int], parent: tuple[int, int]) -> Offset:
"""Align a size according to alignment rules. """Align a size according to alignment rules.
Args: Args:
@@ -506,8 +514,8 @@ class StylesBase(ABC):
width, height = child width, height = child
parent_width, parent_height = parent parent_width, parent_height = parent
return Offset( return Offset(
self.align_width(width, parent_width), self._align_width(width, parent_width),
self.align_height(height, parent_height), self._align_height(height, parent_height),
) )

View File

@@ -444,7 +444,7 @@ class Stylesheet:
# Check if this can / should be animated # Check if this can / should be animated
if is_animatable(key) and new_render_value != old_render_value: if is_animatable(key) and new_render_value != old_render_value:
transition = new_styles.get_transition(key) transition = new_styles._get_transition(key)
if transition is not None: if transition is not None:
duration, easing, delay = transition duration, easing, delay = transition
node.app.animator.animate( node.app.animator.animate(

View File

@@ -1,36 +0,0 @@
from __future__ import annotations
from fractions import Fraction
from .._layout import ArrangeResult, Layout, WidgetPlacement
from ..geometry import Region, Size
from ..widget import Widget
class CenterLayout(Layout):
"""Positions widgets in the center of the screen."""
name = "center"
def arrange(
self, parent: Widget, children: list[Widget], size: Size
) -> ArrangeResult:
placements: list[WidgetPlacement] = []
parent_size = parent.outer_size
container_width, container_height = size
fraction_unit = Fraction(size.width)
for widget in children:
width, height, margin = widget._get_box_model(
size, parent_size, fraction_unit
)
margin_width = width + margin.width
margin_height = height + margin.height
x = margin.left + max(0, (container_width - margin_width) // 2)
y = margin.top + max(0, (container_height - margin_height) // 2)
region = Region(x, y, int(width), int(height))
placements.append(WidgetPlacement(region, margin, widget, 0))
return placements, set(children)

View File

@@ -1,13 +1,11 @@
from __future__ import annotations from __future__ import annotations
from .._layout import Layout from .._layout import Layout
from .center import CenterLayout
from .horizontal import HorizontalLayout from .horizontal import HorizontalLayout
from .grid import GridLayout from .grid import GridLayout
from .vertical import VerticalLayout from .vertical import VerticalLayout
LAYOUT_MAP: dict[str, type[Layout]] = { LAYOUT_MAP: dict[str, type[Layout]] = {
"center": CenterLayout,
"horizontal": HorizontalLayout, "horizontal": HorizontalLayout,
"grid": GridLayout, "grid": GridLayout,
"vertical": VerticalLayout, "vertical": VerticalLayout,

View File

@@ -23,7 +23,7 @@ class HorizontalLayout(Layout):
placements: list[WidgetPlacement] = [] placements: list[WidgetPlacement] = []
add_placement = placements.append add_placement = placements.append
x = max_width = max_height = Fraction(0) x = max_height = Fraction(0)
parent_size = parent.outer_size parent_size = parent.outer_size
styles = [child.styles for child in children if child.styles.width is not None] styles = [child.styles for child in children if child.styles.width is not None]
@@ -48,21 +48,19 @@ class HorizontalLayout(Layout):
displayed_children = [child for child in children if child.display] displayed_children = [child for child in children if child.display]
_Region = Region
_WidgetPlacement = WidgetPlacement
for widget, box_model, margin in zip(children, box_models, margins): for widget, box_model, margin in zip(children, box_models, margins):
content_width, content_height, box_margin = box_model content_width, content_height, box_margin = box_model
offset_y = ( offset_y = box_margin.top
widget.styles.align_height(
int(content_height), size.height - box_margin.height
)
+ box_model.margin.top
)
next_x = x + content_width next_x = x + content_width
region = Region(int(x), offset_y, int(next_x - int(x)), int(content_height)) region = _Region(
int(x), offset_y, int(next_x - int(x)), int(content_height)
)
max_height = max( max_height = max(
max_height, content_height + offset_y + box_model.margin.bottom max_height, content_height + offset_y + box_model.margin.bottom
) )
add_placement(WidgetPlacement(region, box_model.margin, widget, 0)) add_placement(_WidgetPlacement(region, box_model.margin, widget, 0))
x = next_x + margin x = next_x + margin
max_width = x
return placements, set(displayed_children) return placements, set(displayed_children)

View File

@@ -44,17 +44,15 @@ class VerticalLayout(Layout):
y = Fraction(box_models[0].margin.top if box_models else 0) y = Fraction(box_models[0].margin.top if box_models else 0)
_Region = Region
_WidgetPlacement = WidgetPlacement
for widget, box_model, margin in zip(children, box_models, margins): for widget, box_model, margin in zip(children, box_models, margins):
content_width, content_height, box_margin = box_model content_width, content_height, box_margin = box_model
offset_x = (
widget.styles.align_width(
int(content_width), size.width - box_margin.width
)
+ box_model.margin.left
)
next_y = y + content_height next_y = y + content_height
region = Region(offset_x, int(y), int(content_width), int(next_y) - int(y)) region = _Region(
add_placement(WidgetPlacement(region, box_model.margin, widget, 0)) box_margin.left, int(y), int(content_width), int(next_y) - int(y)
)
add_placement(_WidgetPlacement(region, box_model.margin, widget, 0))
y = next_y + margin y = next_y + margin
return placements, set(children) return placements, set(children)

View File

@@ -1,7 +1,7 @@
from ..app import ComposeResult from ..app import ComposeResult
from ._static import Static from ._static import Static
from ._button import Button from ._button import Button
from ..layout import Container from ..containers import Container
from rich.markdown import Markdown from rich.markdown import Markdown
@@ -26,11 +26,9 @@ Where the fear has gone there will be nothing. Only I will remain."
class Welcome(Static): class Welcome(Static):
DEFAULT_CSS = """ DEFAULT_CSS = """
Welcome { Welcome {
width: 100%; width: 100%;
height: 100%; height: 100%;
padding: 1 2;
background: $surface; background: $surface;
} }
@@ -47,9 +45,7 @@ class Welcome(Static):
Welcome #close { Welcome #close {
dock: bottom; dock: bottom;
width: 100%; width: 100%;
margin-top: 1;
} }
""" """
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:

File diff suppressed because one or more lines are too long

View File

@@ -14,10 +14,6 @@ def test_layers(snap_compare):
assert snap_compare("docs/examples/guide/layout/layers.py") assert snap_compare("docs/examples/guide/layout/layers.py")
def test_center_layout(snap_compare):
assert snap_compare("docs/examples/guide/layout/center_layout.py")
def test_horizontal_layout(snap_compare): def test_horizontal_layout(snap_compare):
assert snap_compare("docs/examples/guide/layout/horizontal_layout.py") assert snap_compare("docs/examples/guide/layout/horizontal_layout.py")

View File

@@ -1,28 +0,0 @@
from textual._layout import WidgetPlacement
from textual.geometry import Region, Size, Spacing
from textual.layouts.center import CenterLayout
from textual.widget import Widget
def test_center_layout():
widget = Widget()
widget._size = Size(80, 24)
child = Widget()
child.styles.width = 10
child.styles.height = 5
layout = CenterLayout()
placements, widgets = layout.arrange(widget, [child], Size(60, 20))
assert widgets == {child}
expected = [
WidgetPlacement(
region=Region(x=25, y=7, width=10, height=5),
margin=Spacing(),
widget=child,
order=0,
fixed=False,
),
]
assert placements == expected