Merge branch 'css' of github.com:Textualize/textual into delay-transition

This commit is contained in:
Darren Burns
2022-08-30 16:07:46 +01:00
32 changed files with 488 additions and 100 deletions

View File

@@ -0,0 +1,24 @@
#vertical-layout {
layout: vertical;
background: darkmagenta;
height: auto;
}
#horizontal-layout {
layout: horizontal;
background: darkcyan;
height: auto;
}
#center-layout {
layout: center;
background: darkslateblue;
height: 7;
}
Static {
margin: 1;
width: 12;
color: black;
background: yellowgreen;
}

View File

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

View File

@@ -0,0 +1,24 @@
#one {
text-align: left;
background: lightblue;
}
#two {
text-align: center;
background: indianred;
}
#three {
text-align: right;
background: palegreen;
}
#four {
text-align: justify;
background: palevioletred;
}
Static {
padding: 1;
}

View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from textual.app import App, ComposeResult
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."
)
class TextAlign(App):
def compose(self) -> ComposeResult:
left = Static("[b]Left aligned[/]\n" + TEXT, id="one")
yield left
right = Static("[b]Center aligned[/]\n" + TEXT, id="two")
yield right
center = Static("[b]Right aligned[/]\n" + TEXT, id="three")
yield center
full = Static("[b]Justified[/]\n" + TEXT, id="four")
yield full
app = TextAlign(css_path="text_align.css")

View File

@@ -5,7 +5,7 @@ The `background` rule sets the background color of the widget.
## Syntax
```
background: COLOR [PERCENTAGE]
background: <COLOR> [<PERCENTAGE>];
```
## Example

View File

@@ -2,8 +2,22 @@
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 |
| ------------ |---------------------------------------------------------|
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.
## Syntax
```
border: [<COLOR>] [<BORDER VALUE>];
border-top: [<COLOR>] [<BORDER VALUE>];
border-right: [<COLOR>] [<BORDER VALUE>];
border-bottom: [<COLOR>] [<BORDER VALUE>];
border-left: [<COLOR>] [<BORDER VALUE>];
```
### Values
| Border value | Description |
|--------------|---------------------------------------------------------|
| `"ascii"` | A border with plus, hyphen, and vertical bar |
| `"blank"` | A blank border (reserves space for a border) |
| `"dashed"` | Dashed line border |
@@ -20,19 +34,7 @@ The `border` rule enables the drawing of a box around a widget. A border is set
| `"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.
## Syntax
```
border: [<COLOR>] [<BORDER VALUE>];
border-top: [<COLOR>] [<BORDER VALUE>];
border-right: [<COLOR>] [<BORDER VALUE>];
border-bottom: [<COLOR>] [<BORDER VALUE>];
border-left: [<COLOR>] [<BORDER VALUE>];
```
For example, `heavy white` would display a heavy white line around a widget.
## Border command

View File

@@ -1,10 +1,6 @@
# 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.
The `box-sizing` property determines how the width and height of a widget are calculated.
## Syntax
@@ -12,9 +8,18 @@ You can set `box-sizing` to `content-box` which tells Textual that padding and b
box-sizing: [border-box|content-box];
```
### Values
| Values | Description |
|------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `border-box` (default) | Padding and border are included in the width and height. If you add padding and/or border the widget will not change in size, but you will have less space for content. |
| `content-box` | Padding and border will 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.
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"

View File

@@ -1,6 +1,6 @@
# 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.
The `display` property defines whether a widget is displayed or not.
## Syntax
@@ -8,6 +8,13 @@ The `display` property defines if a Widget is displayed or not. The default valu
display: [none|block];
```
### Values
| Value | Description |
|-------------------|---------------------------------------------------------------------------|
| `block` (default) | Display the widget as normal |
| `none` | The widget not be displayed, and space will no longer be reserved for it. |
## Example
Note that the second widget is hidden by adding the "hidden" class which sets the display style to None.

50
docs/styles/layout.md Normal file
View File

@@ -0,0 +1,50 @@
# Layout
The `layout` property defines how a widget arranges its children.
## Syntax
```
layout: [vertical|horizontal|center];
```
### Values
| Value | Description |
|----------------------|-------------------------------------------------------------------------------|
| `vertical` (default) | Child widgets will be arranged along the vertical axis, from top to bottom. |
| `horizontal` | Child widgets will be arranged along the horizontal axis, from left to right. |
| `center` | A single child widget will be placed in the center. |
## Example
Note how the `layout` property affects the arrangement of widgets in the example below.
=== "layout.py"
```python
--8<-- "docs/examples/styles/layout.py"
```
=== "layout.css"
```sass
--8<-- "docs/examples/styles/layout.css"
```
=== "Output"
```{.textual path="docs/examples/styles/layout.py"}
```
## CSS
```sass
layout: horizontal;
```
## Python
```python
widget.layout = "horizontal"
```

View File

@@ -2,8 +2,8 @@
The `margin` rule adds space around the entire widget. Margin may be specified with 1, 2 or 4 values.
| example | |
| ------------------ | ------------------------------------------------------------------- |
| Example | Description |
|--------------------|---------------------------------------------------------------------|
| `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 |

View File

@@ -1,11 +1,28 @@
# 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.
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 an 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.
An outline is set with a border value (see table below) followed by a color.
| Border value | Explanation |
| ------------ | ------------------------------------------------------- |
Outlines may also be set individually with the `outline-top`, `outline-right`, `outline-bottom` and `outline-left`
rules.
## Syntax
```
outline: [<COLOR>] [<BORDER VALUE>];
outline-top: [<COLOR>] [<BORDER VALUE>];
outline-right: [<COLOR>] [<BORDER VALUE>];
outline-bottom: [<COLOR>] [<BORDER VALUE>];
outline-left: [<COLOR>] [<BORDER VALUE>];
```
### Values
| Border value | Description |
|--------------|---------------------------------------------------------|
| `"ascii"` | A border with plus, hyphen, and vertical bar |
| `"blank"` | A blank border (reserves space for a border) |
| `"dashed"` | Dashed line border |
@@ -22,19 +39,7 @@ An outline is set with a border value (see below) followed by a color.
| `"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.
## Syntax
```
outline: [<COLOR>] [<BORDER VALUE>];
outline-top: [<COLOR>] [<BORDER VALUE>];
outline-right: [<COLOR>] [<BORDER VALUE>];
outline-bottom: [<COLOR>] [<BORDER VALUE>];
outline-left: [<COLOR>] [<BORDER VALUE>];
```
For example, `heavy white` would display a heavy white line around a widget.
## Example
@@ -61,10 +66,10 @@ This examples shows a widget with an outline. Note how the outline occludes the
```sass
/* Set a heavy white outline */
outline: heavy white;
outline:heavy white;
/* set a red outline on the left */
outline-left: outer red;
outline-left:outer red;
```
## Python
@@ -73,6 +78,6 @@ outline-left: outer red;
# Set a heavy white outline
widget.outline = ("heavy", "white)
# Set a red outline on the left
widget.outline_left = ("outer", "red)
# Set a red outline on the left
widget.outline_left = ("outer", "red)
```

View File

@@ -1,12 +1,7 @@
# 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 `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).
The default value for overflow is `"auto auto"` which will show scrollbars automatically for both scrollbars if content doesn't fit within container.
@@ -20,11 +15,20 @@ overflow-x: [auto|hidden|scroll];
overflow-y: [auto|hidden|scroll];
```
### Values
| Value | Description |
|------------------|---------------------------------------------------------|
| `auto` (default) | Automatically show the scrollbar if content doesn't fit |
| `hidden` | Never show the scrollbar |
| `scroll` | Always show the scrollbar |
## 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 shown.
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 shown.
=== "overflow.py"

View File

@@ -1,9 +1,10 @@
# 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.
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) |
@@ -12,7 +13,7 @@ There are a number of rules to set the colors used in Textual scrollbars. You wo
| `scrollbar-background-active` | Scrollbar background when the thumb is being dragged |
| `scrollbar-corner-color` | The gap between the horizontal and vertical scrollbars |
## Example:
## Syntax
```
scrollbar-color: <COLOR>;

57
docs/styles/text_align.md Normal file
View File

@@ -0,0 +1,57 @@
# Text-align
The `text-align` rule aligns text within a widget.
## Syntax
```
text-align: [left|start|center|right|end|justify];
```
### Values
| Value | Description |
|-----------|----------------------------------|
| `left` | Left aligns text in the widget |
| `start` | Left aligns text in the widget |
| `center` | Center aligns text in the widget |
| `right` | Right aligns text in the widget |
| `end` | Right aligns text in the widget |
| `justify` | Justifies text in the widget |
## Example
This example shows, from top to bottom: `left`, `center`, `right`, and `justify` text alignments.
=== "text_align.py"
```python
--8<-- "docs/examples/styles/text_align.py"
```
=== "text_align.css"
```css
--8<-- "docs/examples/styles/text_align.css"
```
=== "Output"
```{.textual path="docs/examples/styles/text_align.py"}
```
## CSS
```sass
/* Set text in all Widgets to be right aligned */
Widget {
text-align: right;
}
```
## Python
```python
# Set text in the widget to be right aligned
widget.styles.text_align = "right"
```

View File

@@ -1,23 +1,26 @@
# 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:
The `text-style` rule enables a number of different ways of displaying text.
| Style | Effect |
| ------------- | -------------------------------------------------------------- |
| `"bold"` | **bold text** |
| `"italic"` | _italic text_ |
| `"reverse"` | reverse video text (foreground and background colors reversed) |
| `"underline"` | <u>underline text</u> |
| `"strike"` | <s>strikethrough text</s> |
Text styles may be set in combination. For example "bold underline" or "reverse underline strike".
Text styles may be set in combination.
For example `bold underline` or `reverse underline strike`.
## Syntax
```
text-style: <TEXT STYLE> ...
text-style: <TEXT STYLE> ...;
```
### Values
| Value | Description |
|-------------|----------------------------------------------------------------|
| `bold` | **bold text** |
| `italic` | _italic text_ |
| `reverse` | reverse video text (foreground and background colors reversed) |
| `underline` | <u>underline text</u> |
| `strike` | <s>strikethrough text</s> |
## Example
Each of the three text panels has a different text style.

View File

@@ -1,13 +1,20 @@
# 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.
The `visibility` rule may be used to make a widget invisible while still reserving spacing for it.
## Syntax
```
visibility: [hidden|visible];
visibility: [visible|hidden];
```
### Values
| Value | Description |
|---------------------|----------------------------------------|
| `visible` (default) | The widget will be displayed as normal |
| `hidden` | The widget will be invisible |
## Example
Note that the second widget is hidden, while leaving a space where it would have been rendered.

View File

@@ -44,6 +44,7 @@ nav:
- "styles/content_align.md"
- "styles/display.md"
- "styles/height.md"
- "styles/layout.md"
- "styles/margin.md"
- "styles/max_height.md"
- "styles/max_width.md"
@@ -53,9 +54,10 @@ nav:
- "styles/outline.md"
- "styles/overflow.md"
- "styles/padding.md"
- "styles/scrollbar.md"
- "styles/scrollbar_gutter.md"
- "styles/scrollbar_size.md"
- "styles/scrollbar.md"
- "styles/text_align.md"
- "styles/text_style.md"
- "styles/tint.md"
- "styles/visibility.md"

View File

@@ -16,7 +16,14 @@ Screen {
offset-x: 0;
}
.box {
.box1 {
background: orangered;
height: 12;
width: 30;
}
.box2 {
background: blueviolet;
height: 6;
width: 12;
}

View File

@@ -24,8 +24,9 @@ class JustABox(App):
self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar")
def compose(self) -> ComposeResult:
self.box = Box()
self.box = Box(classes="box1")
yield self.box
yield Box(classes="box2")
yield Widget(id="sidebar")
def key_a(self):

View File

@@ -0,0 +1,33 @@
from __future__ import annotations
from textual.app import App, ComposeResult
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 TextAlign(App):
def compose(self) -> ComposeResult:
left = Static("[b]Left aligned[/]\n" + TEXT, id="one")
yield left
right = Static("[b]Center aligned[/]\n" + TEXT, id="two")
yield right
center = Static("[b]Right aligned[/]\n" + TEXT, id="three")
yield center
full = Static("[b]Fully justified[/]\n" + TEXT, id="four")
yield full
app = TextAlign(css_path="text_align.scss", watch_css=True)
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,24 @@
#one {
text-align: left;
background: lightblue;
}
#two {
text-align: center;
background: indianred;
}
#three {
text-align: right;
background: palegreen;
}
#four {
text-align: justify;
background: palevioletred;
}
Static {
padding: 1;
}

View File

@@ -9,10 +9,10 @@ from textual.css._help_renderables import Example, Bullet, HelpText
from textual.css.constants import (
VALID_BORDER,
VALID_LAYOUT,
VALID_EDGE,
VALID_ALIGN_HORIZONTAL,
VALID_ALIGN_VERTICAL,
VALID_STYLE_FLAGS,
VALID_TEXT_ALIGN,
)
if sys.version_info >= (3, 8):
@@ -648,6 +648,26 @@ def align_help_text() -> HelpText:
)
def text_align_help_text() -> HelpText:
"""Help text to show when the user supplies an invalid value for the text-align property
Returns:
HelpText: Renderable for displaying the help text for this property.
"""
return HelpText(
summary="Invalid value for the [i]text-align[/] property.",
bullets=[
Bullet(
f"The [i]text-align[/] property must be one of {friendly_list(VALID_TEXT_ALIGN)}",
examples=[
Example("text-align: center;"),
Example("text-align: right;"),
],
)
],
)
def offset_single_axis_help_text(property_name: str) -> HelpText:
"""Help text to show when the user supplies an invalid value for an offset-* property.

View File

@@ -24,6 +24,7 @@ from ._help_text import (
property_invalid_value_help_text,
scrollbar_size_property_help_text,
scrollbar_size_single_axis_help_text,
text_align_help_text,
)
from .constants import (
VALID_ALIGN_HORIZONTAL,
@@ -36,6 +37,7 @@ from .constants import (
VALID_VISIBILITY,
VALID_STYLE_FLAGS,
VALID_SCROLLBAR_GUTTER,
VALID_TEXT_ALIGN,
)
from .errors import DeclarationError, StyleValueError
from .model import Declaration
@@ -618,6 +620,20 @@ class StylesBuilder:
style_definition = " ".join(token.value for token in tokens)
self.styles.text_style = style_definition
def process_text_align(self, name: str, tokens: list[Token]) -> None:
"""Process a text-align declaration"""
if not tokens:
return
if len(tokens) > 1 or tokens[0].value not in VALID_TEXT_ALIGN:
self.error(
name,
tokens[0],
text_align_help_text(),
)
self.styles._rules["text_align"] = tokens[0].value
def process_dock(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return

View File

@@ -38,6 +38,7 @@ VALID_BOX_SIZING: Final = {"border-box", "content-box"}
VALID_OVERFLOW: Final = {"scroll", "hidden", "auto"}
VALID_ALIGN_HORIZONTAL: Final = {"left", "center", "right"}
VALID_ALIGN_VERTICAL: Final = {"top", "middle", "bottom"}
VALID_TEXT_ALIGN: Final = {"start", "end", "left", "right", "center", "justify"}
VALID_SCROLLBAR_GUTTER: Final = {"auto", "stable"}
VALID_STYLE_FLAGS: Final = {
"none",

View File

@@ -31,7 +31,6 @@ from ._style_properties import (
SpacingProperty,
StringEnumProperty,
StyleFlagsProperty,
StyleProperty,
TransitionsProperty,
)
from .constants import (
@@ -42,6 +41,7 @@ from .constants import (
VALID_OVERFLOW,
VALID_SCROLLBAR_GUTTER,
VALID_VISIBILITY,
VALID_TEXT_ALIGN,
)
from .scalar import Scalar, ScalarOffset, Unit
from .scalar_animation import ScalarAnimation
@@ -57,6 +57,7 @@ from .types import (
Specificity3,
Specificity6,
Visibility,
TextAlign,
)
if sys.version_info >= (3, 8):
@@ -143,6 +144,8 @@ class RulesMap(TypedDict, total=False):
content_align_horizontal: AlignHorizontal
content_align_vertical: AlignVertical
text_align: TextAlign
RULE_NAMES = list(RulesMap.__annotations__.keys())
RULE_NAMES_SET = frozenset(RULE_NAMES)
@@ -250,6 +253,8 @@ class StylesBase(ABC):
content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
content_align = AlignProperty()
text_align = StringEnumProperty(VALID_TEXT_ALIGN, "start")
def __eq__(self, styles: object) -> bool:
"""Check that Styles contains the same rules."""
if not isinstance(styles, StylesBase):
@@ -459,7 +464,6 @@ class StylesBase(ABC):
@rich.repr.auto
@dataclass
class Styles(StylesBase):
node: DOMNode | None = None
_rules: RulesMap = field(default_factory=dict)

View File

@@ -45,10 +45,6 @@ class TokenError(Exception):
def _get_snippet(self) -> Panel:
"""Get a short snippet of code around a given line number.
Args:
code (str): The code.
line_no (int): Line number.
Returns:
Panel: A renderable.
"""

View File

@@ -39,6 +39,7 @@ ScrollbarGutter = Literal["auto", "stable"]
BoxSizing = Literal["border-box", "content-box"]
Overflow = Literal["scroll", "hidden", "auto"]
EdgeStyle = Tuple[EdgeType, Color]
TextAlign = Literal["left", "start", "center", "right", "end", "justify"]
Specificity3 = Tuple[int, int, int]
Specificity4 = Tuple[int, int, int, int]

View File

@@ -619,7 +619,7 @@ class Region(NamedTuple):
"""Move the offset of the Region.
Args:
translate (tuple[int, int]): Offset to add to region.
offset (tuple[int, int]): Offset to add to region.
Returns:
Region: A new region shifted by (x, y)

View File

@@ -242,7 +242,7 @@ class MessagePump(metaclass=MessagePumpMeta):
name: str | None = None,
repeat: int = 0,
pause: bool = False,
):
) -> Timer:
"""Call a function at periodic intervals.
Args:

View File

@@ -1,14 +1,11 @@
from __future__ import annotations
from asyncio import Lock
from itertools import islice
from fractions import Fraction
from itertools import islice
from operator import attrgetter
from typing import (
TYPE_CHECKING,
Any,
Awaitable,
Callable,
ClassVar,
Collection,
Iterable,
@@ -16,8 +13,7 @@ from typing import (
)
import rich.repr
from rich.console import Console, RenderableType
from rich.console import Console, RenderableType, JustifyMethod
from rich.measure import Measurement
from rich.segment import Segment
from rich.style import Style
@@ -33,13 +29,13 @@ from ._segment_tools import align_lines
from ._styles_cache import StylesCache
from ._types import Lines
from .box_model import BoxModel, get_box_model
from .css.constants import VALID_TEXT_ALIGN
from .dom import DOMNode
from .dom import NoScreen
from .geometry import Offset, Region, Size, Spacing, clamp
from .layouts.vertical import VerticalLayout
from .message import Message
from .reactive import Reactive
from .dom import NoScreen
if TYPE_CHECKING:
from .app import App, ComposeResult
@@ -1215,11 +1211,15 @@ class Widget(DOMNode):
"""
if isinstance(renderable, str):
renderable = Text.from_markup(renderable)
justify = _get_rich_justify(self.styles.text_align)
renderable = Text.from_markup(renderable, justify=justify)
rich_style = self.rich_style
if isinstance(renderable, Text):
renderable.stylize(rich_style)
if not renderable.justify:
justify = _get_rich_justify(self.styles.text_align)
renderable.justify = justify
else:
renderable = Styled(renderable, rich_style)
@@ -1380,9 +1380,6 @@ class Widget(DOMNode):
def render(self) -> RenderableType:
"""Get renderable for widget.
Args:
style (Styles): The Styles object for this Widget.
Returns:
RenderableType: Any renderable
"""
@@ -1580,3 +1577,22 @@ class Widget(DOMNode):
self.scroll_page_up()
return True
return False
def _get_rich_justify(css_align: str) -> JustifyMethod:
"""Given the value for CSS text-align, return the analogous argument
for the Rich text `justify` parameter.
Args:
css_align: The value of text-align CSS property.
Returns:
JustifyMethod: The Rich JustifyMethod that corresponds to the text-align
value
"""
assert css_align in VALID_TEXT_ALIGN
return {
"start": "left",
"end": "right",
"justify": "full",
}.get(css_align, css_align)

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import pytest
from textual.color import Color
from textual.css.errors import UnresolvedVariableError
from textual.css.parse import substitute_references
@@ -1132,3 +1131,27 @@ class TestParsePadding:
stylesheet = Stylesheet()
stylesheet.add_source(css)
assert stylesheet.rules[0].styles.padding == Spacing(2, 3, -1, 1)
class TestParseTextAlign:
@pytest.mark.parametrize("valid_align", ["left", "start", "center", "right", "end", "justify"])
def test_text_align(self, valid_align):
css = f"#foo {{ text-align: {valid_align} }}"
stylesheet = Stylesheet()
stylesheet.add_source(css)
assert stylesheet.rules[0].styles.text_align == valid_align
def test_text_align_invalid(self):
css = "#foo { text-align: invalid-value; }"
stylesheet = Stylesheet()
with pytest.raises(StylesheetParseError):
stylesheet.add_source(css)
stylesheet.parse()
rules = stylesheet._parse_rules(css, "foo")
assert rules[0].errors
def test_text_align_empty_uses_default(self):
css = "#foo { text-align: ; }"
stylesheet = Stylesheet()
stylesheet.add_source(css)
assert stylesheet.rules[0].styles.text_align == "start"

View File

@@ -25,4 +25,4 @@ def test_auto_refresh():
elapsed = app.run(quit_after=1, headless=True)
assert elapsed is not None
# CI can run slower, so we need to give this a bit of margin
assert elapsed >= 0.3 and elapsed < 0.6
assert 0.2 <= elapsed < 0.8