From 0db5fe47e0735e9d66d36f3f6187fa3860a5c61e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 25 Aug 2022 10:53:44 +0100 Subject: [PATCH 01/14] Text justify initial work, help text --- sandbox/darren/text_justify.py | 43 ++++++++++++++++++++++++++++++ sandbox/darren/text_justify.scss | 0 src/textual/css/_help_text.py | 34 +++++++++++++++++++++++ src/textual/css/_styles_builder.py | 15 +++++++++++ src/textual/css/constants.py | 1 + src/textual/css/styles.py | 7 ++++- src/textual/css/types.py | 1 + src/textual/widget.py | 3 ++- 8 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 sandbox/darren/text_justify.py create mode 100644 sandbox/darren/text_justify.scss diff --git a/sandbox/darren/text_justify.py b/sandbox/darren/text_justify.py new file mode 100644 index 000000000..aa3419631 --- /dev/null +++ b/sandbox/darren/text_justify.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from rich.text import Text + +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 TextJustify(App): + def compose(self) -> ComposeResult: + left = Static(Text(TEXT)) + left.styles.text_justify = "left" + yield left + + right = Static(TEXT) + right.styles.text_justify = "right" + yield right + + center = Static(TEXT) + center.styles.text_justify = "center" + yield center + + full = Static(TEXT) + full.styles.text_justify = "full" + yield full + + +app = TextJustify(css_path="text_justify.scss", watch_css=True) + +if __name__ == "__main__": + from rich.console import Console + + console = Console() + text = Text(TEXT) + console.print(TEXT, justify="full") diff --git a/sandbox/darren/text_justify.scss b/sandbox/darren/text_justify.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py index d24835e93..e607741cb 100644 --- a/src/textual/css/_help_text.py +++ b/src/textual/css/_help_text.py @@ -13,6 +13,7 @@ from textual.css.constants import ( VALID_ALIGN_HORIZONTAL, VALID_ALIGN_VERTICAL, VALID_STYLE_FLAGS, + VALID_JUSTIFY, ) if sys.version_info >= (3, 8): @@ -648,6 +649,39 @@ def align_help_text() -> HelpText: ) +def text_justify_help_text(context: str) -> HelpText: + """Help text to show when the user supplies an invalid value for the text-justify property + + Returns: + HelpText: Renderable for displaying the help text for this property. + """ + return HelpText( + summary="Invalid value for the [i]text-justify[/] property.", + bullets=[ + *ContextSpecificBullets( + css=[ + Bullet( + f"The [i]text-justify[/] property must be one of {friendly_list(VALID_JUSTIFY)}", + examples=[ + Example("text-justify: center;"), + Example("text-justify: right;"), + ], + ) + ], + inline=[ + Bullet( + f"The [i]text_justify[/] property must be one of {friendly_list(VALID_JUSTIFY)}", + examples=[ + Example("widget.styles.text_justify = 'center'"), + Example("widget.styles.text_justify = 'right'"), + ], + ) + ], + ).get_by_context(context) + ], + ) + + 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..806a84f48 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_justify_help_text, ) from .constants import ( VALID_ALIGN_HORIZONTAL, @@ -36,6 +37,7 @@ from .constants import ( VALID_VISIBILITY, VALID_STYLE_FLAGS, VALID_SCROLLBAR_GUTTER, + VALID_JUSTIFY, ) from .errors import DeclarationError, StyleValueError from .model import Declaration @@ -618,6 +620,19 @@ class StylesBuilder: style_definition = " ".join(token.value for token in tokens) self.styles.text_style = style_definition + def process_text_justify(self, name: str, tokens: list[Token]) -> None: + if not tokens: + return + + if len(tokens) > 1 or tokens[0].value not in VALID_JUSTIFY: + self.error( + name, + tokens[0], + text_justify_help_text("css"), + ) + + self.styles._rules["text_justify"] = 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..0f9b25e54 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_JUSTIFY: Final = {"left", "center", "right", "full"} 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 dd0b53e6f..7b49c45ed 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -41,6 +41,7 @@ from .constants import ( VALID_OVERFLOW, VALID_SCROLLBAR_GUTTER, VALID_VISIBILITY, + VALID_JUSTIFY, ) from .scalar import Scalar, ScalarOffset, Unit from .scalar_animation import ScalarAnimation @@ -56,6 +57,7 @@ from .types import ( Specificity3, Specificity6, Visibility, + TextJustify, ) if sys.version_info >= (3, 8): @@ -142,6 +144,8 @@ class RulesMap(TypedDict, total=False): content_align_horizontal: AlignHorizontal content_align_vertical: AlignVertical + text_justify: TextJustify + RULE_NAMES = list(RulesMap.__annotations__.keys()) RULE_NAMES_SET = frozenset(RULE_NAMES) @@ -249,6 +253,8 @@ class StylesBase(ABC): content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") content_align = AlignProperty() + text_justify = StringEnumProperty(VALID_JUSTIFY, "left") + def __eq__(self, styles: object) -> bool: """Check that Styles contains the same rules.""" if not isinstance(styles, StylesBase): @@ -458,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/types.py b/src/textual/css/types.py index d969397f0..7b06e7561 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] +TextJustify = Literal["left", "center", "right", "full"] Specificity3 = Tuple[int, int, int] Specificity4 = Tuple[int, int, int, int] diff --git a/src/textual/widget.py b/src/textual/widget.py index 92ef7018b..a8f582260 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -982,11 +982,12 @@ class Widget(DOMNode): """ if isinstance(renderable, str): - renderable = Text.from_markup(renderable) + renderable = Text.from_markup(renderable, justify=self.styles.text_justify) rich_style = self.rich_style if isinstance(renderable, Text): renderable.stylize(rich_style) + renderable.justify = self.styles.text_justify else: renderable = Styled(renderable, rich_style) From 27c07dc7f031f9caad5c88898c419021849a90ee Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 25 Aug 2022 17:13:49 +0100 Subject: [PATCH 02/14] Only apply justify CSS if Text object has no justify set --- src/textual/widget.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index a8f582260..61bf2cc7e 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -987,7 +987,8 @@ class Widget(DOMNode): rich_style = self.rich_style if isinstance(renderable, Text): renderable.stylize(rich_style) - renderable.justify = self.styles.text_justify + if not renderable.justify: + renderable.justify = self.styles.text_justify else: renderable = Styled(renderable, rich_style) From 9eb5a17df7e6d3552ef2320be03dbe8448a02db2 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 25 Aug 2022 17:16:44 +0100 Subject: [PATCH 03/14] Update sandbox example to use text-justify CSS property --- sandbox/darren/text_justify.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sandbox/darren/text_justify.py b/sandbox/darren/text_justify.py index aa3419631..e76c4ab0e 100644 --- a/sandbox/darren/text_justify.py +++ b/sandbox/darren/text_justify.py @@ -36,8 +36,4 @@ class TextJustify(App): app = TextJustify(css_path="text_justify.scss", watch_css=True) if __name__ == "__main__": - from rich.console import Console - - console = Console() - text = Text(TEXT) - console.print(TEXT, justify="full") + app.run() From d64466c6ceeff9e472edabf8191ae9262033c7b1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 10:10:46 +0100 Subject: [PATCH 04/14] Add a docstring --- src/textual/css/_styles_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 806a84f48..c6b276528 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -621,6 +621,7 @@ class StylesBuilder: self.styles.text_style = style_definition def process_text_justify(self, name: str, tokens: list[Token]) -> None: + """Process a text-justify declaration""" if not tokens: return From a45bf58f7e81ed7361d4a4b34f356877da5a2ad1 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 11:34:50 +0100 Subject: [PATCH 05/14] Adding tests for text-justify CSS property --- tests/css/test_parse.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py index 5b11472cf..70beb4392 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,21 @@ class TestParsePadding: stylesheet = Stylesheet() stylesheet.add_source(css) assert stylesheet.rules[0].styles.padding == Spacing(2, 3, -1, 1) + + +class TestParseTextJustify: + @pytest.mark.parametrize("valid_justify", ["left", "center", "full", "right"]) + def test_text_justify(self, valid_justify): + css = f"#foo {{ text-justify: {valid_justify} }}" + stylesheet = Stylesheet() + stylesheet.add_source(css) + assert stylesheet.rules[0].styles.text_justify == valid_justify + + def test_text_justify_invalid(self): + css = "#foo { text-justify: invalid-value }" + stylesheet = Stylesheet() + with pytest.raises(StylesheetParseError): + stylesheet.add_source(css) + stylesheet.parse() + rules = stylesheet._parse_rules(css, "foo") + assert rules[0].errors From 57620fee8c1101822552ac7dba7d14abf48bc1ec Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 12:28:08 +0100 Subject: [PATCH 06/14] Test that text-justify property with empty value uses default --- tests/css/test_parse.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py index 70beb4392..0d340e1b7 100644 --- a/tests/css/test_parse.py +++ b/tests/css/test_parse.py @@ -1141,10 +1141,16 @@ class TestParseTextJustify: assert stylesheet.rules[0].styles.text_justify == valid_justify def test_text_justify_invalid(self): - css = "#foo { text-justify: invalid-value }" + css = "#foo { text-justify: 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_justify_empty_uses_default(self): + css = "#foo { text-justify: ; }" + stylesheet = Stylesheet() + stylesheet.add_source(css) + assert stylesheet.rules[0].styles.text_justify == "left" From acd56c6f7f470b337420d510822fb33f27284841 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 12:38:53 +0100 Subject: [PATCH 07/14] Initial docs for text-justify CSS property --- docs/styles/text_justify.md | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 docs/styles/text_justify.md diff --git a/docs/styles/text_justify.md b/docs/styles/text_justify.md new file mode 100644 index 000000000..b0cfcdf25 --- /dev/null +++ b/docs/styles/text_justify.md @@ -0,0 +1,40 @@ +# Text-justify + +The `text-justify` rule justifies text within a widget. + +## Syntax + +``` +text-justify: [left|center|right|full]; +``` + +### Values + +| Value | Description | +|----------|-------------------------------------| +| `left` | Left justifies text in the widget | +| `center` | Center justifies text in the widget | +| `right` | Right justifies text in the widget | +| `full` | Fully justifies text in the widget | + +## Example + +In this example, we can see, from top to bottom, + `left`, `center`, `right`, and `full` justified text respectively. + +=== "text_justify.py" + + ```python + --8<-- "docs/examples/styles/text_justify.py" + ``` + +=== "text_justify.css" + + ```css + --8<-- "docs/examples/styles/text_justify.css" + ``` + +=== "Output" + + ```{.textual path="docs/examples/styles/text_justify.py"} + ``` From 7bfdd11527713cac51dbb1904ee3685c1f89eabf Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 12:41:02 +0100 Subject: [PATCH 08/14] Expand auto-refresh margin in test --- tests/test_auto_refresh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py index 5d211224c..4314f0729 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.3 <= elapsed < 0.8 From 3b155216f0bb167de47bc0b4e5cd6ee0224c60ec Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 14:51:27 +0100 Subject: [PATCH 09/14] Docs for text-justify --- docs/examples/styles/text_justify.css | 24 +++++++++++++++++++++++ docs/examples/styles/text_justify.py | 28 +++++++++++++++++++++++++++ docs/styles/scrollbar.md | 2 +- docs/styles/text_justify.md | 21 ++++++++++++++++++-- mkdocs.yml | 11 ++++++----- sandbox/darren/text_justify.py | 28 +++++++++++---------------- sandbox/darren/text_justify.scss | 24 +++++++++++++++++++++++ src/textual/geometry.py | 2 +- src/textual/message_pump.py | 2 +- src/textual/widget.py | 3 --- 10 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 docs/examples/styles/text_justify.css create mode 100644 docs/examples/styles/text_justify.py diff --git a/docs/examples/styles/text_justify.css b/docs/examples/styles/text_justify.css new file mode 100644 index 000000000..589f7bfd6 --- /dev/null +++ b/docs/examples/styles/text_justify.css @@ -0,0 +1,24 @@ +#one { + text-justify: left; + background: lightblue; + +} + +#two { + text-justify: center; + background: indianred; +} + +#three { + text-justify: right; + background: palegreen; +} + +#four { + text-justify: full; + background: palevioletred; +} + +Static { + padding: 1; +} diff --git a/docs/examples/styles/text_justify.py b/docs/examples/styles/text_justify.py new file mode 100644 index 000000000..cc86529f8 --- /dev/null +++ b/docs/examples/styles/text_justify.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 TextJustify(App): + def compose(self) -> ComposeResult: + left = Static("[b]Left justified[/]\n" + TEXT, id="one") + yield left + + right = Static("[b]Center justified[/]\n" + TEXT, id="two") + yield right + + center = Static("[b]Right justified[/]\n" + TEXT, id="three") + yield center + + full = Static("[b]Full justified[/]\n" + TEXT, id="four") + yield full + + +app = TextJustify(css_path="text_justify.css") diff --git a/docs/styles/scrollbar.md b/docs/styles/scrollbar.md index 57b4346d7..85003779d 100644 --- a/docs/styles/scrollbar.md +++ b/docs/styles/scrollbar.md @@ -12,7 +12,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_justify.md b/docs/styles/text_justify.md index b0cfcdf25..ab829a75f 100644 --- a/docs/styles/text_justify.md +++ b/docs/styles/text_justify.md @@ -2,6 +2,8 @@ The `text-justify` rule justifies text within a widget. +This property is not the same as the `text-justify` property in browser CSS. + ## Syntax ``` @@ -19,8 +21,7 @@ text-justify: [left|center|right|full]; ## Example -In this example, we can see, from top to bottom, - `left`, `center`, `right`, and `full` justified text respectively. +This example shows, from top to bottom: `left`, `center`, `right`, and `full` justified text. === "text_justify.py" @@ -38,3 +39,19 @@ In this example, we can see, from top to bottom, ```{.textual path="docs/examples/styles/text_justify.py"} ``` + +## CSS + +```sass +/* Set text in all Widgets to be right justified */ +Widget { + text-justify: right; +} +``` + +## Python + +```python +# Set text in the widget to be right justified +widget.styles.text_justify = "right" +``` diff --git a/mkdocs.yml b/mkdocs.yml index 5d17dd154..5735b0ab5 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" @@ -53,9 +53,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_justify.md" - "styles/text_style.md" - "styles/tint.md" - "styles/visibility.md" @@ -81,7 +82,7 @@ markdown_extensions: - admonition - def_list - meta - + - toc: permalink: true baselevel: 1 diff --git a/sandbox/darren/text_justify.py b/sandbox/darren/text_justify.py index e76c4ab0e..74f8afdfe 100644 --- a/sandbox/darren/text_justify.py +++ b/sandbox/darren/text_justify.py @@ -1,35 +1,29 @@ from __future__ import annotations -from rich.text import Text - 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.""" +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 TextJustify(App): def compose(self) -> ComposeResult: - left = Static(Text(TEXT)) - left.styles.text_justify = "left" + left = Static("[b]Left justified[/]\n" + TEXT, id="one") yield left - right = Static(TEXT) - right.styles.text_justify = "right" + right = Static("[b]Center justified[/]\n" + TEXT, id="two") yield right - center = Static(TEXT) - center.styles.text_justify = "center" + center = Static("[b]Right justified[/]\n" + TEXT, id="three") yield center - full = Static(TEXT) - full.styles.text_justify = "full" + full = Static("[b]Full justified[/]\n" + TEXT, id="four") yield full diff --git a/sandbox/darren/text_justify.scss b/sandbox/darren/text_justify.scss index e69de29bb..589f7bfd6 100644 --- a/sandbox/darren/text_justify.scss +++ b/sandbox/darren/text_justify.scss @@ -0,0 +1,24 @@ +#one { + text-justify: left; + background: lightblue; + +} + +#two { + text-justify: center; + background: indianred; +} + +#three { + text-justify: right; + background: palegreen; +} + +#four { + text-justify: full; + background: palevioletred; +} + +Static { + padding: 1; +} 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 806f89cf1..d1c31761f 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -1382,9 +1382,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 """ From ad803a7753d7ad9748bef2ae72700e36740dec59 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 15:33:42 +0100 Subject: [PATCH 10/14] Text justify has become text align --- .../{text_justify.css => text_align.css} | 8 +-- .../styles/{text_justify.py => text_align.py} | 12 ++-- docs/styles/text_align.md | 57 +++++++++++++++++++ docs/styles/text_justify.md | 57 ------------------- mkdocs.yml | 2 +- .../darren/{text_justify.py => text_align.py} | 12 ++-- .../{text_justify.scss => text_align.scss} | 8 +-- src/textual/css/_help_text.py | 20 +++---- src/textual/css/_styles_builder.py | 14 ++--- src/textual/css/constants.py | 2 +- src/textual/css/styles.py | 7 +-- src/textual/widget.py | 37 ++++++++---- tests/css/test_parse.py | 16 +++--- 13 files changed, 134 insertions(+), 118 deletions(-) rename docs/examples/styles/{text_justify.css => text_align.css} (65%) rename docs/examples/styles/{text_justify.py => text_align.py} (56%) create mode 100644 docs/styles/text_align.md delete mode 100644 docs/styles/text_justify.md rename sandbox/darren/{text_justify.py => text_align.py} (64%) rename sandbox/darren/{text_justify.scss => text_align.scss} (65%) diff --git a/docs/examples/styles/text_justify.css b/docs/examples/styles/text_align.css similarity index 65% rename from docs/examples/styles/text_justify.css rename to docs/examples/styles/text_align.css index 589f7bfd6..c594254d6 100644 --- a/docs/examples/styles/text_justify.css +++ b/docs/examples/styles/text_align.css @@ -1,21 +1,21 @@ #one { - text-justify: left; + text-align: left; background: lightblue; } #two { - text-justify: center; + text-align: center; background: indianred; } #three { - text-justify: right; + text-align: right; background: palegreen; } #four { - text-justify: full; + text-align: justify; background: palevioletred; } diff --git a/docs/examples/styles/text_justify.py b/docs/examples/styles/text_align.py similarity index 56% rename from docs/examples/styles/text_justify.py rename to docs/examples/styles/text_align.py index cc86529f8..27e2892fa 100644 --- a/docs/examples/styles/text_justify.py +++ b/docs/examples/styles/text_align.py @@ -10,19 +10,19 @@ TEXT = ( ) -class TextJustify(App): +class TextAlign(App): def compose(self) -> ComposeResult: - left = Static("[b]Left justified[/]\n" + TEXT, id="one") + left = Static("[b]Left aligned[/]\n" + TEXT, id="one") yield left - right = Static("[b]Center justified[/]\n" + TEXT, id="two") + right = Static("[b]Center aligned[/]\n" + TEXT, id="two") yield right - center = Static("[b]Right justified[/]\n" + TEXT, id="three") + center = Static("[b]Right aligned[/]\n" + TEXT, id="three") yield center - full = Static("[b]Full justified[/]\n" + TEXT, id="four") + full = Static("[b]Justified[/]\n" + TEXT, id="four") yield full -app = TextJustify(css_path="text_justify.css") +app = TextAlign(css_path="text_align.css") 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_justify.md b/docs/styles/text_justify.md deleted file mode 100644 index ab829a75f..000000000 --- a/docs/styles/text_justify.md +++ /dev/null @@ -1,57 +0,0 @@ -# Text-justify - -The `text-justify` rule justifies text within a widget. - -This property is not the same as the `text-justify` property in browser CSS. - -## Syntax - -``` -text-justify: [left|center|right|full]; -``` - -### Values - -| Value | Description | -|----------|-------------------------------------| -| `left` | Left justifies text in the widget | -| `center` | Center justifies text in the widget | -| `right` | Right justifies text in the widget | -| `full` | Fully justifies text in the widget | - -## Example - -This example shows, from top to bottom: `left`, `center`, `right`, and `full` justified text. - -=== "text_justify.py" - - ```python - --8<-- "docs/examples/styles/text_justify.py" - ``` - -=== "text_justify.css" - - ```css - --8<-- "docs/examples/styles/text_justify.css" - ``` - -=== "Output" - - ```{.textual path="docs/examples/styles/text_justify.py"} - ``` - -## CSS - -```sass -/* Set text in all Widgets to be right justified */ -Widget { - text-justify: right; -} -``` - -## Python - -```python -# Set text in the widget to be right justified -widget.styles.text_justify = "right" -``` diff --git a/mkdocs.yml b/mkdocs.yml index 5735b0ab5..070d93a3c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,7 +56,7 @@ nav: - "styles/scrollbar.md" - "styles/scrollbar_gutter.md" - "styles/scrollbar_size.md" - - "styles/text_justify.md" + - "styles/text_align.md" - "styles/text_style.md" - "styles/tint.md" - "styles/visibility.md" diff --git a/sandbox/darren/text_justify.py b/sandbox/darren/text_align.py similarity index 64% rename from sandbox/darren/text_justify.py rename to sandbox/darren/text_align.py index 74f8afdfe..ccc55696a 100644 --- a/sandbox/darren/text_justify.py +++ b/sandbox/darren/text_align.py @@ -12,22 +12,22 @@ TEXT = ( ) -class TextJustify(App): +class TextAlign(App): def compose(self) -> ComposeResult: - left = Static("[b]Left justified[/]\n" + TEXT, id="one") + left = Static("[b]Left aligned[/]\n" + TEXT, id="one") yield left - right = Static("[b]Center justified[/]\n" + TEXT, id="two") + right = Static("[b]Center aligned[/]\n" + TEXT, id="two") yield right - center = Static("[b]Right justified[/]\n" + TEXT, id="three") + center = Static("[b]Right aligned[/]\n" + TEXT, id="three") yield center - full = Static("[b]Full justified[/]\n" + TEXT, id="four") + full = Static("[b]Fully justified[/]\n" + TEXT, id="four") yield full -app = TextJustify(css_path="text_justify.scss", watch_css=True) +app = TextAlign(css_path="text_align.scss", watch_css=True) if __name__ == "__main__": app.run() diff --git a/sandbox/darren/text_justify.scss b/sandbox/darren/text_align.scss similarity index 65% rename from sandbox/darren/text_justify.scss rename to sandbox/darren/text_align.scss index 589f7bfd6..c594254d6 100644 --- a/sandbox/darren/text_justify.scss +++ b/sandbox/darren/text_align.scss @@ -1,21 +1,21 @@ #one { - text-justify: left; + text-align: left; background: lightblue; } #two { - text-justify: center; + text-align: center; background: indianred; } #three { - text-justify: right; + text-align: right; background: palegreen; } #four { - text-justify: full; + text-align: justify; background: palevioletred; } diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py index e607741cb..70d36e28e 100644 --- a/src/textual/css/_help_text.py +++ b/src/textual/css/_help_text.py @@ -13,7 +13,7 @@ from textual.css.constants import ( VALID_ALIGN_HORIZONTAL, VALID_ALIGN_VERTICAL, VALID_STYLE_FLAGS, - VALID_JUSTIFY, + VALID_TEXT_ALIGN, ) if sys.version_info >= (3, 8): @@ -649,31 +649,31 @@ def align_help_text() -> HelpText: ) -def text_justify_help_text(context: str) -> HelpText: - """Help text to show when the user supplies an invalid value for the text-justify property +def text_align_help_text(context: str) -> 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-justify[/] property.", + summary="Invalid value for the [i]text-align[/] property.", bullets=[ *ContextSpecificBullets( css=[ Bullet( - f"The [i]text-justify[/] property must be one of {friendly_list(VALID_JUSTIFY)}", + f"The [i]text-align[/] property must be one of {friendly_list(VALID_TEXT_ALIGN)}", examples=[ - Example("text-justify: center;"), - Example("text-justify: right;"), + Example("text-align: center;"), + Example("text-align: right;"), ], ) ], inline=[ Bullet( - f"The [i]text_justify[/] property must be one of {friendly_list(VALID_JUSTIFY)}", + f"The [i]text_align[/] property must be one of {friendly_list(VALID_TEXT_ALIGN)}", examples=[ - Example("widget.styles.text_justify = 'center'"), - Example("widget.styles.text_justify = 'right'"), + Example("widget.styles.text_align = 'center'"), + Example("widget.styles.text_align = 'right'"), ], ) ], diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index c6b276528..9973e8f36 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -24,7 +24,7 @@ from ._help_text import ( property_invalid_value_help_text, scrollbar_size_property_help_text, scrollbar_size_single_axis_help_text, - text_justify_help_text, + text_align_help_text, ) from .constants import ( VALID_ALIGN_HORIZONTAL, @@ -37,7 +37,7 @@ from .constants import ( VALID_VISIBILITY, VALID_STYLE_FLAGS, VALID_SCROLLBAR_GUTTER, - VALID_JUSTIFY, + VALID_TEXT_ALIGN, ) from .errors import DeclarationError, StyleValueError from .model import Declaration @@ -620,19 +620,19 @@ class StylesBuilder: style_definition = " ".join(token.value for token in tokens) self.styles.text_style = style_definition - def process_text_justify(self, name: str, tokens: list[Token]) -> None: - """Process a text-justify declaration""" + 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_JUSTIFY: + if len(tokens) > 1 or tokens[0].value not in VALID_TEXT_ALIGN: self.error( name, tokens[0], - text_justify_help_text("css"), + text_align_help_text("css"), ) - self.styles._rules["text_justify"] = tokens[0].value + self.styles._rules["text_align"] = tokens[0].value def process_dock(self, name: str, tokens: list[Token]) -> None: if not tokens: diff --git a/src/textual/css/constants.py b/src/textual/css/constants.py index 0f9b25e54..0025551d7 100644 --- a/src/textual/css/constants.py +++ b/src/textual/css/constants.py @@ -38,7 +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_JUSTIFY: Final = {"left", "center", "right", "full"} +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 c9f22ad6c..77a1043b2 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,7 +41,7 @@ from .constants import ( VALID_OVERFLOW, VALID_SCROLLBAR_GUTTER, VALID_VISIBILITY, - VALID_JUSTIFY, + VALID_TEXT_ALIGN, ) from .scalar import Scalar, ScalarOffset, Unit from .scalar_animation import ScalarAnimation @@ -145,7 +144,7 @@ class RulesMap(TypedDict, total=False): content_align_horizontal: AlignHorizontal content_align_vertical: AlignVertical - text_justify: TextJustify + text_align: TextJustify RULE_NAMES = list(RulesMap.__annotations__.keys()) @@ -254,7 +253,7 @@ class StylesBase(ABC): content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top") content_align = AlignProperty() - text_justify = StringEnumProperty(VALID_JUSTIFY, "left") + text_align = StringEnumProperty(VALID_TEXT_ALIGN, "start") def __eq__(self, styles: object) -> bool: """Check that Styles contains the same rules.""" diff --git a/src/textual/widget.py b/src/textual/widget.py index d1c31761f..f58202a0b 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,13 +1211,15 @@ class Widget(DOMNode): """ if isinstance(renderable, str): - renderable = Text.from_markup(renderable, justify=self.styles.text_justify) + 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: - renderable.justify = self.styles.text_justify + justify = _get_rich_justify(self.styles.text_align) + renderable.justify = justify else: renderable = Styled(renderable, rich_style) @@ -1579,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 0d340e1b7..026de9d8a 100644 --- a/tests/css/test_parse.py +++ b/tests/css/test_parse.py @@ -1134,14 +1134,14 @@ class TestParsePadding: class TestParseTextJustify: @pytest.mark.parametrize("valid_justify", ["left", "center", "full", "right"]) - def test_text_justify(self, valid_justify): - css = f"#foo {{ text-justify: {valid_justify} }}" + def test_text_align(self, valid_justify): + css = f"#foo {{ text-align: {valid_justify} }}" stylesheet = Stylesheet() stylesheet.add_source(css) - assert stylesheet.rules[0].styles.text_justify == valid_justify + assert stylesheet.rules[0].styles.text_align == valid_justify - def test_text_justify_invalid(self): - css = "#foo { text-justify: invalid-value; }" + def test_text_align_invalid(self): + css = "#foo { text-align: invalid-value; }" stylesheet = Stylesheet() with pytest.raises(StylesheetParseError): stylesheet.add_source(css) @@ -1149,8 +1149,8 @@ class TestParseTextJustify: rules = stylesheet._parse_rules(css, "foo") assert rules[0].errors - def test_text_justify_empty_uses_default(self): - css = "#foo { text-justify: ; }" + def test_text_align_empty_uses_default(self): + css = "#foo { text-align: ; }" stylesheet = Stylesheet() stylesheet.add_source(css) - assert stylesheet.rules[0].styles.text_justify == "left" + assert stylesheet.rules[0].styles.text_align == "left" From 6ff60474174381a87d183bcab8ffd130d653361a Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 15:38:09 +0100 Subject: [PATCH 11/14] text-align testing --- tests/css/test_parse.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/css/test_parse.py b/tests/css/test_parse.py index 026de9d8a..d22c8fe46 100644 --- a/tests/css/test_parse.py +++ b/tests/css/test_parse.py @@ -1132,13 +1132,13 @@ class TestParsePadding: assert stylesheet.rules[0].styles.padding == Spacing(2, 3, -1, 1) -class TestParseTextJustify: - @pytest.mark.parametrize("valid_justify", ["left", "center", "full", "right"]) - def test_text_align(self, valid_justify): - css = f"#foo {{ text-align: {valid_justify} }}" +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_justify + assert stylesheet.rules[0].styles.text_align == valid_align def test_text_align_invalid(self): css = "#foo { text-align: invalid-value; }" @@ -1153,4 +1153,4 @@ class TestParseTextJustify: css = "#foo { text-align: ; }" stylesheet = Stylesheet() stylesheet.add_source(css) - assert stylesheet.rules[0].styles.text_align == "left" + assert stylesheet.rules[0].styles.text_align == "start" From 56d2516c645f7db5437aea6e83a6e517cafb60cc Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 15:51:32 +0100 Subject: [PATCH 12/14] TextJustify type to TextAlign --- src/textual/css/styles.py | 4 ++-- src/textual/css/types.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 77a1043b2..e8a25c514 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -57,7 +57,7 @@ from .types import ( Specificity3, Specificity6, Visibility, - TextJustify, + TextAlign, ) if sys.version_info >= (3, 8): @@ -144,7 +144,7 @@ class RulesMap(TypedDict, total=False): content_align_horizontal: AlignHorizontal content_align_vertical: AlignVertical - text_align: TextJustify + text_align: TextAlign RULE_NAMES = list(RulesMap.__annotations__.keys()) diff --git a/src/textual/css/types.py b/src/textual/css/types.py index 7b06e7561..6fb0929a4 100644 --- a/src/textual/css/types.py +++ b/src/textual/css/types.py @@ -39,7 +39,7 @@ ScrollbarGutter = Literal["auto", "stable"] BoxSizing = Literal["border-box", "content-box"] Overflow = Literal["scroll", "hidden", "auto"] EdgeStyle = Tuple[EdgeType, Color] -TextJustify = Literal["left", "center", "right", "full"] +TextAlign = Literal["left", "start", "center", "right", "end", "justify"] Specificity3 = Tuple[int, int, int] Specificity4 = Tuple[int, int, int, int] From 39665fb11065e7c53661e6cb9c4abe4447431765 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 16:19:05 +0100 Subject: [PATCH 13/14] Help text for text align --- src/textual/css/_help_text.py | 28 +++++++--------------------- src/textual/css/_styles_builder.py | 2 +- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/src/textual/css/_help_text.py b/src/textual/css/_help_text.py index 70d36e28e..2dc87dd1b 100644 --- a/src/textual/css/_help_text.py +++ b/src/textual/css/_help_text.py @@ -9,7 +9,6 @@ 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, @@ -649,7 +648,7 @@ def align_help_text() -> HelpText: ) -def text_align_help_text(context: str) -> HelpText: +def text_align_help_text() -> HelpText: """Help text to show when the user supplies an invalid value for the text-align property Returns: @@ -658,26 +657,13 @@ def text_align_help_text(context: str) -> HelpText: return HelpText( summary="Invalid value for the [i]text-align[/] property.", bullets=[ - *ContextSpecificBullets( - css=[ - 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;"), - ], - ) + 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;"), ], - inline=[ - Bullet( - f"The [i]text_align[/] property must be one of {friendly_list(VALID_TEXT_ALIGN)}", - examples=[ - Example("widget.styles.text_align = 'center'"), - Example("widget.styles.text_align = 'right'"), - ], - ) - ], - ).get_by_context(context) + ) ], ) diff --git a/src/textual/css/_styles_builder.py b/src/textual/css/_styles_builder.py index 9973e8f36..f94b966b6 100644 --- a/src/textual/css/_styles_builder.py +++ b/src/textual/css/_styles_builder.py @@ -629,7 +629,7 @@ class StylesBuilder: self.error( name, tokens[0], - text_align_help_text("css"), + text_align_help_text(), ) self.styles._rules["text_align"] = tokens[0].value From 3b33e220b78496a264103a3c4ef3316dcf91e984 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Fri, 26 Aug 2022 16:23:44 +0100 Subject: [PATCH 14/14] Change threshold in auto-refresh test --- tests/test_auto_refresh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_auto_refresh.py b/tests/test_auto_refresh.py index 4314f0729..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 0.3 <= elapsed < 0.8 + assert 0.2 <= elapsed < 0.8