diff --git a/docs/events/mount.md b/docs/events/mount.md
index ff393acb65..950d93a2ed 100644
--- a/docs/events/mount.md
+++ b/docs/events/mount.md
@@ -2,6 +2,8 @@
The `Mount` event is sent to a widget and Application when it is first mounted.
+The mount event is typically used to set the initial state of a widget or to add new children widgets.
+
- [ ] Bubbles
## Parameters
diff --git a/docs/events/resize.md b/docs/events/resize.md
index 1c47f63b5b..36a7e75629 100644
--- a/docs/events/resize.md
+++ b/docs/events/resize.md
@@ -17,3 +17,7 @@ The `Resize` event is sent to a widget when its size changes and when it is firs
`event.container_size`
: The size of the widget's container.
+
+## Code
+
+::: textual.events.Mount
diff --git a/docs/examples/light_dark.py b/docs/examples/light_dark.py
index feb542d449..8cd3cd78ce 100644
--- a/docs/examples/light_dark.py
+++ b/docs/examples/light_dark.py
@@ -17,4 +17,5 @@ def compose(self):
def handle_pressed(self, event):
self.dark = not self.dark
+ self.bell()
event.button.label = "Lights ON" if self.dark else "Lights OFF"
diff --git a/docs/examples/styles/README.md b/docs/examples/styles/README.md
new file mode 100644
index 0000000000..8c80435825
--- /dev/null
+++ b/docs/examples/styles/README.md
@@ -0,0 +1,9 @@
+These are the examples from the documentation, used to generate screenshots.
+
+You can run them with the textual CLI.
+
+For example:
+
+```
+textual run text_style.py
+```
diff --git a/docs/examples/styles/background.py b/docs/examples/styles/background.py
new file mode 100644
index 0000000000..41f5d7e48f
--- /dev/null
+++ b/docs/examples/styles/background.py
@@ -0,0 +1,29 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+class BackgroundApp(App):
+ CSS = """
+ Static {
+ height: 1fr;
+ content-align: center middle;
+ color: white;
+ }
+ #static1 {
+ background: red;
+ }
+ #static2 {
+ background: rgb(0, 255, 0);
+ }
+ #static3 {
+ background: hsl(240, 100%, 50%);
+ }
+ """
+
+ def compose(self):
+ yield Static("Widget 1", id="static1")
+ yield Static("Widget 2", id="static2")
+ yield Static("Widget 3", id="static3")
+
+
+app = BackgroundApp()
diff --git a/docs/examples/styles/border.py b/docs/examples/styles/border.py
new file mode 100644
index 0000000000..2aa7af768e
--- /dev/null
+++ b/docs/examples/styles/border.py
@@ -0,0 +1,40 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+class BorderApp(App):
+ CSS = """
+ Screen {
+ background: white;
+ }
+ Screen > Static {
+ height: 5;
+ content-align: center middle;
+ color: white;
+ margin: 1;
+ box-sizing: border-box;
+ }
+ #static1 {
+ background: red 20%;
+ color: red;
+ border: solid red;
+ }
+ #static2 {
+ background: green 20%;
+ color: green;
+ border: dashed green;
+ }
+ #static3 {
+ background: blue 20%;
+ color: blue;
+ border: tall blue;
+ }
+ """
+
+ def compose(self):
+ yield Static("My border is solid red", id="static1")
+ yield Static("My border is dashed green", id="static2")
+ yield Static("My border is tall blue", id="static3")
+
+
+app = BorderApp()
diff --git a/docs/examples/styles/box_sizing.py b/docs/examples/styles/box_sizing.py
new file mode 100644
index 0000000000..bc264d6c88
--- /dev/null
+++ b/docs/examples/styles/box_sizing.py
@@ -0,0 +1,32 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+class BoxSizingApp(App):
+ CSS = """
+ Screen {
+ background: white;
+ color: black;
+ }
+ Static {
+ background: blue 20%;
+ height: 5;
+ margin: 2;
+ padding: 1;
+ border: wide black;
+ }
+ #static1 {
+ box-sizing: border-box;
+ }
+ #static2 {
+ box-sizing: content-box;
+ }
+
+ """
+
+ def compose(self):
+ yield Static("I'm using border-box!", id="static1")
+ yield Static("I'm using content-box!", id="static2")
+
+
+app = BoxSizingApp()
diff --git a/docs/examples/styles/color.py b/docs/examples/styles/color.py
new file mode 100644
index 0000000000..415dfbc285
--- /dev/null
+++ b/docs/examples/styles/color.py
@@ -0,0 +1,28 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+class ColorApp(App):
+ CSS = """
+ Static {
+ height:1fr;
+ content-align: center middle;
+ }
+ #static1 {
+ color: red;
+ }
+ #static2 {
+ color: rgb(0, 255, 0);
+ }
+ #static3 {
+ color: hsl(240, 100%, 50%)
+ }
+ """
+
+ def compose(self):
+ yield Static("I'm red!", id="static1")
+ yield Static("I'm rgb(0, 255, 0)!", id="static2")
+ yield Static("I'm hsl(240, 100%, 50%)!", id="static3")
+
+
+app = ColorApp()
diff --git a/docs/examples/styles/display.py b/docs/examples/styles/display.py
new file mode 100644
index 0000000000..463c767593
--- /dev/null
+++ b/docs/examples/styles/display.py
@@ -0,0 +1,27 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+class DisplayApp(App):
+ CSS = """
+ Screen {
+ background: green;
+ }
+ Static {
+ height: 5;
+ background: white;
+ color: blue;
+ border: heavy blue;
+ }
+ Static.remove {
+ display: none;
+ }
+ """
+
+ def compose(self):
+ yield Static("Widget 1")
+ yield Static("Widget 2", classes="remove")
+ yield Static("Widget 3")
+
+
+app = DisplayApp()
diff --git a/docs/examples/styles/height.py b/docs/examples/styles/height.py
new file mode 100644
index 0000000000..f94baeeeb0
--- /dev/null
+++ b/docs/examples/styles/height.py
@@ -0,0 +1,18 @@
+from textual.app import App
+from textual.widget import Widget
+
+
+class HeightApp(App):
+ CSS = """
+ Screen > Widget {
+ background: green;
+ height: 50%;
+ color: white;
+ }
+ """
+
+ def compose(self):
+ yield Widget()
+
+
+app = HeightApp()
diff --git a/docs/examples/styles/margin.py b/docs/examples/styles/margin.py
new file mode 100644
index 0000000000..6e5a3c59ab
--- /dev/null
+++ b/docs/examples/styles/margin.py
@@ -0,0 +1,33 @@
+from textual.app import App
+from textual.widgets import Static
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.
+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 MarginApp(App):
+ CSS = """
+
+ Screen {
+ background: white;
+ color: black;
+ }
+
+ Static {
+ margin: 4 8;
+ background: blue 20%;
+ border: blue wide;
+ }
+
+ """
+
+ def compose(self):
+ yield Static(TEXT)
+
+
+app = MarginApp()
diff --git a/docs/examples/styles/offset.py b/docs/examples/styles/offset.py
new file mode 100644
index 0000000000..aee8833754
--- /dev/null
+++ b/docs/examples/styles/offset.py
@@ -0,0 +1,46 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+class OffsetApp(App):
+ CSS = """
+ Screen {
+ background: white;
+ color: black;
+ layout: horizontal;
+ }
+ Static {
+ width: 20;
+ height: 10;
+ content-align: center middle;
+ }
+
+ .paul {
+ offset: 8 2;
+ background: red 20%;
+ border: outer red;
+ color: red;
+ }
+
+ .duncan {
+ offset: 4 10;
+ background: green 20%;
+ border: outer green;
+ color: green;
+ }
+
+ .chani {
+ offset: 0 5;
+ background: blue 20%;
+ border: outer blue;
+ color: blue;
+ }
+ """
+
+ def compose(self):
+ yield Static("Paul (offset 8 2)", classes="paul")
+ yield Static("Duncan (offset 4 10)", classes="duncan")
+ yield Static("Chani (offset 0 5)", classes="chani")
+
+
+app = OffsetApp()
diff --git a/docs/examples/styles/outline.py b/docs/examples/styles/outline.py
new file mode 100644
index 0000000000..0fc1a476c8
--- /dev/null
+++ b/docs/examples/styles/outline.py
@@ -0,0 +1,31 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.
+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 OutlineApp(App):
+ CSS = """
+ Screen {
+ background: white;
+ color: black;
+ }
+ Static {
+ margin: 4 8;
+ background: green 20%;
+ outline: wide green;
+ }
+ """
+
+ def compose(self):
+ yield Static(TEXT)
+
+
+app = OutlineApp()
diff --git a/docs/examples/styles/overflow.py b/docs/examples/styles/overflow.py
new file mode 100644
index 0000000000..560541170e
--- /dev/null
+++ b/docs/examples/styles/overflow.py
@@ -0,0 +1,44 @@
+from textual.app import App
+from textual.widgets import Static
+from textual.layout import Horizontal, Vertical
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.
+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 OverflowApp(App):
+ CSS = """
+ Screen {
+ background: $background;
+ color: black;
+ }
+
+ Vertical {
+ width: 1fr;
+ }
+
+ Static {
+ margin: 1 2;
+ background: blue 20%;
+ border: blue wide;
+ height: auto;
+ }
+
+ #right {
+ overflow-y: hidden;
+ }
+ """
+
+ def compose(self):
+ yield Horizontal(
+ Vertical(Static(TEXT), Static(TEXT), Static(TEXT), id="left"),
+ Vertical(Static(TEXT), Static(TEXT), Static(TEXT), id="right"),
+ )
+
+
+app = OverflowApp()
diff --git a/docs/examples/styles/padding.py b/docs/examples/styles/padding.py
new file mode 100644
index 0000000000..b65606cf14
--- /dev/null
+++ b/docs/examples/styles/padding.py
@@ -0,0 +1,32 @@
+from textual.app import App
+from textual.widgets import Static
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.
+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 PaddingApp(App):
+ CSS = """
+
+ Screen {
+ background: white;
+ color: blue;
+ }
+
+ Static {
+ padding: 4 8;
+ background: blue 20%;
+ }
+
+ """
+
+ def compose(self):
+ yield Static(TEXT)
+
+
+app = PaddingApp()
diff --git a/docs/examples/styles/scrollbar_size.py b/docs/examples/styles/scrollbar_size.py
new file mode 100644
index 0000000000..2caaafed63
--- /dev/null
+++ b/docs/examples/styles/scrollbar_size.py
@@ -0,0 +1,38 @@
+from textual.app import App
+from textual import layout
+from textual.widgets import Static
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.
+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 ScrollbarApp(App):
+ CSS = """
+ Screen {
+ background: white;
+ color: blue 80%;
+ layout: horizontal;
+ }
+
+ Static {
+ padding: 1 2;
+ width: 200;
+ }
+
+ .panel {
+ scrollbar-size: 10 4;
+ padding: 1 2;
+ }
+ """
+
+ def compose(self):
+ yield layout.Vertical(Static(TEXT * 5), classes="panel")
+
+
+app = ScrollbarApp()
diff --git a/docs/examples/styles/scrollbars.py b/docs/examples/styles/scrollbars.py
new file mode 100644
index 0000000000..0a024e5e8c
--- /dev/null
+++ b/docs/examples/styles/scrollbars.py
@@ -0,0 +1,49 @@
+from textual.app import App
+from textual import layout
+from textual.widgets import Static
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.
+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 ScrollbarApp(App):
+ CSS = """
+
+ Screen {
+ background: #212121;
+ color: white 80%;
+ layout: horizontal;
+ }
+
+ Static {
+ padding: 1 2;
+ }
+
+ .panel1 {
+ width: 1fr;
+ scrollbar-color: green;
+ scrollbar-background: #bbb;
+ padding: 1 2;
+ }
+
+ .panel2 {
+ width: 1fr;
+ scrollbar-color: yellow;
+ scrollbar-background: purple;
+ padding: 1 2;
+ }
+
+ """
+
+ def compose(self):
+ yield layout.Vertical(Static(TEXT * 5), classes="panel1")
+ yield layout.Vertical(Static(TEXT * 5), classes="panel2")
+
+
+app = ScrollbarApp()
diff --git a/docs/examples/styles/text_style.py b/docs/examples/styles/text_style.py
new file mode 100644
index 0000000000..42f9710d2b
--- /dev/null
+++ b/docs/examples/styles/text_style.py
@@ -0,0 +1,41 @@
+from textual.app import App
+from textual.widgets import Static
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.
+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 TextStyleApp(App):
+ CSS = """
+ Screen {
+ layout: horizontal;
+ }
+ Static {
+ width:1fr;
+ }
+ #static1 {
+ background: red 30%;
+ text-style: bold;
+ }
+ #static2 {
+ background: green 30%;
+ text-style: italic;
+ }
+ #static3 {
+ background: blue 30%;
+ text-style: reverse;
+ }
+ """
+
+ def compose(self):
+ yield Static(TEXT, id="static1")
+ yield Static(TEXT, id="static2")
+ yield Static(TEXT, id="static3")
+
+
+app = TextStyleApp()
diff --git a/docs/examples/styles/tint.py b/docs/examples/styles/tint.py
new file mode 100644
index 0000000000..1d6d5678b5
--- /dev/null
+++ b/docs/examples/styles/tint.py
@@ -0,0 +1,25 @@
+from textual.app import App
+from textual.color import Color
+from textual.widgets import Static
+
+
+class TintApp(App):
+ CSS = """
+ Static {
+ height: 3;
+ text-style: bold;
+ background: white;
+ color: black;
+ content-align: center middle;
+ }
+ """
+
+ def compose(self):
+ color = Color.parse("green")
+ for tint_alpha in range(0, 101, 10):
+ widget = Static(f"tint: green {tint_alpha}%;")
+ widget.styles.tint = color.with_alpha(tint_alpha / 100)
+ yield widget
+
+
+app = TintApp()
diff --git a/docs/examples/styles/visibility.py b/docs/examples/styles/visibility.py
new file mode 100644
index 0000000000..6b30954f3a
--- /dev/null
+++ b/docs/examples/styles/visibility.py
@@ -0,0 +1,27 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+class VisibilityApp(App):
+ CSS = """
+ Screen {
+ background: green;
+ }
+ Static {
+ height: 5;
+ background: white;
+ color: blue;
+ border: heavy blue;
+ }
+ Static.invisible {
+ visibility: hidden;
+ }
+ """
+
+ def compose(self):
+ yield Static("Widget 1")
+ yield Static("Widget 2", classes="invisible")
+ yield Static("Widget 3")
+
+
+app = VisibilityApp()
diff --git a/docs/examples/styles/width.py b/docs/examples/styles/width.py
index 78ee6b6990..4a1a0d0e19 100644
--- a/docs/examples/styles/width.py
+++ b/docs/examples/styles/width.py
@@ -4,9 +4,10 @@
class WidthApp(App):
CSS = """
- Widget {
- background: blue 50%;
+ Screen > Widget {
+ background: green;
width: 50%;
+ color: white;
}
"""
diff --git a/docs/introduction.md b/docs/introduction.md
index 144497747b..0cd199918b 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -42,9 +42,9 @@ The first step in all Textual applications is to import the `App` class from `te
--8<-- "docs/examples/introduction/intro01.py"
```
-This App class is responsible for loading data, setting up the screen, managing events etc. In a real app most of the core logic of your application will be contained within methods on the this class.
+This App class is responsible for loading data, setting up the screen, managing events etc. In a real app most of the core logic of your application will be contained within methods on this class.
-The last two lines create an instance of the application and calls `run()`:
+The last two lines create an instance of the application and call the `run()` method:
```python hl_lines="8 9" title="intro01.py"
--8<-- "docs/examples/introduction/intro01.py"
@@ -130,7 +130,7 @@ This script imports App as before, but also the `Widget` class from `textual.wid
Widgets support many of the same events as the Application itself, and can be thought of as mini-applications in their own right. The Clock widget responds to a Mount event which is the first event received when a widget is _mounted_ (added to the App). The code in `Clock.on_mount` sets `styles.content_align` to tuple of `("center", "middle")` which tells Textual to display the Widget's content aligned to the horizontal center, and in the middle vertically. If you resize the terminal, you should find the time remains in the center.
-The second line in `on_mount` calls `self.set_interval` which tells Textual to invoke the `self.refresh` function once a second to refresh the Clock widget.
+The second line in `on_mount` calls `self.set_interval` which tells Textual to invoke the `self.refresh` method once per second.
When Textual refreshes a widget it calls it's `render` method:
diff --git a/docs/styles/background.md b/docs/styles/background.md
new file mode 100644
index 0000000000..8571ef1114
--- /dev/null
+++ b/docs/styles/background.md
@@ -0,0 +1,41 @@
+# Background
+
+The `background` rule sets the background color of the widget.
+
+=== "background.py"
+
+ ```python
+ --8<-- "docs/examples/styles/background.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/background.py"}
+ ```
+
+## CSS
+
+```sass
+/* Blue background */
+background: blue;
+
+/* 20% red backround */
+background: red 20%;
+
+/* RGB color */
+background: rgb(100,120,200);
+```
+
+## Python
+
+You can use the same syntax as CSS, or explicitly set a `Color` object for finer-grained control.
+
+```python
+# Set blue background
+widget.styles.background = "blue"
+
+from textual.color import Color
+# Set with a color object
+widget.styles.background = Color.parse("pink")
+widget.styles.background = Color(120, 60, 100)
+```
diff --git a/docs/styles/border.md b/docs/styles/border.md
new file mode 100644
index 0000000000..03992b3249
--- /dev/null
+++ b/docs/styles/border.md
@@ -0,0 +1,68 @@
+# Border
+
+The `border` rule enables the drawing of a box around a widget. A border is set with a border value (see below) followed by a color.
+
+| Border value | Explanation |
+| ------------ | ------------------------------------------------------- |
+| `"ascii"` | A border with plus, hyphen, and vertical bar |
+| `"blank"` | A blank border (reserves space for a border) |
+| `"dashed"` | Dashed line border |
+| `"double"` | Double lined border |
+| `"heavy"` | Heavy border |
+| `"hidden"` | Alias for "none" |
+| `"hkey"` | Horizontal key-line border |
+| `"inner"` | Thick solid border |
+| `"none"` | Disabled border |
+| `"outer"` | Think solid border with additional space around content |
+| `"round"` | Rounded corners |
+| `"solid"` | Solid border |
+| `"tall"` | Solid border with extras space top and bottom |
+| `"vkey"` | Vertical key-line border |
+| `"wide"` | Solid border with additional space left and right |
+
+For example `heavy white` would display a heavy white line around a widget.
+
+Borders may also be set individually for the four edges of a widget with the `border-top`, `border-right`, `border-bottom` and `border-left` rules.
+
+## Border command
+
+The `textual` CLI has a subcommand which will let you explore the various border types:
+
+```
+textual borders
+```
+
+## Example
+
+This examples shows three widgets with different border styles.
+
+=== "border.py"
+
+ ```python
+ --8<-- "docs/examples/styles/border.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/border.py"}
+ ```
+
+## CSS
+
+```sass
+/* Set a heavy white border */
+border: heavy white;
+
+/* set a red border on the left */
+border-left: outer red;
+```
+
+## Python
+
+```python
+# Set a heavy white border
+widget.border = ("heavy", "white")
+
+# Set a red border on the left
+widget.border_left = ("outer", "red")
+```
diff --git a/docs/styles/box_sizing.md b/docs/styles/box_sizing.md
new file mode 100644
index 0000000000..6ef19eadfc
--- /dev/null
+++ b/docs/styles/box_sizing.md
@@ -0,0 +1,42 @@
+# Box-sizing
+
+The `box-sizing` rule impacts how `width` and `height` rules are translated in to screen dimensions, when combined with `padding` and `border`.
+
+The default value is `border-box` which means that padding and border are included in the width and height. This setting means that if you add padding and/or border the widget will not change in size, but you will have less space for content.
+
+You can set `box-sizing` to `content-box` which tells Textual that padding and border should increase the size of the widget, leaving the content area unaffected.
+
+## Example
+
+Both widgets in this example have the same height (5). The top widget has `box-sizing: border-box` which means that padding and border reduces the space for content. The bottom widget has `box-sizing: content-box` which increases the size of the widget to compensate for padding and border.
+
+=== "box_sizing.py"
+
+ ```python
+ --8<-- "docs/examples/styles/box_sizing.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/box_sizing.py"}
+ ```
+
+## CSS
+
+```sass
+/* Set box sizing to border-box (default) */
+box-sizing: border-box;
+
+/* Set box sizing to content-box */
+box-sizing: content-box;
+```
+
+## Python
+
+```python
+# Set box sizing to border-box (default)
+widget.box_sizing = "border-box"
+
+# Set box sizing to content-box
+widget.box_sizing = "content-box"
+```
diff --git a/docs/styles/color.md b/docs/styles/color.md
new file mode 100644
index 0000000000..88d02b34a5
--- /dev/null
+++ b/docs/styles/color.md
@@ -0,0 +1,41 @@
+# Color
+
+The `color` rule sets the text color of a Widget.
+
+=== "color.py"
+
+ ```python
+ --8<-- "docs/examples/styles/color.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/color.py"}
+ ```
+
+## CSS
+
+```sass
+/* Blue text */
+color: blue;
+
+/* 20% red text */
+color: red 20%;
+
+/* RGB color */
+color: rgb(100,120,200);
+```
+
+## Python
+
+You can use the same syntax as CSS, or explicitly set a `Color` object.
+
+```python
+# Set blue text
+widget.styles.color = "blue"
+
+from textual.color import Color
+# Set with a color object
+widget.styles.color = Color.parse("pink")
+
+```
diff --git a/docs/styles/display.md b/docs/styles/display.md
new file mode 100644
index 0000000000..53ecaaaf22
--- /dev/null
+++ b/docs/styles/display.md
@@ -0,0 +1,48 @@
+# Display
+
+The `display` property defines if a Widget is displayed or not. The default value is `"block"` which will display the widget as normal. Setting the property to `"none"` will effectively make it invisible.
+
+## Example
+
+Note that the second widget is hidden by adding the "hidden" class which sets the display style to None.
+
+=== "display.py"
+
+ ```python
+ --8<-- "docs/examples/styles/display.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/display.py"}
+ ```
+
+## CSS
+
+```sass
+/* Widget is on screen */
+display: block;
+
+/* Widget is not on the screen */
+display: none;
+```
+
+## Python
+
+```python
+# Hide the widget
+self.styles.display = "none"
+
+# Show the widget again
+self.styles.display = "block"
+```
+
+There is also a shortcut to show / hide a widget. The `display` property on `Widget` may be set to `True` or `False` to show or hide the widget.
+
+```python
+# Hide the widget
+widget.display = False
+
+# Show the widget
+widget.display = True
+```
diff --git a/docs/styles/height.md b/docs/styles/height.md
new file mode 100644
index 0000000000..b7ba4a0db9
--- /dev/null
+++ b/docs/styles/height.md
@@ -0,0 +1,37 @@
+# Height
+
+The `height` rule sets a widget's height. By default, it sets the height of the content area, but if `box-sizing` is set to `border-box` it sets the height of the border area.
+
+## Example
+
+=== "height.py"
+
+ ```python
+ --8<-- "docs/examples/styles/height.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/height.py"}
+ ```
+
+## CSS
+
+```sass
+/* Explicit cell height */
+height: 10;
+
+/* Percentage height */
+height: 50%;
+
+/* Automatic height */
+width: auto
+```
+
+## Python
+
+```python
+self.styles.height = 10
+self.styles.height = "50%
+self.styles.height = "auto"
+```
diff --git a/docs/styles/margin.md b/docs/styles/margin.md
new file mode 100644
index 0000000000..584657301f
--- /dev/null
+++ b/docs/styles/margin.md
@@ -0,0 +1,38 @@
+# Margin
+
+The `margin` rule adds space around the entire widget. Margin may be specified with 1, 2 or 4 values.
+
+| example | |
+| ------------------ | ------------------------------------------------------------------- |
+| `margin: 1;` | A single value sets a margin of 1 around all 4 edges |
+| `margin: 1 2;` | Two values sets the margin for the top/bottom and left/right edges |
+| `margin: 1 2 3 4;` | Four values sets top, right, bottom, and left margins independently |
+
+Margin may also be set individually by setting `margin-top`, `margin-right`, `margin-bottom`, or `margin-left` to an single value.
+
+## Example
+
+=== "margin.py"
+
+ ```python
+ --8<-- "docs/examples/styles/margin.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/margin.py"}
+ ```
+
+## CSS
+
+```sass
+/* Set margin of 2 on the top and bottom edges, and 4 on the left and right */
+margin: 2 4;
+```
+
+## Python
+
+```python
+# In Python you can set the margin as a tuple of integers
+widget.styles.margin = (2, 3)
+```
diff --git a/docs/styles/max_height.md b/docs/styles/max_height.md
new file mode 100644
index 0000000000..dcf10193eb
--- /dev/null
+++ b/docs/styles/max_height.md
@@ -0,0 +1,25 @@
+# Max-height
+
+The `max-height` rule sets a maximum width for a widget.
+
+## CSS
+
+```sass
+
+/* Set a maximum height of 10 rows */
+max-height: 10;
+
+/* Set a maximum height of 25% of the screen height */
+max-height: 25vh;
+```
+
+## Python
+
+```python
+# Set the maximum width to 10 rows
+widget.styles.max_height = 10
+
+# Set the maximum width to 25% of the screen width
+widget.styles.max_height = "25vw"
+
+```
diff --git a/docs/styles/max_width.md b/docs/styles/max_width.md
new file mode 100644
index 0000000000..deac40f97c
--- /dev/null
+++ b/docs/styles/max_width.md
@@ -0,0 +1,25 @@
+# Max-width
+
+The `max-width` rule sets a maximum width for a widget.
+
+## CSS
+
+```sass
+
+/* Set a maximum width of 10 cells */
+max-width: 10;
+
+/* Set a maximum width of 25% of the screen width */
+max-width: 25vh;
+```
+
+## Python
+
+```python
+# Set the maximum width to 10 cells
+widget.styles.max_width = 10
+
+# Set the maximum width to 25% of the screen width
+widget.styles.max_width = "25vw"
+
+```
diff --git a/docs/styles/min_height.md b/docs/styles/min_height.md
new file mode 100644
index 0000000000..51426e3977
--- /dev/null
+++ b/docs/styles/min_height.md
@@ -0,0 +1,25 @@
+# Min-height
+
+The `min-height` rule sets a minimum height for a widget.
+
+## CSS
+
+```sass
+
+/* Set a minimum height of 10 rows */
+min-height: 10;
+
+/* Set a minimum height of 25% of the screen height */
+min-height: 25vh;
+```
+
+## Python
+
+```python
+# Set the minimum height to 10 rows
+self.styles.min_height = 10
+
+# Set the minimum height to 25% of the screen height
+self.styles.min_height = "25vh"
+
+```
diff --git a/docs/styles/min_width.md b/docs/styles/min_width.md
new file mode 100644
index 0000000000..b6a4cf2c35
--- /dev/null
+++ b/docs/styles/min_width.md
@@ -0,0 +1,25 @@
+# Min-width
+
+The `min-width` rules sets a minimum width for a widget.
+
+## CSS
+
+```sass
+
+/* Set a minimum width of 10 cells */
+min-width: 10;
+
+/* Set a minimum width of 25% of the screen width */
+min-width: 25vh;
+```
+
+## Python
+
+```python
+# Set the minimum width to 10 cells
+widget.styles.min_width = 10
+
+# Set the minimum width to 25% of the screen height
+widget.styles.min_width = "25vh"
+
+```
diff --git a/docs/styles/offset.md b/docs/styles/offset.md
new file mode 100644
index 0000000000..1c794c3f77
--- /dev/null
+++ b/docs/styles/offset.md
@@ -0,0 +1,30 @@
+# Offset
+
+The `offset` rule adds an offset to the widget's position.
+
+## Example
+
+=== "offset.py"
+
+ ```python
+ --8<-- "docs/examples/styles/offset.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/offset.py"}
+ ```
+
+## CSS
+
+```sass
+/* Move the widget 2 cells in the x direction, and 4 in the y direction. */
+offset: 2 4;
+```
+
+## Python
+
+```python
+# Move the widget 2 cells in the x direction, and 4 in the y direction.
+widget.styles.offset = (2, 4)
+```
diff --git a/docs/styles/outline.md b/docs/styles/outline.md
new file mode 100644
index 0000000000..8d438a095d
--- /dev/null
+++ b/docs/styles/outline.md
@@ -0,0 +1,62 @@
+# Outline
+
+The `outline` rule enables the drawing of a box around a widget. Similar to `border`, but unlike border, outline will draw over the content area. This rule can be useful for emphasis if you want to display a outline for a brief time to draw the user's attention to it.
+
+An outline is set with a border value (see below) followed by a color.
+
+| Border value | Explanation |
+| ------------ | ------------------------------------------------------- |
+| `"ascii"` | A border with plus, hyphen, and vertical bar |
+| `"blank"` | A blank border (reserves space for a border) |
+| `"dashed"` | Dashed line border |
+| `"double"` | Double lined border |
+| `"heavy"` | Heavy border |
+| `"hidden"` | Alias for "none" |
+| `"hkey"` | Horizontal key-line border |
+| `"inner"` | Thick solid border |
+| `"none"` | Disabled border |
+| `"outer"` | Think solid border with additional space around content |
+| `"round"` | Rounded corners |
+| `"solid"` | Solid border |
+| `"tall"` | Solid border with extras space top and bottom |
+| `"vkey"` | Vertical key-line border |
+| `"wide"` | Solid border with additional space left and right |
+
+For example `heavy white` would display a heavy white line around a widget.
+
+Outlines may also be set individually with the `outline-top`, `outline-right`, `outline-bottom` and `outline-left` rules.
+
+## Example
+
+This examples shows a widget with an outline. Note how the outline occludes the text area.
+
+=== "outline.py"
+
+ ```python
+ --8<-- "docs/examples/styles/outline.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/outline.py"}
+ ```
+
+## CSS
+
+```sass
+/* Set a heavy white outline */
+outline: heavy white;
+
+/* set a red outline on the left */
+outline-left: outer red;
+```
+
+## Python
+
+```python
+# Set a heavy white outline
+widget.outline = ("heavy", "white)
+
+# Set a red outline on the left
+widget.outline_left = ("outer", "red)
+```
diff --git a/docs/styles/overflow.md b/docs/styles/overflow.md
new file mode 100644
index 0000000000..8d0adaf52d
--- /dev/null
+++ b/docs/styles/overflow.md
@@ -0,0 +1,54 @@
+# Overflow
+
+The `overflow` rule specifies if and when scrollbars should be displayed on the `x` and `y` axis. The rule takes two overflow values; one for the horizontal bar (x axis), followed by the vertical bar (y-axis).
+
+| Overflow value | Effect |
+| -------------- | ------------------------------------------------------------------------- |
+| `"auto"` | Automatically show the scrollbar if the content doesn't fit (the default) |
+| `"hidden"` | Never show the scrollbar |
+| `"scroll"` | Always show the scrollbar |
+
+The default value for overflow is `"auto auto"` which will show scrollbars automatically for both scrollbars if content doesn't fit within container.
+
+Overflow may also be set independently by setting the `overflow-x` rule for the horizontal bar, and `overflow-y` for the vertical bar.
+
+## Example
+
+Here we split the screen in to left and right sections, each with three vertically scrolling widgets that do not fit in to the height of the terminal.
+
+The left side has `overflow-y: auto` (the default) and will automatically show a scrollbar. The right side has `overflow-y: hidden` which will prevent a scrollbar from being show.
+
+=== "width.py"
+
+ ```python
+ --8<-- "docs/examples/styles/overflow.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/overflow.py"}
+ ```
+
+## CSS
+
+```sass
+/* Automatic scrollbars on both axies (the default) */
+overflow: auto auto;
+
+/* Hide the vertical scrollbar */
+overflow-y: hidden;
+
+/* Always show the horizontal scrollbar */
+overflow-x: scroll;
+```
+
+## Python
+
+```python
+# Hide the vertical scrollbar
+widget.styles.overflow_y = "hidden"
+
+# Always show the horizontal scrollbar
+widget.styles.overflow_x = "scroll"
+
+```
diff --git a/docs/styles/padding.md b/docs/styles/padding.md
new file mode 100644
index 0000000000..f8f54477bb
--- /dev/null
+++ b/docs/styles/padding.md
@@ -0,0 +1,40 @@
+# Padding
+
+The padding rule adds space around the content of a widget. You can specify padding with 1, 2 or 4 numbers.
+
+| example | |
+| ------------------- | ------------------------------------------------------------------- |
+| `padding: 1;` | A single value sets a padding of 1 around all 4 edges |
+| `padding: 1 2;` | Two values sets the padding for the top/bottom and left/right edges |
+| `padding: 1 2 3 4;` | Four values sets top, right, bottom, and left padding independently |
+
+Padding may also be set individually by setting `padding-top`, `padding-right`, `padding-bottom`, or `padding-left` to an single value.
+
+## Example
+
+This example adds padding around static text.
+
+=== "padding.py"
+
+ ```python
+ --8<-- "docs/examples/styles/padding.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/padding.py"}
+ ```
+
+## CSS
+
+```sass
+/* Set padding of 2 on the top and bottom edges, and 4 on the left and right */
+padding: 2 4;
+```
+
+## Python
+
+```python
+# In Python you can set the padding as a tuple of integers
+widget.styles.padding = (2, 3)
+```
diff --git a/docs/styles/scrollbar.md b/docs/styles/scrollbar.md
new file mode 100644
index 0000000000..9a11b1cd95
--- /dev/null
+++ b/docs/styles/scrollbar.md
@@ -0,0 +1,43 @@
+# Scrollbar colors
+
+There are a number of rules to set the colors used in Textual scrollbars. You won't typically need to do this, as the default themes have carefully chosen colors, but you can if you want to.
+
+| Rule | Color |
+| ----------------------------- | ------------------------------------------------------- |
+| `scrollbar-color` | Scrollbar "thumb" (movable part) |
+| `scrollbar-color-hover` | Scrollbar thumb when the mouse is hovering over it |
+| `scrollbar-color-active` | Scrollbar thumb when it is active (being dragged) |
+| `scrollbar-background` | Scrollbar background |
+| `scrollbar-background-hover` | Scrollbar background when the mouse is hovering over it |
+| `scrollbar-background-active` | Scrollbar background when the thumb is being dragged |
+
+## Example
+
+In this example we have two panels with different scrollbar colors set for each.
+
+=== "scrollbars.py"
+
+ ```python
+ --8<-- "docs/examples/styles/scrollbars.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/scrollbars.py"}
+ ```
+
+## CSS
+
+```sass
+/* Set widget scrollbar color to yellow */
+Widget {
+ scrollbar-color: yellow;
+}
+```
+
+## Python
+
+```python
+# Set the scrollbar color to yellow
+widget.styles.scrollbar_color = "yellow"
+```
diff --git a/docs/styles/scrollbar_size.md b/docs/styles/scrollbar_size.md
new file mode 100644
index 0000000000..f6653c2543
--- /dev/null
+++ b/docs/styles/scrollbar_size.md
@@ -0,0 +1,37 @@
+# Scrollbar-size
+
+The `scrollbar-size` rule changes the size of the scrollbars. It takes 2 integers for horizontal and vertical scrollbar size respectively.
+
+The scrollbar dimensions may also be set individually with `scrollbar-size-horizontal` and `scrollbar-size-vertical`.
+
+## Example
+
+In this example we modify the size of the widgets scrollbar to be _much_ larger than usual.
+
+=== "scrollbar_size.py"
+
+ ```python
+ --8<-- "docs/examples/styles/scrollbar_size.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/scrollbar_size.py"}
+ ```
+
+## CSS
+
+```sass
+/* Set horizontal scrollbar to 10, and vertical scrollbar to 4 */
+Widget {
+ scrollbar-size: 10 4;
+}
+```
+
+## Python
+
+```python
+# Set horizontal scrollbar to 10, and vertical scrollbar to 4
+widget.styles.horizontal_scrollbar = 10
+widget.styles.vertical_scrollbar = 10
+```
diff --git a/docs/styles/text_style.md b/docs/styles/text_style.md
new file mode 100644
index 0000000000..3dc1037fe8
--- /dev/null
+++ b/docs/styles/text_style.md
@@ -0,0 +1,40 @@
+# Text-style
+
+The `text-style` rule enables a number of different ways of displaying text. The value may be set to any of the following:
+
+| Style | Effect |
+| ------------- | -------------------------------------------------------------- |
+| `"bold"` | **bold text** |
+| `"italic"` | _italic text_ |
+| `"reverse"` | reverse video text (foreground and background colors reversed) |
+| `"underline"` | underline text |
+| `"strike"` | strikethrough text |
+
+Text styles may be set in combination. For example "bold underline" or "reverse underline strike".
+
+## Example
+
+Each of the three text panels has a different text style.
+
+=== "text_style.py"
+
+ ```python
+ --8<-- "docs/examples/styles/text_style.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/text_style.py"}
+ ```
+
+## CSS
+
+```sass
+text-style: italic;
+```
+
+## Python
+
+```python
+widget.styles.text_style = "italic"
+```
diff --git a/docs/styles/tint.md b/docs/styles/tint.md
new file mode 100644
index 0000000000..d2d412955b
--- /dev/null
+++ b/docs/styles/tint.md
@@ -0,0 +1,39 @@
+# Tint
+
+The tint rule blends a color with the widget. The color should likely have an _alpha_ component, or the end result would obscure the widget content.
+
+## Example
+
+This examples shows a green tint with gradually increasing alpha.
+
+=== "tint.py"
+
+ ```python
+ --8<-- "docs/examples/styles/tint.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/tint.py"}
+ ```
+
+## CSS
+
+```sass
+/* A red tint (could indicate an error) */
+tint: red 20%
+
+/* A green tint */
+tint: rgba(0, 200, 0, 0.3);
+```
+
+# Python
+
+```python
+# A red tint
+from textual.color import Color
+widget.styles.tint = Color.parse("red").with_alpha(0.2);
+
+# A green tint
+widget.styles.tint = "rgba(0, 200, 0, 0.3):
+```
diff --git a/docs/styles/visibility.md b/docs/styles/visibility.md
new file mode 100644
index 0000000000..a6c843bf8e
--- /dev/null
+++ b/docs/styles/visibility.md
@@ -0,0 +1,48 @@
+# Visibility
+
+The `visibility` rule may be used to make a widget invisible while still reserving spacing for it. The default value is `"visible"` which will cause the Widget to be displayed as normal. Setting the value to `"hidden"` will cause the Widget to become invisible.
+
+## Example
+
+Note that the second widget is hidden, while leaving a space where it would have been rendered.
+
+=== "visibility.py"
+
+ ```python
+ --8<-- "docs/examples/styles/visibility.py"
+ ```
+
+=== "Output"
+
+ ```{.textual path="docs/examples/styles/visibility.py"}
+ ```
+
+## CSS
+
+```sass
+/* Widget is on screen */
+visibility: visible;
+
+/* Widget is not on the screen */
+visibility: hidden;
+```
+
+## Python
+
+```python
+# Widget is invisible
+self.styles.visibility = "hidden"
+
+# Widget is visible
+self.styles.visibility = "visible"
+```
+
+There is also a shortcut to set a Widget's visibility. The `visible` property on `Widget` may be set to `True` or `False`.
+
+```python
+# Make a widget invisible
+widget.visible = False
+
+# Make the widget visible again
+widget.visible = True
+```
diff --git a/docs/styles/width.md b/docs/styles/width.md
index 18cc7b57e7..6665520022 100644
--- a/docs/styles/width.md
+++ b/docs/styles/width.md
@@ -1,6 +1,6 @@
# Width
-The `width` property sets a widget's width. By default, it sets the width of the content area, but if `box-sizing` is set to `border-box` it sets the width of the border area.
+The `width` rule sets a widget's width. By default, it sets the width of the content area, but if `box-sizing` is set to `border-box` it sets the width of the border area.
## Example
diff --git a/examples/borders.py b/examples/borders.py
new file mode 100644
index 0000000000..b3f8ece6d5
--- /dev/null
+++ b/examples/borders.py
@@ -0,0 +1,29 @@
+from itertools import cycle
+
+from textual.app import App
+from textual.color import Color
+from textual.constants import BORDERS
+from textual.widgets import Static
+
+
+class BorderApp(App):
+ """Displays a pride flag."""
+
+ COLORS = ["red", "orange", "yellow", "green", "blue", "purple"]
+
+ def compose(self):
+ self.dark = True
+ for border, color in zip(BORDERS, cycle(self.COLORS)):
+ static = Static(f"border: {border} {color};")
+ static.styles.height = 7
+ static.styles.background = Color.parse(color).with_alpha(0.2)
+ static.styles.margin = (1, 2)
+ static.styles.border = (border, color)
+ static.styles.content_align = ("center", "middle")
+ yield static
+
+
+app = BorderApp()
+
+if __name__ == "__main__":
+ app.run()
diff --git a/mkdocs.yml b/mkdocs.yml
index cd5aee9e34..d35b2d6765 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -11,6 +11,26 @@ nav:
- "events/mount.md"
- "events/resize.md"
- Styles:
+ - "styles/background.md"
+ - "styles/border.md"
+ - "styles/box_sizing.md"
+ - "styles/color.md"
+ - "styles/display.md"
+ - "styles/min_height.md"
+ - "styles/max_height.md"
+ - "styles/min_width.md"
+ - "styles/max_width.md"
+ - "styles/height.md"
+ - "styles/margin.md"
+ - "styles/offset.md"
+ - "styles/outline.md"
+ - "styles/overflow.md"
+ - "styles/padding.md"
+ - "styles/scrollbar.md"
+ - "styles/scrollbar_size.md"
+ - "styles/text_style.md"
+ - "styles/tint.md"
+ - "styles/visibility.md"
- "styles/width.md"
- Widgets: "/widgets/"
- Reference:
diff --git a/sandbox/will/basic.css b/sandbox/will/basic.css
index 3c9043b6b3..fef9e4248b 100644
--- a/sandbox/will/basic.css
+++ b/sandbox/will/basic.css
@@ -100,7 +100,7 @@ Tweet {
.scrollable {
-
+ overflow-x: auto;
overflow-y: scroll;
margin: 1 2;
height: 20;
diff --git a/sandbox/will/basic.py b/sandbox/will/basic.py
index dee825f0e7..bda44f314f 100644
--- a/sandbox/will/basic.py
+++ b/sandbox/will/basic.py
@@ -9,39 +9,51 @@
from textual.widgets import Static, DataTable
CODE = '''
-class Offset(NamedTuple):
- """A point defined by x and y coordinates."""
-
- x: int = 0
- y: int = 0
-
- @property
- def is_origin(self) -> bool:
- """Check if the point is at the origin (0, 0)"""
- return self == (0, 0)
-
- def __bool__(self) -> bool:
- return self != (0, 0)
-
- def __add__(self, other: object) -> Offset:
- if isinstance(other, tuple):
- _x, _y = self
- x, y = other
- return Offset(_x + x, _y + y)
- return NotImplemented
-
- def __sub__(self, other: object) -> Offset:
- if isinstance(other, tuple):
- _x, _y = self
- x, y = other
- return Offset(_x - x, _y - y)
- return NotImplemented
-
- def __mul__(self, other: object) -> Offset:
- if isinstance(other, (float, int)):
- x, y = self
- return Offset(int(x * other), int(y * other))
- return NotImplemented
+from __future__ import annotations
+
+from typing import Iterable, TypeVar
+
+T = TypeVar("T")
+
+
+def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for first value."""
+ iter_values = iter(values)
+ try:
+ value = next(iter_values)
+ except StopIteration:
+ return
+ yield True, value
+ for value in iter_values:
+ yield False, value
+
+
+def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
+ """Iterate and generate a tuple with a flag for last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ for value in iter_values:
+ yield False, previous_value
+ previous_value = value
+ yield True, previous_value
+
+
+def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
+ """Iterate and generate a tuple with a flag for first and last value."""
+ iter_values = iter(values)
+ try:
+ previous_value = next(iter_values)
+ except StopIteration:
+ return
+ first = True
+ for value in iter_values:
+ yield first, False, previous_value
+ first = False
+ previous_value = value
+ yield first, True, previous_value
'''
@@ -111,7 +123,10 @@ def compose(self) -> ComposeResult:
yield from (
Tweet(TweetBody()),
Widget(
- Static(Syntax(CODE, "python"), classes="code"),
+ Static(
+ Syntax(CODE, "python", line_numbers=True, indent_guides=True),
+ classes="code",
+ ),
classes="scrollable",
),
table,
diff --git a/src/textual/cli/cli.py b/src/textual/cli/cli.py
index d7409a10f9..f303246f61 100644
--- a/src/textual/cli/cli.py
+++ b/src/textual/cli/cli.py
@@ -18,7 +18,7 @@ def run():
pass
-@run.command(help="Run the Textual Devtools console")
+@run.command(help="Run the Textual Devtools console.")
def console():
_run_devtools()
@@ -157,3 +157,11 @@ def run_app(import_name: str, dev: bool) -> None:
sys.exit(1)
app.run()
+
+
+@run.command("borders")
+def borders():
+ """Explore the border styles available in Textual."""
+ from ..devtools import borders
+
+ borders.app.run()
diff --git a/src/textual/color.py b/src/textual/color.py
index 3d74584513..7b2be31bf0 100644
--- a/src/textual/color.py
+++ b/src/textual/color.py
@@ -203,7 +203,7 @@ def hex(self) -> str:
str: A CSS hex-style color, e.g. "#46b3de" or "#3342457f"
"""
- r, g, b, a = self
+ r, g, b, a = self.clamped
return (
f"#{r:02X}{g:02X}{b:02X}"
if a == 1
diff --git a/src/textual/constants.py b/src/textual/constants.py
new file mode 100644
index 0000000000..6429da7906
--- /dev/null
+++ b/src/textual/constants.py
@@ -0,0 +1,11 @@
+"""
+Constants that we might want to expose via the public API.
+
+"""
+
+from ._border import BORDER_CHARS
+
+__all__ = ["BORDERS"]
+
+
+BORDERS = list(BORDER_CHARS)
diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py
index 220add7dcf..d24835e933 100644
--- a/src/textual/css/_help_text.py
+++ b/src/textual/css/_help_text.py
@@ -443,52 +443,6 @@ def layout_property_help_text(property_name: str, context: StylingContext) -> He
)
-def docks_property_help_text(property_name: str, context: StylingContext) -> HelpText:
- """Help text to show when the user supplies an invalid value for docks.
-
- Args:
- property_name (str): The name of the property
- context (StylingContext | None): The context the property is being used in.
-
- Returns:
- HelpText: Renderable for displaying the help text for this property
- """
- property_name = _contextualize_property_name(property_name, context)
- return HelpText(
- summary=f"Invalid value for [i]{property_name}[/] property",
- bullets=[
- *ContextSpecificBullets(
- inline=[
- Bullet(
- f"The [i]{property_name}[/] property expects an iterable of DockGroups",
- examples=[
- Example(
- f"widget.styles.{property_name} = [DockGroup(...), DockGroup(...)]"
- )
- ],
- ),
- ],
- css=[
- Bullet(
- f"The [i]{property_name}[/] property expects a value of the form =/...",
- examples=[
- Example(
- f"{property_name}: lhs=left/2; [dim]# dock named [u]lhs[/], on [u]left[/] edge, with z-index [u]2[/]"
- ),
- Example(
- f"{property_name}: top=top/3 rhs=right/2; [dim]# declaring multiple docks"
- ),
- ],
- ),
- Bullet(" can be any string you want"),
- Bullet(f" must be one of {friendly_list(VALID_EDGE)}"),
- Bullet(f" must be an integer"),
- ],
- ).get_by_context(context)
- ],
- )
-
-
def dock_property_help_text(property_name: str, context: StylingContext) -> HelpText:
"""Help text to show when the user supplies an invalid value for dock.
diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py
index 8eee77d6ac..f52021c472 100644
--- a/src/textual/css/_styles_builder.py
+++ b/src/textual/css/_styles_builder.py
@@ -15,7 +15,6 @@
string_enum_help_text,
border_property_help_text,
layout_property_help_text,
- docks_property_help_text,
dock_property_help_text,
fractional_property_help_text,
align_help_text,
diff --git a/src/textual/devtools/borders.py b/src/textual/devtools/borders.py
new file mode 100644
index 0000000000..ac1dea6e70
--- /dev/null
+++ b/src/textual/devtools/borders.py
@@ -0,0 +1,64 @@
+from textual.app import App, ComposeResult
+from textual.constants import BORDERS
+from textual.widgets import Button, Static
+from textual import layout
+
+
+TEXT = """I must not fear.
+Fear is the mind-killer.
+Fear is the little-death that brings total obliteration.
+I will face my fear.
+I will permit it to pass over me and through me.
+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(layout.Vertical):
+ CSS = """
+ BorderButtons {
+ dock: left;
+ width: 24;
+ }
+
+ BorderButtons > Button {
+ width: 100%;
+ }
+ """
+
+ def compose(self) -> ComposeResult:
+ for border in BORDERS:
+ if border:
+ yield Button(border, id=border)
+
+
+class BorderApp(App):
+ """Demonstrates the border styles."""
+
+ CSS = """
+ Static {
+ margin: 2 4;
+ padding: 2 4;
+ border: solid $primary;
+ height: auto;
+ background: $panel;
+ color: $text-panel;
+ }
+ """
+
+ def compose(self):
+ yield BorderButtons()
+ self.text = Static(TEXT)
+ yield self.text
+
+ def handle_pressed(self, event):
+ self.text.styles.border = (
+ event.button.id,
+ self.stylesheet.variables["primary"],
+ )
+ self.bell()
+
+
+app = BorderApp()
+
+if __name__ == "__main__":
+ app.run()
diff --git a/src/textual/events.py b/src/textual/events.py
index c5b96dacb3..a2822ec602 100644
--- a/src/textual/events.py
+++ b/src/textual/events.py
@@ -87,8 +87,7 @@ def __rich_repr__(self) -> rich.repr.Result:
class Resize(Event, verbosity=2, bubble=False):
"""Sent when the app or widget has been resized."""
- __slots__ = ["size"]
- size: Size
+ __slots__ = ["size", "virtual_size", "container_size"]
def __init__(
self,
diff --git a/src/textual/geometry.py b/src/textual/geometry.py
index 620d2419b8..176586c99a 100644
--- a/src/textual/geometry.py
+++ b/src/textual/geometry.py
@@ -45,10 +45,13 @@ def clamp(value: T, minimum: T, maximum: T) -> T:
class Offset(NamedTuple):
- """A point defined by x and y coordinates."""
+ """A cell offset defined by x and y coordinates. Offsets are typically relative to the
+ top left of the terminal or other container."""
x: int = 0
+ """Offset in the x-axis (horizontal)"""
y: int = 0
+ """Offset in the y-axis (vertical)"""
@property
def is_origin(self) -> bool:
@@ -118,7 +121,10 @@ class Size(NamedTuple):
"""An area defined by its width and height."""
width: int = 0
+ """The width in cells."""
+
height: int = 0
+ """The height in cells."""
def __bool__(self) -> bool:
"""A Size is Falsy if it has area 0."""
@@ -196,12 +202,32 @@ def __contains__(self, other: Any) -> bool:
class Region(NamedTuple):
- """Defines a rectangular region."""
+ """Defines a rectangular region.
+
+ A Region consists a coordinate (x and y) and dimensions (width and height).
+
+ ```
+ (x, y)
+ ┌────────────────────┐ ▲
+ │ │ │
+ │ │ │
+ │ │ height
+ │ │ │
+ │ │ │
+ └────────────────────┘ ▼
+ ◀─────── width ──────▶
+ ```
+
+ """
x: int = 0
+ """Offset in the x-axis (horizontal)"""
y: int = 0
+ """Offset in the y-axis (vertical)"""
width: int = 0
+ """The widget of the region"""
height: int = 0
+ """The height of the region"""
@classmethod
def from_union(
@@ -754,9 +780,13 @@ class Spacing(NamedTuple):
"""The spacing around a renderable."""
top: int = 0
+ """Space from the top of a region."""
right: int = 0
+ """Space from the left of a region."""
bottom: int = 0
+ """Space from the bottom of a region."""
left: int = 0
+ """Space from the left of a region."""
def __bool__(self) -> bool:
return self != (0, 0, 0, 0)
diff --git a/src/textual/layouts/horizontal.py b/src/textual/layouts/horizontal.py
index c2749b9acb..79ca328096 100644
--- a/src/textual/layouts/horizontal.py
+++ b/src/textual/layouts/horizontal.py
@@ -30,7 +30,7 @@ def arrange(
total_fraction = sum(
[int(style.width.value) for style in styles if style.width.is_fraction]
)
- fraction_unit = Fraction(size.height, total_fraction or 1)
+ fraction_unit = Fraction(size.width, total_fraction or 1)
box_models = [
widget.get_box_model(size, parent_size, fraction_unit)
diff --git a/src/textual/renderables/tint.py b/src/textual/renderables/tint.py
index 196a07be49..022862b3a8 100644
--- a/src/textual/renderables/tint.py
+++ b/src/textual/renderables/tint.py
@@ -39,18 +39,29 @@ def process_segments(
from_rich_color = Color.from_rich_color
style_from_color = Style.from_color
_Segment = Segment
+
+ NULL_STYLE = Style()
for segment in segments:
text, style, control = segment
- if control or style is None:
+ if control:
yield segment
else:
+ style = style or NULL_STYLE
yield _Segment(
text,
(
style
+ style_from_color(
- (from_rich_color(style.color) + color).rich_color,
- (from_rich_color(style.bgcolor) + color).rich_color,
+ (
+ (from_rich_color(style.color) + color).rich_color
+ if style.color is not None
+ else None
+ ),
+ (
+ (from_rich_color(style.bgcolor) + color).rich_color
+ if style.bgcolor is not None
+ else None
+ ),
)
),
control,
diff --git a/src/textual/widgets/_button.py b/src/textual/widgets/_button.py
index f1b1dba84f..aa4ccf4759 100644
--- a/src/textual/widgets/_button.py
+++ b/src/textual/widgets/_button.py
@@ -192,15 +192,20 @@ async def on_click(self, event: events.Click) -> None:
if self.disabled:
return
# Manage the "active" effect:
+ self._start_active_affect()
+ # ...and let other components know that we've just been clicked:
+ await self.emit(Button.Pressed(self))
+
+ def _start_active_affect(self) -> None:
+ """Start a small animation to show the button was clicked."""
self.add_class("-active")
self.set_timer(
self.ACTIVE_EFFECT_DURATION, partial(self.remove_class, "-active")
)
- # ...and let other components know that we've just been clicked:
- await self.emit(Button.Pressed(self))
async def on_key(self, event: events.Key) -> None:
if event.key == "enter" and not self.disabled:
+ self._start_active_affect()
await self.emit(Button.Pressed(self))
@classmethod
diff --git a/tests/css/test_help_text.py b/tests/css/test_help_text.py
index 1114961d80..928ca2d624 100644
--- a/tests/css/test_help_text.py
+++ b/tests/css/test_help_text.py
@@ -9,8 +9,6 @@
color_property_help_text,
border_property_help_text,
layout_property_help_text,
- docks_property_help_text,
- dock_property_help_text,
fractional_property_help_text,
offset_property_help_text,
align_help_text,
@@ -92,12 +90,6 @@ def test_layout_property_help_text(styling_context):
assert "layout" in rendered
-def test_docks_property_help_text(styling_context):
- rendered = render(docks_property_help_text("docks", styling_context))
- assert "Invalid value for" in rendered
- assert "docks" in rendered
-
-
def test_fractional_property_help_text(styling_context):
rendered = render(fractional_property_help_text("opacity", styling_context))
assert "Invalid value for" in rendered
diff --git a/tests/test_color.py b/tests/test_color.py
index 3f97196651..0e1e51ebbc 100644
--- a/tests/test_color.py
+++ b/tests/test_color.py
@@ -115,22 +115,28 @@ def test_color_parse(text, expected):
assert Color.parse(text) == expected
-@pytest.mark.parametrize("input,output", [
- ("rgb( 300, 300 , 300 )", Color(255, 255, 255)),
- ("rgba( 2 , 3 , 4, 1.0 )", Color(2, 3, 4, 1.0)),
- ("hsl( 45, 25% , 25% )", Color(80, 72, 48)),
- ("hsla( 45, 25% , 25%, 0.35 )", Color(80, 72, 48, 0.35)),
-])
+@pytest.mark.parametrize(
+ "input,output",
+ [
+ ("rgb( 300, 300 , 300 )", Color(255, 255, 255)),
+ ("rgba( 2 , 3 , 4, 1.0 )", Color(2, 3, 4, 1.0)),
+ ("hsl( 45, 25% , 25% )", Color(80, 72, 48)),
+ ("hsla( 45, 25% , 25%, 0.35 )", Color(80, 72, 48, 0.35)),
+ ],
+)
def test_color_parse_input_has_spaces(input, output):
assert Color.parse(input) == output
-@pytest.mark.parametrize("input,output", [
- ("rgb(300, 300, 300)", Color(255, 255, 255)),
- ("rgba(300, 300, 300, 300)", Color(255, 255, 255, 1.0)),
- ("hsl(400, 200%, 250%)", Color(255, 255, 255, 1.0)),
- ("hsla(400, 200%, 250%, 1.9)", Color(255, 255, 255, 1.0)),
-])
+@pytest.mark.parametrize(
+ "input,output",
+ [
+ ("rgb(300, 300, 300)", Color(255, 255, 255)),
+ ("rgba(300, 300, 300, 300)", Color(255, 255, 255, 1.0)),
+ ("hsl(400, 200%, 250%)", Color(255, 255, 255, 1.0)),
+ ("hsla(400, 200%, 250%, 1.9)", Color(255, 255, 255, 1.0)),
+ ],
+)
def test_color_parse_clamp(input, output):
assert Color.parse(input) == output
@@ -141,7 +147,8 @@ def test_color_parse_hsl_negative_degrees():
def test_color_parse_hsla_negative_degrees():
assert Color.parse("hsla(-45, 50%, 50%, 0.2)") == Color.parse(
- "hsla(315, 50%, 50%, 0.2)")
+ "hsla(315, 50%, 50%, 0.2)"
+ )
def test_color_parse_color():