diff --git a/docs/examples/styles/background.py b/docs/examples/styles/background.py
new file mode 100644
index 000000000..792cf4c85
--- /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("Hello, World!", id="static1")
+ yield Static("Hello, World!", id="static2")
+ yield Static("Hello, World!", id="static3")
+
+
+app = BackgroundApp()
diff --git a/docs/examples/styles/border.py b/docs/examples/styles/border.py
new file mode 100644
index 000000000..958976939
--- /dev/null
+++ b/docs/examples/styles/border.py
@@ -0,0 +1,34 @@
+from textual.app import App
+from textual.widgets import Static
+
+
+class BorderApp(App):
+ CSS = """
+ Screen > Static {
+ height:5;
+ content-align: center middle;
+ color: white;
+ margin: 1;
+ box-sizing: border-box;
+ }
+ #static1 {
+ background: red 20%;
+ border: solid red;
+ }
+ #static2 {
+ background: green 20%;
+ border: dashed green;
+ }
+ #static3 {
+ background: blue 20%;
+ border: tall blue;
+ }
+ """
+
+ def compose(self):
+ yield Static("Hello, World!", id="static1")
+ yield Static("Hello, World!", id="static2")
+ yield Static("Hello, World!", id="static3")
+
+
+app = BorderApp()
diff --git a/docs/examples/styles/color.py b/docs/examples/styles/color.py
new file mode 100644
index 000000000..6d29d9a30
--- /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("Hello, World!", id="static1")
+ yield Static("Hello, World!", id="static2")
+ yield Static("Hello, World!", id="static3")
+
+
+app = ColorApp()
diff --git a/docs/examples/styles/display.py b/docs/examples/styles/display.py
index 5528b5b2f..cc5bd0021 100644
--- a/docs/examples/styles/display.py
+++ b/docs/examples/styles/display.py
@@ -1,24 +1,27 @@
from textual.app import App
-from textual.widget import Widget
+from textual.widgets import Static
-class WidthApp(App):
+class DisplayApp(App):
CSS = """
- Screen > Widget {
- height: 5;
- background: blue;
- color: white;
- border: heavy white;
+ Screen {
+ background: green;
}
- Widget.hidden {
+ Static {
+ height: 5;
+ background: white;
+ color: blue;
+ border: heavy blue;
+ }
+ Static.remove {
display: none;
}
"""
def compose(self):
- yield Widget(id="widget1")
- yield Widget(id="widget2", classes="hidden")
- yield Widget(id="widget3")
+ yield Static("Widget 1")
+ yield Static("widget 2", classes="remove")
+ yield Static("widget 3")
-app = WidthApp()
+app = DisplayApp()
diff --git a/docs/examples/styles/height.py b/docs/examples/styles/height.py
index c1863d5b2..f94baeeeb 100644
--- a/docs/examples/styles/height.py
+++ b/docs/examples/styles/height.py
@@ -2,7 +2,7 @@ from textual.app import App
from textual.widget import Widget
-class WidthApp(App):
+class HeightApp(App):
CSS = """
Screen > Widget {
background: green;
@@ -15,4 +15,4 @@ class WidthApp(App):
yield Widget()
-app = WidthApp()
+app = HeightApp()
diff --git a/docs/examples/styles/margin.py b/docs/examples/styles/margin.py
new file mode 100644
index 000000000..c7aec1dfe
--- /dev/null
+++ b/docs/examples/styles/margin.py
@@ -0,0 +1,27 @@
+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 = """
+
+ Static {
+ margin: 4 8;
+ background: blue 20%;
+ }
+
+ """
+
+ def compose(self):
+ yield Static(TEXT)
+
+
+app = MarginApp()
diff --git a/docs/examples/styles/padding.py b/docs/examples/styles/padding.py
new file mode 100644
index 000000000..8b345514b
--- /dev/null
+++ b/docs/examples/styles/padding.py
@@ -0,0 +1,27 @@
+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 = """
+
+ Static {
+ padding: 4 8;
+ background: blue 20%;
+ }
+
+ """
+
+ def compose(self):
+ yield Static(TEXT)
+
+
+app = PaddingApp()
diff --git a/docs/examples/styles/text_style.py b/docs/examples/styles/text_style.py
new file mode 100644
index 000000000..42f9710d2
--- /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/visibility.py b/docs/examples/styles/visibility.py
new file mode 100644
index 000000000..aeec9d228
--- /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/styles/background.md b/docs/styles/background.md
new file mode 100644
index 000000000..3530e0d80
--- /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.
+
+```python
+# Set blue background
+widget.styles.background = "blue"
+
+from textual.color import Color
+# Set with a color object
+widget.styles.background = Color.parse("pink")
+
+```
diff --git a/docs/styles/color.md b/docs/styles/color.md
new file mode 100644
index 000000000..51806d857
--- /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 background */
+color: blue;
+
+/* 20% red backround */
+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 background
+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/height.md b/docs/styles/height.md
index 4a877e131..12aefac09 100644
--- a/docs/styles/height.md
+++ b/docs/styles/height.md
@@ -1,6 +1,6 @@
# Height
-The `height` property sets a widget's height. 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 `height` style sets a widget's height. 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/docs/styles/margin.md b/docs/styles/margin.md
new file mode 100644
index 000000000..a0cc4d443
--- /dev/null
+++ b/docs/styles/margin.md
@@ -0,0 +1,34 @@
+# Margin
+
+The `margin` rule adds space around the entire widget.
+
+- `1` Sets a margin of 1 around all 4 edges
+- `1 2` Sets a margin of 1 on the top and bottom edges, and a margin of 2 on the left and right edges
+- `1 2 3 4` Sets a margin of one on the top edge, 2 on the right, 3 on the bottom, and 4 on the left.
+
+## 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/padding.md b/docs/styles/padding.md
new file mode 100644
index 000000000..0c8272fbc
--- /dev/null
+++ b/docs/styles/padding.md
@@ -0,0 +1,34 @@
+# Padding
+
+The padding rule adds space around the content of a widget. You can specify padding with 1, 2 or 4 numbers.
+
+- `1` Sets a padding of 1 around all 4 edges
+- `1 2` Sets a padding of 1 on the top and bottom edges, and a padding of two on the left and right edges
+- `1 2 3 4` Sets a padding of one on the top edge, 2 on the right, 3 on the bottom, and 4 on the left.
+
+## Example
+
+=== "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/text_style.md b/docs/styles/text_style.md
new file mode 100644
index 000000000..d5869f9bf
--- /dev/null
+++ b/docs/styles/text_style.md
@@ -0,0 +1,36 @@
+# 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:
+
+- `"bold"` Sets **bold text**
+- `"italic"` Sets _italic text_
+- `"reverse"` Sets reverse video text (foreground and background colors reversed)
+- `"underline"` Sets underline text
+- `"strike"` Sets strikethrough text
+
+Text styles may be set in combination. For example "bold underline" or "reverse underline strike".
+
+## Example
+
+=== "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/visibility.md b/docs/styles/visibility.md
new file mode 100644
index 000000000..9db7ba10b
--- /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 be removed from the screen.
+
+## 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 18cc7b57e..933f9257d 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` style 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 000000000..b3f8ece6d
--- /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 f7ddbf67e..a5298665c 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -11,9 +11,15 @@ nav:
- "events/mount.md"
- "events/resize.md"
- Styles:
+ - "styles/background.md"
+ - "styles/color.md"
- "styles/display.md"
- - "styles/width.md"
- "styles/height.md"
+ - "styles/margin.md"
+ - "styles/padding.md"
+ - "styles/text_style.md"
+ - "styles/visibility.md"
+ - "styles/width.md"
- Widgets: "/widgets/"
- Reference:
- "reference/app.md"
diff --git a/src/textual/constants.py b/src/textual/constants.py
new file mode 100644
index 000000000..6429da790
--- /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/layouts/horizontal.py b/src/textual/layouts/horizontal.py
index 251e93838..9660b9aec 100644
--- a/src/textual/layouts/horizontal.py
+++ b/src/textual/layouts/horizontal.py
@@ -29,7 +29,7 @@ class HorizontalLayout(Layout):
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/tests/test_color.py b/tests/test_color.py
index 3f9719665..0e1e51ebb 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():