diff --git a/docs/examples/styles/layout.css b/docs/examples/styles/layout.css new file mode 100644 index 000000000..1b8cacd13 --- /dev/null +++ b/docs/examples/styles/layout.css @@ -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; +} diff --git a/docs/examples/styles/layout.py b/docs/examples/styles/layout.py new file mode 100644 index 000000000..be91681f6 --- /dev/null +++ b/docs/examples/styles/layout.py @@ -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") diff --git a/docs/examples/styles/text_align.css b/docs/examples/styles/text_align.css new file mode 100644 index 000000000..c594254d6 --- /dev/null +++ b/docs/examples/styles/text_align.css @@ -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; +} diff --git a/docs/examples/styles/text_align.py b/docs/examples/styles/text_align.py new file mode 100644 index 000000000..27e2892fa --- /dev/null +++ b/docs/examples/styles/text_align.py @@ -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") diff --git a/docs/styles/background.md b/docs/styles/background.md index 38a1c59c8..1cf5283c7 100644 --- a/docs/styles/background.md +++ b/docs/styles/background.md @@ -5,7 +5,7 @@ The `background` rule sets the background color of the widget. ## Syntax ``` -background: COLOR [PERCENTAGE] +background: []; ``` ## Example diff --git a/docs/styles/border.md b/docs/styles/border.md index c060bfb25..757ce988b 100644 --- a/docs/styles/border.md +++ b/docs/styles/border.md @@ -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: [] []; +border-top: [] []; +border-right: [] []; +border-bottom: [] []; +border-left: [] []; +``` + +### 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: [] []; -border-top: [] []; -border-right: [] []; -border-bottom: [] []; -border-left: [] []; -``` +For example, `heavy white` would display a heavy white line around a widget. ## Border command diff --git a/docs/styles/box_sizing.md b/docs/styles/box_sizing.md index 549dcb395..bc089ad44 100644 --- a/docs/styles/box_sizing.md +++ b/docs/styles/box_sizing.md @@ -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" diff --git a/docs/styles/display.md b/docs/styles/display.md index de29cffd0..962838939 100644 --- a/docs/styles/display.md +++ b/docs/styles/display.md @@ -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. diff --git a/docs/styles/layout.md b/docs/styles/layout.md new file mode 100644 index 000000000..65fdf7998 --- /dev/null +++ b/docs/styles/layout.md @@ -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" +``` diff --git a/docs/styles/margin.md b/docs/styles/margin.md index 8f34b0fc6..21cab8c61 100644 --- a/docs/styles/margin.md +++ b/docs/styles/margin.md @@ -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 | diff --git a/docs/styles/outline.md b/docs/styles/outline.md index e77574b15..2267fedb7 100644 --- a/docs/styles/outline.md +++ b/docs/styles/outline.md @@ -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: [] []; +outline-top: [] []; +outline-right: [] []; +outline-bottom: [] []; +outline-left: [] []; +``` + +### 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: [] []; -outline-top: [] []; -outline-right: [] []; -outline-bottom: [] []; -outline-left: [] []; -``` +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) ``` diff --git a/docs/styles/overflow.md b/docs/styles/overflow.md index af109c3b0..f850281a1 100644 --- a/docs/styles/overflow.md +++ b/docs/styles/overflow.md @@ -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" diff --git a/docs/styles/scrollbar.md b/docs/styles/scrollbar.md index 57b4346d7..3a4ec1f33 100644 --- a/docs/styles/scrollbar.md +++ b/docs/styles/scrollbar.md @@ -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: ; diff --git a/docs/styles/text_align.md b/docs/styles/text_align.md new file mode 100644 index 000000000..1fc9b515c --- /dev/null +++ b/docs/styles/text_align.md @@ -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" +``` diff --git a/docs/styles/text_style.md b/docs/styles/text_style.md index 936220b3c..b54a1f2aa 100644 --- a/docs/styles/text_style.md +++ b/docs/styles/text_style.md @@ -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"` | underline text | -| `"strike"` | strikethrough text | - -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: ...; ``` +### Values + +| Value | Description | +|-------------|----------------------------------------------------------------| +| `bold` | **bold text** | +| `italic` | _italic text_ | +| `reverse` | reverse video text (foreground and background colors reversed) | +| `underline` | underline text | +| `strike` | strikethrough text | + ## Example Each of the three text panels has a different text style. diff --git a/docs/styles/visibility.md b/docs/styles/visibility.md index ffacd4956..d864487b0 100644 --- a/docs/styles/visibility.md +++ b/docs/styles/visibility.md @@ -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. diff --git a/mkdocs.yml b/mkdocs.yml index 5d17dd154..3f9af74bb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,10 +6,10 @@ nav: - "getting_started.md" - "introduction.md" - Guide: - - "guide/devtools.md" + - "guide/devtools.md" - "guide/CSS.md" - "guide/events.md" - + - "actions.md" - Events: - "events/blur.md" @@ -35,7 +35,7 @@ nav: - "events/resize.md" - "events/screen_resume.md" - "events/screen_suspend.md" - - "events/show.md" + - "events/show.md" - Styles: - "styles/background.md" - "styles/border.md" @@ -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" @@ -81,7 +83,7 @@ markdown_extensions: - admonition - def_list - meta - + - toc: permalink: true baselevel: 1 diff --git a/sandbox/darren/just_a_box.css b/sandbox/darren/just_a_box.css index 3a09b9c02..cd30ae532 100644 --- a/sandbox/darren/just_a_box.css +++ b/sandbox/darren/just_a_box.css @@ -2,23 +2,14 @@ Screen { background: lightcoral; } -#left_pane { - background: red; - width: 20 - overflow: scroll scroll; -} - -#middle_pane { - background: green; - width: 140; -} - -#right_pane { - background: blue; - width: 30; -} - -.box { +.box1 { + background: orangered; height: 12; width: 30; } + +.box2 { + background: blueviolet; + height: 6; + width: 12; +} diff --git a/sandbox/darren/just_a_box.py b/sandbox/darren/just_a_box.py index ee8be631e..24d2715f4 100644 --- a/sandbox/darren/just_a_box.py +++ b/sandbox/darren/just_a_box.py @@ -1,7 +1,5 @@ from __future__ import annotations -import asyncio - from rich.console import RenderableType from textual import events @@ -23,17 +21,20 @@ class Box(Widget, can_focus=True): class JustABox(App): def compose(self) -> ComposeResult: - self.box = Box() + self.box = Box(classes="box1") yield self.box + yield Box(classes="box2") def key_a(self): - self.animator.animate( - self.box.styles, - "opacity", - value=0.0, - duration=2.0, - on_complete=self.box.remove, - ) + self.box.styles.display = "none" + # self.box.styles.visibility = "hidden" + # self.animator.animate( + # self.box.styles, + # "opacity", + # value=0.0, + # duration=2.0, + # on_complete=self.box.remove, + # ) async def on_key(self, event: events.Key) -> None: await self.dispatch_key(event) diff --git a/sandbox/darren/text_align.py b/sandbox/darren/text_align.py new file mode 100644 index 000000000..ccc55696a --- /dev/null +++ b/sandbox/darren/text_align.py @@ -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() diff --git a/sandbox/darren/text_align.scss b/sandbox/darren/text_align.scss new file mode 100644 index 000000000..c594254d6 --- /dev/null +++ b/sandbox/darren/text_align.scss @@ -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; +} diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py index 6339f2c33..e148426ed 100644 --- a/src/textual/css/_help_text.py +++ b/src/textual/css/_help_text.py @@ -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): @@ -670,6 +670,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. diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index aeb0cb7f7..2af067c5a 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -26,6 +26,7 @@ from ._help_text import ( string_enum_help_text, style_flags_property_help_text, table_rows_or_columns_help_text, + text_align_help_text, ) from .constants import ( VALID_ALIGN_HORIZONTAL, @@ -38,6 +39,7 @@ from .constants import ( VALID_VISIBILITY, VALID_STYLE_FLAGS, VALID_SCROLLBAR_GUTTER, + VALID_TEXT_ALIGN, ) from .errors import DeclarationError, StyleValueError from .model import Declaration @@ -620,6 +622,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 diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 502c43faa..124ecb9bc 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -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", diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 7d2a7e179..2fe837e9e 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -42,6 +42,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 +58,7 @@ from .types import ( Specificity3, Specificity6, Visibility, + TextAlign, ) if sys.version_info >= (3, 8): @@ -153,6 +155,8 @@ class RulesMap(TypedDict, total=False): row_span: int column_span: int + text_align: TextAlign + RULE_NAMES = list(RulesMap.__annotations__.keys()) RULE_NAMES_SET = frozenset(RULE_NAMES) @@ -271,6 +275,8 @@ class StylesBase(ABC): row_span = IntegerProperty(default=1, layout=True) column_span = IntegerProperty(default=1, layout=True) + 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): @@ -480,7 +486,6 @@ class StylesBase(ABC): @rich.repr.auto @dataclass class Styles(StylesBase): - node: DOMNode | None = None _rules: RulesMap = field(default_factory=dict) diff --git a/src/textual/css/tokenizer.py b/src/textual/css/tokenizer.py index cf090866c..c7fb9183b 100644 --- a/src/textual/css/tokenizer.py +++ b/src/textual/css/tokenizer.py @@ -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. """ diff --git a/src/textual/css/types.py b/src/textual/css/types.py index d969397f0..6fb0929a4 100644 --- a/src/textual/css/types.py +++ b/src/textual/css/types.py @@ -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] diff --git a/src/textual/geometry.py b/src/textual/geometry.py index 559f3792a..7812431be 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -632,7 +632,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) diff --git a/src/textual/message_pump.py b/src/textual/message_pump.py index 7f8d0bb91..e7eeb4c18 100644 --- a/src/textual/message_pump.py +++ b/src/textual/message_pump.py @@ -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: diff --git a/src/textual/widget.py b/src/textual/widget.py index 96ab3ebed..0a0d357ac 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -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) diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py index 5b11472cf..d22c8fe46 100644 --- a/tests/css/test_parse.py +++ b/tests/css/test_parse.py @@ -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 @@ -1131,3 +1130,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" diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py index 5d211224c..d5122a7a4 100644 --- a/tests/test_auto_refresh.py +++ b/tests/test_auto_refresh.py @@ -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