From 4b5fd43423a327e4cd6d477a66bebc9588fd1488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Thu, 17 Nov 2022 16:40:09 +0000 Subject: [PATCH 01/17] Add scaffolding for the Placeholder widget. --- src/textual/widgets/_placeholder.py | 218 ++++++++++++++++++++++------ 1 file changed, 177 insertions(+), 41 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 7ce714a241..3562a7d735 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -1,63 +1,199 @@ from __future__ import annotations -from rich import box +from itertools import cycle +from typing import Literal + +from rich import box, repr from rich.align import Align -from rich.console import RenderableType from rich.panel import Panel from rich.pretty import Pretty -import rich.repr -from rich.style import Style from .. import events -from ..reactive import Reactive -from ..widget import Widget +from ..css._error_tools import friendly_list +from ..reactive import reactive +from ..widgets import Static + +PlaceholderVariant = Literal["default", "state", "position", "css", "text"] +_VALID_PLACEHOLDER_VARIANTS_ORDERED = ["default", "state", "position", "css", "text"] +_VALID_PLACEHOLDER_VARIANTS = set(_VALID_PLACEHOLDER_VARIANTS_ORDERED) +_PLACEHOLDER_BACKGROUND_COLORS = [ + "#881177", + "#aa3355", + "#cc6666", + "#ee9944", + "#eedd00", + "#99dd55", + "#44dd88", + "#22ccbb", + "#00bbcc", + "#0099cc", + "#3366bb", + "#663399", +] + + +class InvalidPlaceholderVariant(Exception): + pass + + +@repr.auto(angular=False) +class Placeholder(Static, can_focus=True): + """A simple placeholder widget to use before you build your custom widgets. + This placeholder has a couple of variants that show different data. + Clicking the placeholder cycles through the available variants, but a placeholder + can also be initialised in a specific variant. -@rich.repr.auto(angular=False) -class Placeholder(Widget, can_focus=True): + The variants available are: + default: shows a placeholder with a solid color. + state: shows the placeholder mouse over and focus state. + position: shows the size and position of the placeholder. + css: shows the css rules that apply to the placeholder. + text: shows some Lorem Ipsum text on the placeholder.""" - has_focus: Reactive[bool] = Reactive(False) - mouse_over: Reactive[bool] = Reactive(False) + DEFAULT_CSS = """ + Placeholder { + content-align: center middle; + } + """ + # Consecutive placeholders get assigned consecutive colors. + COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) + + variant = reactive("default") def __init__( - # parent class constructor signature: self, - *children: Widget, + variant: PlaceholderVariant = "default", + *, name: str | None = None, id: str | None = None, classes: str | None = None, - # ...and now for our own class specific params: - title: str | None = None, ) -> None: - super().__init__(*children, name=name, id=id, classes=classes) - self.title = title - - def __rich_repr__(self) -> rich.repr.Result: - yield from super().__rich_repr__() - yield "has_focus", self.has_focus, False - yield "mouse_over", self.mouse_over, False - - def render(self) -> RenderableType: - # Apply colours only inside render_styled - # Pass the full RICH style object into `render` - not the `Styles` - return Panel( - Align.center( - Pretty(self, no_wrap=True, overflow="ellipsis"), - vertical="middle", - ), - title=self.title or self.__class__.__name__, - border_style="green" if self.mouse_over else "blue", - box=box.HEAVY if self.has_focus else box.ROUNDED, + """Create a Placeholder widget. + + Args: + variant (PlaceholderVariant, optional): The variant of the placeholder. + Defaults to "default". + name (str | None, optional): The name of the placeholder. Defaults to None. + id (str | None, optional): The ID of the placeholder in the DOM. + Defaults to None. + classes (str | None, optional): A space separated string with the CSS classes + of the placeholder, if any. Defaults to None. + """ + super().__init__(name=name, id=id, classes=classes) + self.color = next(Placeholder.COLORS) + self.variant = self.validate_variant(variant) + # Set a cycle through the variants with the correct starting point. + self.variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED) + while next(self.variants_cycle) != self.variant: + pass + + def on_click(self) -> None: + """Clicking on the placeholder cycles through the placeholder variants.""" + self.cycle_variant() + + def cycle_variant(self) -> None: + """Get the next variant in the cycle.""" + self.variant = next(self.variants_cycle) + + def watch_variant(self, old_variant: str, variant: str) -> None: + self.remove_class(f"-{old_variant}") + self.add_class(f"-{variant}") + self.update_on_variant_change(variant) + + def update_on_variant_change(self, variant: str) -> None: + """Calls the appropriate method to update the render of the placeholder.""" + update_variant_method = getattr(self, f"_update_{variant}_variant", None) + assert update_variant_method is not None + try: + update_variant_method() + except TypeError as te: # triggered if update_variant_method is None + raise InvalidPlaceholderVariant( + "Valid placeholder variants are " + + f"{friendly_list(_VALID_PLACEHOLDER_VARIANTS)}" + ) from te + + def _update_default_variant(self) -> None: + """Update the placeholder with the "default" variant. + + This variant prints a panel with a solid color. + """ + self.update( + Panel( + Align.center("Placeholder"), + style=f"on {self.color}", + border_style=self.color, + ) + ) + + def _update_state_variant(self) -> None: + """Update the placeholder with the "state" variant. + + This variant pretty prints the placeholder, together with information about + whether the placeholder has focus and/or the mouse over it. + """ + data = {"has_focus": self.has_focus, "mouse_over": self.mouse_over} + self.update( + Panel( + Align.center( + Pretty(data), + vertical="middle", + ), + title="Placeholder", + border_style="green" if self.mouse_over else "blue", + box=box.HEAVY if self.has_focus else box.ROUNDED, + ) + ) + + def _update_position_variant(self) -> None: + """Update the placeholder with the "position" variant. + + This variant shows the position and the size of the widget. + """ + width, height = self.size + position_data = { + "width": width, + "height": height, + } + self.update(Panel(Align.center(Pretty(position_data)), title="Placeholder")) + + def _update_css_variant(self) -> None: + """Update the placeholder with the "css" variant. + + This variant shows all the CSS rules that are applied to this placeholder.""" + self.update(Panel(Pretty(self.styles), title="Placeholder")) + + def _update_text_variant(self) -> None: + """Update the placeholder with the "text" variant. + + This variant shows some Lorem Ipsum text.""" + self.update( + Panel( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat ac elit sit amet accumsan. Suspendisse bibendum nec libero quis gravida. Phasellus id eleifend ligula. Nullam imperdiet sem tellus, sed vehicula nisl faucibus sit amet. Praesent iaculis tempor ultricies. Sed lacinia, tellus id rutrum lacinia, sapien sapien congue mauris, sit amet pellentesque quam quam vel nisl. Curabitur vulputate erat pellentesque mauris posuere, non dictum risus mattis.", + title="Placeholder", + ) ) - async def on_focus(self, event: events.Focus) -> None: - self.has_focus = True + def on_resize(self, event: events.Resize) -> None: + """Update the placeholder render if the current variant needs it.""" + if self.variant == "position": + self._update_position_variant() - async def on_blur(self, event: events.Blur) -> None: - self.has_focus = False + def watch_has_focus(self, has_focus: bool) -> None: + """Update the placeholder render if the current variant needs it.""" + if self.variant == "state": + self._update_state_variant() - async def on_enter(self, event: events.Enter) -> None: - self.mouse_over = True + def watch_mouse_over(self, mouse_over: bool) -> None: + """Update the placeholder render if the current variant needs it.""" + if self.variant == "state": + self._update_state_variant() - async def on_leave(self, event: events.Leave) -> None: - self.mouse_over = False + def validate_variant(self, variant: PlaceholderVariant) -> str: + """Validate the variant to which the placeholder was set.""" + if variant not in _VALID_PLACEHOLDER_VARIANTS: + raise InvalidPlaceholderVariant( + "Valid placeholder variants are " + + f"{friendly_list(_VALID_PLACEHOLDER_VARIANTS)}" + ) + return variant From 67947d5806bb3181eba349f0da3fd35e0542d1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Thu, 17 Nov 2022 16:49:13 +0000 Subject: [PATCH 02/17] Fix documentation about the variant 'size'. --- src/textual/widgets/_placeholder.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 3562a7d735..e645e8b04f 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -13,8 +13,8 @@ from ..reactive import reactive from ..widgets import Static -PlaceholderVariant = Literal["default", "state", "position", "css", "text"] -_VALID_PLACEHOLDER_VARIANTS_ORDERED = ["default", "state", "position", "css", "text"] +PlaceholderVariant = Literal["default", "state", "size", "css", "text"] +_VALID_PLACEHOLDER_VARIANTS_ORDERED = ["default", "state", "size", "css", "text"] _VALID_PLACEHOLDER_VARIANTS = set(_VALID_PLACEHOLDER_VARIANTS_ORDERED) _PLACEHOLDER_BACKGROUND_COLORS = [ "#881177", @@ -47,9 +47,10 @@ class Placeholder(Static, can_focus=True): The variants available are: default: shows a placeholder with a solid color. state: shows the placeholder mouse over and focus state. - position: shows the size and position of the placeholder. + size: shows the size of the placeholder. css: shows the css rules that apply to the placeholder. - text: shows some Lorem Ipsum text on the placeholder.""" + text: shows some Lorem Ipsum text on the placeholder. + """ DEFAULT_CSS = """ Placeholder { @@ -145,10 +146,10 @@ def _update_state_variant(self) -> None: ) ) - def _update_position_variant(self) -> None: - """Update the placeholder with the "position" variant. + def _update_size_variant(self) -> None: + """Update the placeholder with the "size" variant. - This variant shows the position and the size of the widget. + This variant shows the the size of the widget. """ width, height = self.size position_data = { From 83c8a9b55f4ad9a141f23ebe9b2348ba12b06d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Thu, 17 Nov 2022 16:56:36 +0000 Subject: [PATCH 03/17] Style placeholder variants more uniformly. --- src/textual/widgets/_placeholder.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index e645e8b04f..16809d09d0 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -83,6 +83,7 @@ def __init__( """ super().__init__(name=name, id=id, classes=classes) self.color = next(Placeholder.COLORS) + self.styles.background = f"{self.color} 50%" self.variant = self.validate_variant(variant) # Set a cycle through the variants with the correct starting point. self.variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED) @@ -119,13 +120,7 @@ def _update_default_variant(self) -> None: This variant prints a panel with a solid color. """ - self.update( - Panel( - Align.center("Placeholder"), - style=f"on {self.color}", - border_style=self.color, - ) - ) + self.update(Panel("", title="Placeholder")) def _update_state_variant(self) -> None: """Update the placeholder with the "state" variant. @@ -156,18 +151,25 @@ def _update_size_variant(self) -> None: "width": width, "height": height, } - self.update(Panel(Align.center(Pretty(position_data)), title="Placeholder")) + self.update( + Panel( + Align.center(Pretty(position_data), vertical="middle"), + title="Placeholder", + ) + ) def _update_css_variant(self) -> None: """Update the placeholder with the "css" variant. - This variant shows all the CSS rules that are applied to this placeholder.""" + This variant shows all the CSS rules that are applied to this placeholder. + """ self.update(Panel(Pretty(self.styles), title="Placeholder")) def _update_text_variant(self) -> None: """Update the placeholder with the "text" variant. - This variant shows some Lorem Ipsum text.""" + This variant shows some Lorem Ipsum text. + """ self.update( Panel( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat ac elit sit amet accumsan. Suspendisse bibendum nec libero quis gravida. Phasellus id eleifend ligula. Nullam imperdiet sem tellus, sed vehicula nisl faucibus sit amet. Praesent iaculis tempor ultricies. Sed lacinia, tellus id rutrum lacinia, sapien sapien congue mauris, sit amet pellentesque quam quam vel nisl. Curabitur vulputate erat pellentesque mauris posuere, non dictum risus mattis.", From 5dfa9c4845b1de8be3b2ed3e0455a6feb9a6c41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 18 Nov 2022 09:55:27 +0000 Subject: [PATCH 04/17] Change placeholder styles. --- src/textual/widgets/_placeholder.py | 52 +++++++++-------------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 16809d09d0..1b34780df2 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,9 +3,6 @@ from itertools import cycle from typing import Literal -from rich import box, repr -from rich.align import Align -from rich.panel import Panel from rich.pretty import Pretty from .. import events @@ -36,7 +33,6 @@ class InvalidPlaceholderVariant(Exception): pass -@repr.auto(angular=False) class Placeholder(Static, can_focus=True): """A simple placeholder widget to use before you build your custom widgets. @@ -66,6 +62,7 @@ def __init__( self, variant: PlaceholderVariant = "default", *, + label: str | None = None, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -75,6 +72,8 @@ def __init__( Args: variant (PlaceholderVariant, optional): The variant of the placeholder. Defaults to "default". + label (str | None, optional): The label to identify the placeholder. + If no label is present, uses the placeholder ID instead. Defaults to None. name (str | None, optional): The name of the placeholder. Defaults to None. id (str | None, optional): The ID of the placeholder in the DOM. Defaults to None. @@ -82,8 +81,9 @@ def __init__( of the placeholder, if any. Defaults to None. """ super().__init__(name=name, id=id, classes=classes) + self._placeholder_label = label if label else f"#{id}" if id else "Placeholder" self.color = next(Placeholder.COLORS) - self.styles.background = f"{self.color} 50%" + self.styles.background = f"{self.color} 70%" self.variant = self.validate_variant(variant) # Set a cycle through the variants with the correct starting point. self.variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED) @@ -106,21 +106,19 @@ def watch_variant(self, old_variant: str, variant: str) -> None: def update_on_variant_change(self, variant: str) -> None: """Calls the appropriate method to update the render of the placeholder.""" update_variant_method = getattr(self, f"_update_{variant}_variant", None) - assert update_variant_method is not None - try: - update_variant_method() - except TypeError as te: # triggered if update_variant_method is None + if update_variant_method is None: raise InvalidPlaceholderVariant( "Valid placeholder variants are " + f"{friendly_list(_VALID_PLACEHOLDER_VARIANTS)}" - ) from te + ) + update_variant_method() def _update_default_variant(self) -> None: """Update the placeholder with the "default" variant. This variant prints a panel with a solid color. """ - self.update(Panel("", title="Placeholder")) + self.update(self._placeholder_label) def _update_state_variant(self) -> None: """Update the placeholder with the "state" variant. @@ -129,17 +127,7 @@ def _update_state_variant(self) -> None: whether the placeholder has focus and/or the mouse over it. """ data = {"has_focus": self.has_focus, "mouse_over": self.mouse_over} - self.update( - Panel( - Align.center( - Pretty(data), - vertical="middle", - ), - title="Placeholder", - border_style="green" if self.mouse_over else "blue", - box=box.HEAVY if self.has_focus else box.ROUNDED, - ) - ) + self.update(Pretty(data)) def _update_size_variant(self) -> None: """Update the placeholder with the "size" variant. @@ -147,23 +135,18 @@ def _update_size_variant(self) -> None: This variant shows the the size of the widget. """ width, height = self.size - position_data = { + size_data = { "width": width, "height": height, } - self.update( - Panel( - Align.center(Pretty(position_data), vertical="middle"), - title="Placeholder", - ) - ) + self.update(Pretty(size_data)) def _update_css_variant(self) -> None: """Update the placeholder with the "css" variant. This variant shows all the CSS rules that are applied to this placeholder. """ - self.update(Panel(Pretty(self.styles), title="Placeholder")) + self.update(self.styles.css) def _update_text_variant(self) -> None: """Update the placeholder with the "text" variant. @@ -171,16 +154,13 @@ def _update_text_variant(self) -> None: This variant shows some Lorem Ipsum text. """ self.update( - Panel( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat ac elit sit amet accumsan. Suspendisse bibendum nec libero quis gravida. Phasellus id eleifend ligula. Nullam imperdiet sem tellus, sed vehicula nisl faucibus sit amet. Praesent iaculis tempor ultricies. Sed lacinia, tellus id rutrum lacinia, sapien sapien congue mauris, sit amet pellentesque quam quam vel nisl. Curabitur vulputate erat pellentesque mauris posuere, non dictum risus mattis.", - title="Placeholder", - ) + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat ac elit sit amet accumsan. Suspendisse bibendum nec libero quis gravida. Phasellus id eleifend ligula. Nullam imperdiet sem tellus, sed vehicula nisl faucibus sit amet. Praesent iaculis tempor ultricies. Sed lacinia, tellus id rutrum lacinia, sapien sapien congue mauris, sit amet pellentesque quam quam vel nisl. Curabitur vulputate erat pellentesque mauris posuere, non dictum risus mattis." ) def on_resize(self, event: events.Resize) -> None: """Update the placeholder render if the current variant needs it.""" - if self.variant == "position": - self._update_position_variant() + if self.variant == "size": + self._update_size_variant() def watch_has_focus(self, has_focus: bool) -> None: """Update the placeholder render if the current variant needs it.""" From 392a95d0a9a4f4e94880589ca058f11549044bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 18 Nov 2022 09:58:45 +0000 Subject: [PATCH 05/17] Simplify docstrings. --- src/textual/widgets/_placeholder.py | 34 ++++++++--------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 1b34780df2..48a484b0e8 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -44,7 +44,7 @@ class Placeholder(Static, can_focus=True): default: shows a placeholder with a solid color. state: shows the placeholder mouse over and focus state. size: shows the size of the placeholder. - css: shows the css rules that apply to the placeholder. + css: shows the CSS rules that apply to the placeholder. text: shows some Lorem Ipsum text on the placeholder. """ @@ -114,26 +114,16 @@ def update_on_variant_change(self, variant: str) -> None: update_variant_method() def _update_default_variant(self) -> None: - """Update the placeholder with the "default" variant. - - This variant prints a panel with a solid color. - """ + """Update the placeholder with its label.""" self.update(self._placeholder_label) def _update_state_variant(self) -> None: - """Update the placeholder with the "state" variant. - - This variant pretty prints the placeholder, together with information about - whether the placeholder has focus and/or the mouse over it. - """ + """Update the placeholder with its focus and mouse over status.""" data = {"has_focus": self.has_focus, "mouse_over": self.mouse_over} self.update(Pretty(data)) def _update_size_variant(self) -> None: - """Update the placeholder with the "size" variant. - - This variant shows the the size of the widget. - """ + """Update the placeholder with the size of the placeholder.""" width, height = self.size size_data = { "width": width, @@ -142,33 +132,27 @@ def _update_size_variant(self) -> None: self.update(Pretty(size_data)) def _update_css_variant(self) -> None: - """Update the placeholder with the "css" variant. - - This variant shows all the CSS rules that are applied to this placeholder. - """ + """Update the placeholder with the CSS rules applied to this placeholder.""" self.update(self.styles.css) def _update_text_variant(self) -> None: - """Update the placeholder with the "text" variant. - - This variant shows some Lorem Ipsum text. - """ + """Update the placeholder with some Lorem Ipsum text.""" self.update( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat ac elit sit amet accumsan. Suspendisse bibendum nec libero quis gravida. Phasellus id eleifend ligula. Nullam imperdiet sem tellus, sed vehicula nisl faucibus sit amet. Praesent iaculis tempor ultricies. Sed lacinia, tellus id rutrum lacinia, sapien sapien congue mauris, sit amet pellentesque quam quam vel nisl. Curabitur vulputate erat pellentesque mauris posuere, non dictum risus mattis." ) def on_resize(self, event: events.Resize) -> None: - """Update the placeholder render if the current variant needs it.""" + """Update the placeholder "size" variant with the new placeholder size.""" if self.variant == "size": self._update_size_variant() def watch_has_focus(self, has_focus: bool) -> None: - """Update the placeholder render if the current variant needs it.""" + """Update the placeholder "state" variant with the new focus state.""" if self.variant == "state": self._update_state_variant() def watch_mouse_over(self, mouse_over: bool) -> None: - """Update the placeholder render if the current variant needs it.""" + """Update the placeholder "state" variant with the new mouse over state.""" if self.variant == "state": self._update_state_variant() From 8d94e32afddcce49c737a0bdf5d74034b117ea18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 18 Nov 2022 14:33:38 +0000 Subject: [PATCH 06/17] Remove variants css and state. --- src/textual/widgets/_placeholder.py | 76 +++++++++++++---------------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 48a484b0e8..73c044ee39 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,15 +3,16 @@ from itertools import cycle from typing import Literal -from rich.pretty import Pretty +from rich.text import Text from .. import events +from ..app import ComposeResult from ..css._error_tools import friendly_list from ..reactive import reactive from ..widgets import Static -PlaceholderVariant = Literal["default", "state", "size", "css", "text"] -_VALID_PLACEHOLDER_VARIANTS_ORDERED = ["default", "state", "size", "css", "text"] +PlaceholderVariant = Literal["default", "size", "text"] +_VALID_PLACEHOLDER_VARIANTS_ORDERED = ["default", "size", "text"] _VALID_PLACEHOLDER_VARIANTS = set(_VALID_PLACEHOLDER_VARIANTS_ORDERED) _PLACEHOLDER_BACKGROUND_COLORS = [ "#881177", @@ -33,6 +34,10 @@ class InvalidPlaceholderVariant(Exception): pass +class _PlaceholderLabel(Static): + pass + + class Placeholder(Static, can_focus=True): """A simple placeholder widget to use before you build your custom widgets. @@ -41,15 +46,22 @@ class Placeholder(Static, can_focus=True): can also be initialised in a specific variant. The variants available are: - default: shows a placeholder with a solid color. - state: shows the placeholder mouse over and focus state. + default: shows an identifier label or the ID of the placeholder. size: shows the size of the placeholder. - css: shows the CSS rules that apply to the placeholder. text: shows some Lorem Ipsum text on the placeholder. """ DEFAULT_CSS = """ Placeholder { + align: center middle; + overflow-y: auto; + } + + Placeholder.-text { + padding: 1; + } + + Placeholder > _PlaceholderLabel { content-align: center middle; } """ @@ -81,31 +93,34 @@ def __init__( of the placeholder, if any. Defaults to None. """ super().__init__(name=name, id=id, classes=classes) - self._placeholder_label = label if label else f"#{id}" if id else "Placeholder" - self.color = next(Placeholder.COLORS) - self.styles.background = f"{self.color} 70%" + self._placeholder_text = label if label else f"#{id}" if id else "Placeholder" + self._placeholder_label = _PlaceholderLabel() + self.styles.background = f"{next(Placeholder.COLORS)} 70%" self.variant = self.validate_variant(variant) # Set a cycle through the variants with the correct starting point. - self.variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED) - while next(self.variants_cycle) != self.variant: + self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED) + while next(self._variants_cycle) != self.variant: pass + def compose(self) -> ComposeResult: + yield self._placeholder_label + def on_click(self) -> None: - """Clicking on the placeholder cycles through the placeholder variants.""" + """Click handler to cycle through the placeholder variants.""" self.cycle_variant() def cycle_variant(self) -> None: """Get the next variant in the cycle.""" - self.variant = next(self.variants_cycle) + self.variant = next(self._variants_cycle) def watch_variant(self, old_variant: str, variant: str) -> None: self.remove_class(f"-{old_variant}") self.add_class(f"-{variant}") - self.update_on_variant_change(variant) + self.call_variant_update() - def update_on_variant_change(self, variant: str) -> None: + def call_variant_update(self) -> None: """Calls the appropriate method to update the render of the placeholder.""" - update_variant_method = getattr(self, f"_update_{variant}_variant", None) + update_variant_method = getattr(self, f"_update_{self.variant}_variant", None) if update_variant_method is None: raise InvalidPlaceholderVariant( "Valid placeholder variants are " @@ -115,29 +130,16 @@ def update_on_variant_change(self, variant: str) -> None: def _update_default_variant(self) -> None: """Update the placeholder with its label.""" - self.update(self._placeholder_label) - - def _update_state_variant(self) -> None: - """Update the placeholder with its focus and mouse over status.""" - data = {"has_focus": self.has_focus, "mouse_over": self.mouse_over} - self.update(Pretty(data)) + self._placeholder_label.update(self._placeholder_text) def _update_size_variant(self) -> None: """Update the placeholder with the size of the placeholder.""" width, height = self.size - size_data = { - "width": width, - "height": height, - } - self.update(Pretty(size_data)) - - def _update_css_variant(self) -> None: - """Update the placeholder with the CSS rules applied to this placeholder.""" - self.update(self.styles.css) + self._placeholder_label.update(f"[b]{width} x {height}[/b]") def _update_text_variant(self) -> None: """Update the placeholder with some Lorem Ipsum text.""" - self.update( + self._placeholder_label.update( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat ac elit sit amet accumsan. Suspendisse bibendum nec libero quis gravida. Phasellus id eleifend ligula. Nullam imperdiet sem tellus, sed vehicula nisl faucibus sit amet. Praesent iaculis tempor ultricies. Sed lacinia, tellus id rutrum lacinia, sapien sapien congue mauris, sit amet pellentesque quam quam vel nisl. Curabitur vulputate erat pellentesque mauris posuere, non dictum risus mattis." ) @@ -146,16 +148,6 @@ def on_resize(self, event: events.Resize) -> None: if self.variant == "size": self._update_size_variant() - def watch_has_focus(self, has_focus: bool) -> None: - """Update the placeholder "state" variant with the new focus state.""" - if self.variant == "state": - self._update_state_variant() - - def watch_mouse_over(self, mouse_over: bool) -> None: - """Update the placeholder "state" variant with the new mouse over state.""" - if self.variant == "state": - self._update_state_variant() - def validate_variant(self, variant: PlaceholderVariant) -> str: """Validate the variant to which the placeholder was set.""" if variant not in _VALID_PLACEHOLDER_VARIANTS: From 21630e07fc9451b20680636eb0e4231244b238d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 18 Nov 2022 14:51:13 +0000 Subject: [PATCH 07/17] Make Placeholder non-focusable. --- src/textual/widgets/_placeholder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 73c044ee39..b011b8ee26 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -38,7 +38,7 @@ class _PlaceholderLabel(Static): pass -class Placeholder(Static, can_focus=True): +class Placeholder(Static): """A simple placeholder widget to use before you build your custom widgets. This placeholder has a couple of variants that show different data. From a87c9ca916bfa8a61fce736cd6a1eba18d6b402c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:14:56 +0000 Subject: [PATCH 08/17] Add tests for placeholder widget. --- src/textual/widgets/_placeholder.py | 31 ++-- .../__snapshots__/test_snapshots.ambr | 170 ++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 5 + tests/test_placeholder.py | 15 ++ 4 files changed, 207 insertions(+), 14 deletions(-) create mode 100644 tests/test_placeholder.py diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index b011b8ee26..cbe0b50047 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,17 +3,21 @@ from itertools import cycle from typing import Literal -from rich.text import Text - from .. import events from ..app import ComposeResult from ..css._error_tools import friendly_list -from ..reactive import reactive +from ..reactive import Reactive, reactive from ..widgets import Static PlaceholderVariant = Literal["default", "size", "text"] -_VALID_PLACEHOLDER_VARIANTS_ORDERED = ["default", "size", "text"] -_VALID_PLACEHOLDER_VARIANTS = set(_VALID_PLACEHOLDER_VARIANTS_ORDERED) +_VALID_PLACEHOLDER_VARIANTS_ORDERED: list[PlaceholderVariant] = [ + "default", + "size", + "text", +] +_VALID_PLACEHOLDER_VARIANTS: set[PlaceholderVariant] = set( + _VALID_PLACEHOLDER_VARIANTS_ORDERED +) _PLACEHOLDER_BACKGROUND_COLORS = [ "#881177", "#aa3355", @@ -68,7 +72,7 @@ class Placeholder(Static): # Consecutive placeholders get assigned consecutive colors. COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) - variant = reactive("default") + variant: Reactive[PlaceholderVariant] = reactive("default") def __init__( self, @@ -113,19 +117,18 @@ def cycle_variant(self) -> None: """Get the next variant in the cycle.""" self.variant = next(self._variants_cycle) - def watch_variant(self, old_variant: str, variant: str) -> None: + def watch_variant( + self, old_variant: PlaceholderVariant, variant: PlaceholderVariant + ) -> None: + self.validate_variant(variant) self.remove_class(f"-{old_variant}") self.add_class(f"-{variant}") self.call_variant_update() def call_variant_update(self) -> None: """Calls the appropriate method to update the render of the placeholder.""" - update_variant_method = getattr(self, f"_update_{self.variant}_variant", None) - if update_variant_method is None: - raise InvalidPlaceholderVariant( - "Valid placeholder variants are " - + f"{friendly_list(_VALID_PLACEHOLDER_VARIANTS)}" - ) + update_variant_method = getattr(self, f"_update_{self.variant}_variant") + assert update_variant_method is not None update_variant_method() def _update_default_variant(self) -> None: @@ -148,7 +151,7 @@ def on_resize(self, event: events.Resize) -> None: if self.variant == "size": self._update_size_variant() - def validate_variant(self, variant: PlaceholderVariant) -> str: + def validate_variant(self, variant: PlaceholderVariant) -> PlaceholderVariant: """Validate the variant to which the placeholder was set.""" if variant not in _VALID_PLACEHOLDER_VARIANTS: raise InvalidPlaceholderVariant( diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 51ad5531c1..0e86e084a7 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -6322,6 +6322,176 @@ ''' # --- +# name: test_placeholder_render + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PlaceholderApp + + + + + + + + + + + Placeholder p2 here! + This is a custom label for p1. + #p4 + #p3#p5Placeholde + r + + Lorem ipsum dolor sit  + 26 x 6amet, consectetur 27 x 6 + adipiscing elit. Etiam  + feugiat ac elit sit  + + + Lorem ipsum dolor sit amet,  + consectetur adipiscing elit. Etiam 40 x 6 + feugiat ac elit sit amet accumsan.  + Suspendisse bibendum nec libero quis + gravida. Phasellus id eleifend  + ligula. Nullam imperdiet sem tellus, + sed vehicula nisl faucibus sit amet.Lorem ipsum dolor sit amet,  + Praesent iaculis tempor ultricies. ▆▆consectetur adipiscing elit. Etiam ▆▆ + Sed lacinia, tellus id rutrum feugiat ac elit sit amet accumsan.  + lacinia, sapien sapien congue Suspendisse bibendum nec libero quis + + + + + + ''' +# --- # name: test_textlog_max_lines ''' diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 41846ae117..10fa932f3a 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -71,6 +71,11 @@ def test_buttons_render(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "button.py", press=["tab"]) +def test_placeholder_render(snap_compare): + # Testing the rendering of the multiple placeholder variants and labels. + assert snap_compare(WIDGET_EXAMPLES_DIR / "placeholder.py") + + def test_datatable_render(snap_compare): press = ["tab", "down", "down", "right", "up", "left"] assert snap_compare(WIDGET_EXAMPLES_DIR / "data_table.py", press=press) diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py new file mode 100644 index 0000000000..74fe2e40fe --- /dev/null +++ b/tests/test_placeholder.py @@ -0,0 +1,15 @@ +import pytest + +from textual.widgets import Placeholder +from textual.widgets._placeholder import InvalidPlaceholderVariant + + +def test_invalid_placeholder_variant(): + with pytest.raises(InvalidPlaceholderVariant): + Placeholder("this is clearly not a valid variant!") + + +def test_invalid_reactive_variant_change(): + p = Placeholder() + with pytest.raises(InvalidPlaceholderVariant): + p.variant = "this is clearly not a valid variant!" From a8c5d1abe4eca07c649962d655eb2ebbe1f2615c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:16:38 +0000 Subject: [PATCH 09/17] Add docs for Placeholder. --- CHANGELOG.md | 1 + docs/api/placeholder.md | 1 + docs/examples/widgets/placeholder.css | 50 +++++++++++++++++++++++++++ docs/examples/widgets/placeholder.py | 39 +++++++++++++++++++++ docs/widgets/placeholder.md | 47 +++++++++++++++++++++++++ mkdocs.yml | 1 + 6 files changed, 139 insertions(+) create mode 100644 docs/api/placeholder.md create mode 100644 docs/examples/widgets/placeholder.css create mode 100644 docs/examples/widgets/placeholder.py create mode 100644 docs/widgets/placeholder.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 05165a611c..23e3071e73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). the return value of `DOMQuery.remove`, which uses to return `self`. https://github.com/Textualize/textual/issues/1094 - Added Pilot.wait_for_animation +- Added widget `Placeholder` ### Changed diff --git a/docs/api/placeholder.md b/docs/api/placeholder.md new file mode 100644 index 0000000000..4ec987d9b6 --- /dev/null +++ b/docs/api/placeholder.md @@ -0,0 +1 @@ +::: textual.widgets.Placeholder diff --git a/docs/examples/widgets/placeholder.css b/docs/examples/widgets/placeholder.css new file mode 100644 index 0000000000..0fe651cd51 --- /dev/null +++ b/docs/examples/widgets/placeholder.css @@ -0,0 +1,50 @@ +Placeholder { + height: 100%; +} + +#top { + height: 50%; + width: 100%; + layout: grid; + grid-size: 2 2; +} + +#left { + row-span: 2; +} + +#bot { + height: 50%; + width: 100%; + layout: grid; + grid-size: 8 8; +} + +#c1 { + row-span: 4; + column-span: 8; +} + +#col1, #col2, #col3 { + width: 1fr; +} + +#p1 { + row-span: 4; + column-span: 4; +} + +#p2 { + row-span: 2; + column-span: 4; +} + +#p3 { + row-span: 2; + column-span: 2; +} + +#p4 { + row-span: 1; + column-span: 2; +} diff --git a/docs/examples/widgets/placeholder.py b/docs/examples/widgets/placeholder.py new file mode 100644 index 0000000000..d47cadeca8 --- /dev/null +++ b/docs/examples/widgets/placeholder.py @@ -0,0 +1,39 @@ +from textual.app import App, ComposeResult +from textual.containers import Container, Horizontal, Vertical +from textual.widgets import Placeholder + + +class PlaceholderApp(App): + + CSS_PATH = "placeholder.css" + + def compose(self) -> ComposeResult: + yield Vertical( + Container( + Placeholder(label="This is a custom label for p1.", id="p1"), + Placeholder(label="Placeholder p2 here!", id="p2"), + Placeholder(id="p3"), + Placeholder(id="p4"), + Placeholder(id="p5"), + Placeholder(), + Horizontal( + Placeholder("size", id="col1"), + Placeholder("text", id="col2"), + Placeholder("size", id="col3"), + id="c1", + ), + id="bot" + ), + Container( + Placeholder("text", id="left"), + Placeholder("size", id="topright"), + Placeholder("text", id="botright"), + id="top", + ), + id="content", + ) + + +if __name__ == "__main__": + app = PlaceholderApp() + app.run() diff --git a/docs/widgets/placeholder.md b/docs/widgets/placeholder.md new file mode 100644 index 0000000000..be935d4a33 --- /dev/null +++ b/docs/widgets/placeholder.md @@ -0,0 +1,47 @@ +# Placeholder + + +A widget that is meant to have no complex functionality. +Use the placeholder widget when studying the layout of your app before having to develop your custom widgets. + +The placeholder widget has variants that display different bits of useful information. +Clicking a placeholder will cycle through its variants. + +- [ ] Focusable +- [ ] Container + +## Example + +The example below shows each placeholder variant. + +=== "Output" + + ```{.textual path="docs/examples/widgets/placeholder.py"} + ``` + +=== "placeholder.py" + + ```python + --8<-- "docs/examples/widgets/placeholder.py" + ``` + +=== "placeholder.css" + + ```css + --8<-- "docs/examples/widgets/placeholder.css" + ``` + +## Reactive Attributes + +| Name | Type | Default | Description | +| ---------- | ------ | ----------- | -------------------------------------------------- | +| `variant` | `str` | `"default"` | Styling variant. One of `default`, `size`, `text`. | + + +## Messages + +This widget sends no messages. + +## See Also + +* [Placeholder](../api/placeholder.md) code reference diff --git a/mkdocs.yml b/mkdocs.yml index e7b4e5f9e4..5019e4444e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -96,6 +96,7 @@ nav: - "widgets/footer.md" - "widgets/header.md" - "widgets/input.md" + - "widgets/placeholder.md" - "widgets/static.md" - "widgets/tree_control.md" - API: From be283ab3480d81047b3aad6f61ea8938565021f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:33:15 +0000 Subject: [PATCH 10/17] Use correct typing.Literal for Python 3.7. --- src/textual/widgets/_placeholder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index cbe0b50047..835f257e2a 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -1,13 +1,13 @@ from __future__ import annotations from itertools import cycle -from typing import Literal from .. import events from ..app import ComposeResult from ..css._error_tools import friendly_list from ..reactive import Reactive, reactive from ..widgets import Static +from .._typing import Literal PlaceholderVariant = Literal["default", "size", "text"] _VALID_PLACEHOLDER_VARIANTS_ORDERED: list[PlaceholderVariant] = [ From 0b30c3a1d5af133f095f42cc92f12b702deffe21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 18 Nov 2022 15:54:32 +0000 Subject: [PATCH 11/17] Fix snapshot test by resetting colour cycle. --- src/textual/widgets/_placeholder.py | 5 +++++ tests/snapshot_tests/test_snapshots.py | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 835f257e2a..a15b9f0ab4 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -74,6 +74,11 @@ class Placeholder(Static): variant: Reactive[PlaceholderVariant] = reactive("default") + @classmethod + def reset_color_cycle(cls) -> None: + """Reset the placeholder background color cycle.""" + cls.COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) + def __init__( self, variant: PlaceholderVariant = "default", diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 4409d03b7d..d80b1e7005 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -2,6 +2,8 @@ import pytest +from textual.widgets import Placeholder + # These paths should be relative to THIS directory. WIDGET_EXAMPLES_DIR = Path("../../docs/examples/widgets") LAYOUT_EXAMPLES_DIR = Path("../../docs/examples/guide/layout") @@ -79,6 +81,7 @@ def test_buttons_render(snap_compare): def test_placeholder_render(snap_compare): # Testing the rendering of the multiple placeholder variants and labels. + Placeholder.reset_color_cycle() assert snap_compare(WIDGET_EXAMPLES_DIR / "placeholder.py") From 5332217a561cee5f2abf39d77c396e8de98b64c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Tue, 22 Nov 2022 11:57:17 +0000 Subject: [PATCH 12/17] Make COLORS class attribute private. --- src/textual/widgets/_placeholder.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index a15b9f0ab4..9c2668b536 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -70,14 +70,14 @@ class Placeholder(Static): } """ # Consecutive placeholders get assigned consecutive colors. - COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) + _COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) variant: Reactive[PlaceholderVariant] = reactive("default") @classmethod def reset_color_cycle(cls) -> None: """Reset the placeholder background color cycle.""" - cls.COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) + cls._COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) def __init__( self, @@ -104,7 +104,7 @@ def __init__( super().__init__(name=name, id=id, classes=classes) self._placeholder_text = label if label else f"#{id}" if id else "Placeholder" self._placeholder_label = _PlaceholderLabel() - self.styles.background = f"{next(Placeholder.COLORS)} 70%" + self.styles.background = f"{next(Placeholder._COLORS)} 70%" self.variant = self.validate_variant(variant) # Set a cycle through the variants with the correct starting point. self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED) @@ -133,7 +133,6 @@ def watch_variant( def call_variant_update(self) -> None: """Calls the appropriate method to update the render of the placeholder.""" update_variant_method = getattr(self, f"_update_{self.variant}_variant") - assert update_variant_method is not None update_variant_method() def _update_default_variant(self) -> None: From 9cd592bd044b2175bfb54358f3bdcebff2dc6d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 25 Nov 2022 18:17:53 +0000 Subject: [PATCH 13/17] Reorder Placeholder parameters. --- src/textual/widgets/_placeholder.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 9c2668b536..311d467f52 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -81,9 +81,9 @@ def reset_color_cycle(cls) -> None: def __init__( self, + label: str | None = None, variant: PlaceholderVariant = "default", *, - label: str | None = None, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -91,10 +91,10 @@ def __init__( """Create a Placeholder widget. Args: - variant (PlaceholderVariant, optional): The variant of the placeholder. - Defaults to "default". label (str | None, optional): The label to identify the placeholder. If no label is present, uses the placeholder ID instead. Defaults to None. + variant (PlaceholderVariant, optional): The variant of the placeholder. + Defaults to "default". name (str | None, optional): The name of the placeholder. Defaults to None. id (str | None, optional): The ID of the placeholder in the DOM. Defaults to None. @@ -125,7 +125,6 @@ def cycle_variant(self) -> None: def watch_variant( self, old_variant: PlaceholderVariant, variant: PlaceholderVariant ) -> None: - self.validate_variant(variant) self.remove_class(f"-{old_variant}") self.add_class(f"-{variant}") self.call_variant_update() @@ -157,6 +156,7 @@ def on_resize(self, event: events.Resize) -> None: def validate_variant(self, variant: PlaceholderVariant) -> PlaceholderVariant: """Validate the variant to which the placeholder was set.""" + print("Validating") if variant not in _VALID_PLACEHOLDER_VARIANTS: raise InvalidPlaceholderVariant( "Valid placeholder variants are " From 9654748dd70088dcbbe61d770e1ed2844d5b4a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Fri, 25 Nov 2022 18:23:24 +0000 Subject: [PATCH 14/17] Update example and test files. --- docs/examples/widgets/placeholder.py | 16 ++++++++-------- tests/test_placeholder.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/examples/widgets/placeholder.py b/docs/examples/widgets/placeholder.py index d47cadeca8..0c64998426 100644 --- a/docs/examples/widgets/placeholder.py +++ b/docs/examples/widgets/placeholder.py @@ -10,24 +10,24 @@ class PlaceholderApp(App): def compose(self) -> ComposeResult: yield Vertical( Container( - Placeholder(label="This is a custom label for p1.", id="p1"), - Placeholder(label="Placeholder p2 here!", id="p2"), + Placeholder("This is a custom label for p1.", id="p1"), + Placeholder("Placeholder p2 here!", id="p2"), Placeholder(id="p3"), Placeholder(id="p4"), Placeholder(id="p5"), Placeholder(), Horizontal( - Placeholder("size", id="col1"), - Placeholder("text", id="col2"), - Placeholder("size", id="col3"), + Placeholder(variant="size", id="col1"), + Placeholder(variant="text", id="col2"), + Placeholder(variant="size", id="col3"), id="c1", ), id="bot" ), Container( - Placeholder("text", id="left"), - Placeholder("size", id="topright"), - Placeholder("text", id="botright"), + Placeholder(variant="text", id="left"), + Placeholder(variant="size", id="topright"), + Placeholder(variant="text", id="botright"), id="top", ), id="content", diff --git a/tests/test_placeholder.py b/tests/test_placeholder.py index 74fe2e40fe..6bc7a1e042 100644 --- a/tests/test_placeholder.py +++ b/tests/test_placeholder.py @@ -6,7 +6,7 @@ def test_invalid_placeholder_variant(): with pytest.raises(InvalidPlaceholderVariant): - Placeholder("this is clearly not a valid variant!") + Placeholder(variant="this is clearly not a valid variant!") def test_invalid_reactive_variant_change(): From 7f30f8bac3db4603a8c3eca7b7028b104f43a37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+rodrigogiraoserrao@users.noreply.github.com> Date: Fri, 25 Nov 2022 19:06:56 +0000 Subject: [PATCH 15/17] Remove sneaky print call. --- src/textual/widgets/_placeholder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 311d467f52..2fbbc3a30b 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -156,7 +156,6 @@ def on_resize(self, event: events.Resize) -> None: def validate_variant(self, variant: PlaceholderVariant) -> PlaceholderVariant: """Validate the variant to which the placeholder was set.""" - print("Validating") if variant not in _VALID_PLACEHOLDER_VARIANTS: raise InvalidPlaceholderVariant( "Valid placeholder variants are " From f427c55e591dd8de7ebbc9fae3f01167bc1d007c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Mon, 5 Dec 2022 13:47:07 +0000 Subject: [PATCH 16/17] Remove Placeholder dependency on Static. --- src/textual/widgets/_placeholder.py | 107 +++++++++++++++++----------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 2fbbc3a30b..33ad25e0c0 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,10 +3,11 @@ from itertools import cycle from .. import events -from ..app import ComposeResult +from ..containers import Container from ..css._error_tools import friendly_list from ..reactive import Reactive, reactive -from ..widgets import Static +from ..widget import Widget, RenderResult +from ..widgets import Label from .._typing import Literal PlaceholderVariant = Literal["default", "size", "text"] @@ -32,17 +33,23 @@ "#3366bb", "#663399", ] +_LOREM_IPSUM_PLACEHOLDER_TEXT = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat ac elit sit amet accumsan. Suspendisse bibendum nec libero quis gravida. Phasellus id eleifend ligula. Nullam imperdiet sem tellus, sed vehicula nisl faucibus sit amet. Praesent iaculis tempor ultricies. Sed lacinia, tellus id rutrum lacinia, sapien sapien congue mauris, sit amet pellentesque quam quam vel nisl. Curabitur vulputate erat pellentesque mauris posuere, non dictum risus mattis." class InvalidPlaceholderVariant(Exception): pass -class _PlaceholderLabel(Static): - pass +class _PlaceholderLabel(Widget): + def __init__(self, content, classes) -> None: + super().__init__(classes=classes) + self._content = content + + def render(self) -> RenderResult: + return self._content -class Placeholder(Static): +class Placeholder(Container): """A simple placeholder widget to use before you build your custom widgets. This placeholder has a couple of variants that show different data. @@ -58,19 +65,38 @@ class Placeholder(Static): DEFAULT_CSS = """ Placeholder { align: center middle; - overflow-y: auto; } Placeholder.-text { padding: 1; } + _PlaceholderLabel { + height: auto; + } + Placeholder > _PlaceholderLabel { content-align: center middle; } + + Placeholder.-default > _PlaceholderLabel.-size, + Placeholder.-default > _PlaceholderLabel.-text, + Placeholder.-size > _PlaceholderLabel.-default, + Placeholder.-size > _PlaceholderLabel.-text, + Placeholder.-text > _PlaceholderLabel.-default, + Placeholder.-text > _PlaceholderLabel.-size { + display: none; + } + + Placeholder.-default > _PlaceholderLabel.-default, + Placeholder.-size > _PlaceholderLabel.-size, + Placeholder.-text > _PlaceholderLabel.-text { + display: block; + } """ # Consecutive placeholders get assigned consecutive colors. _COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) + _SIZE_RENDER_TEMPLATE = "[b]{} x {}[/b]" variant: Reactive[PlaceholderVariant] = reactive("default") @@ -101,23 +127,36 @@ def __init__( classes (str | None, optional): A space separated string with the CSS classes of the placeholder, if any. Defaults to None. """ - super().__init__(name=name, id=id, classes=classes) - self._placeholder_text = label if label else f"#{id}" if id else "Placeholder" - self._placeholder_label = _PlaceholderLabel() + # Create and cache labels for all the variants. + self._default_label = _PlaceholderLabel( + label if label else f"#{id}" if id else "Placeholder", + "-default", + ) + self._size_label = _PlaceholderLabel( + "", + "-size", + ) + self._text_label = _PlaceholderLabel( + _LOREM_IPSUM_PLACEHOLDER_TEXT, + "-text", + ) + super().__init__( + self._default_label, + self._size_label, + self._text_label, + name=name, + id=id, + classes=classes, + ) + self.styles.background = f"{next(Placeholder._COLORS)} 70%" + self.variant = self.validate_variant(variant) # Set a cycle through the variants with the correct starting point. self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED) while next(self._variants_cycle) != self.variant: pass - def compose(self) -> ComposeResult: - yield self._placeholder_label - - def on_click(self) -> None: - """Click handler to cycle through the placeholder variants.""" - self.cycle_variant() - def cycle_variant(self) -> None: """Get the next variant in the cycle.""" self.variant = next(self._variants_cycle) @@ -127,32 +166,6 @@ def watch_variant( ) -> None: self.remove_class(f"-{old_variant}") self.add_class(f"-{variant}") - self.call_variant_update() - - def call_variant_update(self) -> None: - """Calls the appropriate method to update the render of the placeholder.""" - update_variant_method = getattr(self, f"_update_{self.variant}_variant") - update_variant_method() - - def _update_default_variant(self) -> None: - """Update the placeholder with its label.""" - self._placeholder_label.update(self._placeholder_text) - - def _update_size_variant(self) -> None: - """Update the placeholder with the size of the placeholder.""" - width, height = self.size - self._placeholder_label.update(f"[b]{width} x {height}[/b]") - - def _update_text_variant(self) -> None: - """Update the placeholder with some Lorem Ipsum text.""" - self._placeholder_label.update( - "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam feugiat ac elit sit amet accumsan. Suspendisse bibendum nec libero quis gravida. Phasellus id eleifend ligula. Nullam imperdiet sem tellus, sed vehicula nisl faucibus sit amet. Praesent iaculis tempor ultricies. Sed lacinia, tellus id rutrum lacinia, sapien sapien congue mauris, sit amet pellentesque quam quam vel nisl. Curabitur vulputate erat pellentesque mauris posuere, non dictum risus mattis." - ) - - def on_resize(self, event: events.Resize) -> None: - """Update the placeholder "size" variant with the new placeholder size.""" - if self.variant == "size": - self._update_size_variant() def validate_variant(self, variant: PlaceholderVariant) -> PlaceholderVariant: """Validate the variant to which the placeholder was set.""" @@ -162,3 +175,13 @@ def validate_variant(self, variant: PlaceholderVariant) -> PlaceholderVariant: + f"{friendly_list(_VALID_PLACEHOLDER_VARIANTS)}" ) return variant + + def on_click(self) -> None: + """Click handler to cycle through the placeholder variants.""" + self.cycle_variant() + + def on_resize(self, event: events.Resize) -> None: + """Update the placeholder "size" variant with the new placeholder size.""" + self._size_label._content = self._SIZE_RENDER_TEMPLATE.format(*self.size) + if self.variant == "size": + self._size_label.refresh(layout=True) From 08bf1bcbe97ecad37b66d6af86cbdede11ee526e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20Gir=C3=A3o=20Serr=C3=A3o?= <5621605+RodrigoGiraoSerrao@users.noreply.github.com> Date: Mon, 5 Dec 2022 15:10:30 +0000 Subject: [PATCH 17/17] Update CHANGELOG. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b77e80e5..61144b427a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Added "inherited bindings" -- BINDINGS classvar will be merged with base classes, unless inherit_bindings is set to False - Added `Tree` widget which replaces `TreeControl`. +- Added widget `Placeholder` https://github.com/Textualize/textual/issues/1200. ### Changed @@ -46,7 +47,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Support lazy-instantiated Screens (callables in App.SCREENS) https://github.com/Textualize/textual/pull/1185 - Display of keys in footer has more sensible defaults https://github.com/Textualize/textual/pull/1213 - Add App.get_key_display, allowing custom key_display App-wide https://github.com/Textualize/textual/pull/1213 -- Added widget `Placeholder` ### Changed