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 bd3e92d9b..49502bd64 100644 --- a/sandbox/darren/just_a_box.css +++ b/sandbox/darren/just_a_box.css @@ -16,7 +16,14 @@ Screen { offset-x: 0; } -.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 094dc5c09..1fed887de 100644 --- a/sandbox/darren/just_a_box.py +++ b/sandbox/darren/just_a_box.py @@ -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): 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 d24835e93..2dc87dd1b 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): @@ -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. diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 4b1184127..f94b966b6 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -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 diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index cf096d98a..0025551d7 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 c4e952ed3..e8a25c514 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -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) 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 537932cfc..3dfd634c6 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -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) 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 86a46dc8e..ab0ea550f 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 7e3fe8d9e..ee2513ea9 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 @@ -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" 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