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/29] Add scaffolding for the Placeholder widget. --- src/textual/widgets/_placeholder.py | 214 +++++++++++++++++++++++----- 1 file changed, 175 insertions(+), 39 deletions(-) diff --git a/src/textual/widgets/_placeholder.py b/src/textual/widgets/_placeholder.py index 7ce714a24..3562a7d73 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", +] -@rich.repr.auto(angular=False) -class Placeholder(Widget, can_focus=True): +class InvalidPlaceholderVariant(Exception): + pass - has_focus: Reactive[bool] = Reactive(False) - mouse_over: Reactive[bool] = Reactive(False) + +@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. + + 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.""" + + 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 + """Create a Placeholder widget. - 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 + 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 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, + 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, + ) ) - async def on_focus(self, event: events.Focus) -> None: - self.has_focus = True + def _update_state_variant(self) -> None: + """Update the placeholder with the "state" variant. - async def on_blur(self, event: events.Blur) -> None: - self.has_focus = False + 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, + ) + ) - async def on_enter(self, event: events.Enter) -> None: - self.mouse_over = True + def _update_position_variant(self) -> None: + """Update the placeholder with the "position" variant. - async def on_leave(self, event: events.Leave) -> None: - self.mouse_over = False + 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", + ) + ) + + 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() + + 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() + + 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() + + 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/29] 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 3562a7d73..e645e8b04 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -13,8 +13,8 @@ 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"] +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 @@ class Placeholder(Static, can_focus=True): ) ) - 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/29] 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 e645e8b04..16809d09d 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -83,6 +83,7 @@ class Placeholder(Static, can_focus=True): """ 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 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): "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/29] 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 16809d09d..1b34780df 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,9 +3,6 @@ from __future__ import annotations 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 @@ class Placeholder(Static, can_focus=True): self, variant: PlaceholderVariant = "default", *, + label: str | None = None, name: str | None = None, id: str | None = None, classes: str | None = None, @@ -75,6 +72,8 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): 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/29] 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 1b34780df..48a484b0e 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 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): 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/29] 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 48a484b0e..73c044ee3 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,15 +3,16 @@ from __future__ import annotations 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 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): 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 @@ class Placeholder(Static, can_focus=True): 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/29] 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 73c044ee3..b011b8ee2 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/29] 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 b011b8ee2..cbe0b5004 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,17 +3,21 @@ from __future__ import annotations 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 @@ class Placeholder(Static): """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 @@ class Placeholder(Static): 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 51ad5531c..0e86e084a 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 41846ae11..10fa932f3 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 000000000..74fe2e40f --- /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/29] 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 05165a611..23e3071e7 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 000000000..4ec987d9b --- /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 000000000..0fe651cd5 --- /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 000000000..d47cadeca --- /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 000000000..be935d4a3 --- /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 e7b4e5f9e..5019e4444 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/29] 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 cbe0b5004..835f257e2 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/29] 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 835f257e2..a15b9f0ab 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 4409d03b7..d80b1e700 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -2,6 +2,8 @@ from pathlib import Path 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/29] 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 a15b9f0ab..9c2668b53 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 @@ class Placeholder(Static): 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 @@ class Placeholder(Static): 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 68da8059ae4529db3888d2410f1f83a36958e6da 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: Wed, 23 Nov 2022 10:14:01 +0000 Subject: [PATCH 13/29] Set default container height to 1fr. --- src/textual/containers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/textual/containers.py b/src/textual/containers.py index 231e220b0..bbd4c13d7 100644 --- a/src/textual/containers.py +++ b/src/textual/containers.py @@ -6,6 +6,7 @@ class Container(Widget): DEFAULT_CSS = """ Container { + height: 1fr; layout: vertical; overflow: auto; } @@ -17,6 +18,7 @@ class Vertical(Widget): DEFAULT_CSS = """ Vertical { + height: 1fr; layout: vertical; overflow-y: auto; } @@ -28,6 +30,7 @@ class Horizontal(Widget): DEFAULT_CSS = """ Horizontal { + height: 1fr; layout: horizontal; overflow-x: hidden; } @@ -39,6 +42,7 @@ class Grid(Widget): DEFAULT_CSS = """ Grid { + height: 1fr; layout: grid; } """ @@ -49,6 +53,7 @@ class Content(Widget, can_focus=True, can_focus_children=False): DEFAULT_CSS = """ Vertical { + height: 1fr; layout: vertical; overflow-y: auto; } From 00bdb671e854c23ded986fe4808ae8a26e7f2d69 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: Wed, 23 Nov 2022 10:15:27 +0000 Subject: [PATCH 14/29] Update changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 18c26b55a..7a769727b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Rebuilt `DirectoryTree` with new `Tree` control. +- Container widgets now have default height of `1fr`. ## [0.5.0] - 2022-11-20 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 15/29] 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 9c2668b53..311d467f5 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -81,9 +81,9 @@ class Placeholder(Static): 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 @@ class Placeholder(Static): """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 @@ class Placeholder(Static): 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 @@ class Placeholder(Static): 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 16/29] 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 d47cadeca..0c6499842 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 74fe2e40f..6bc7a1e04 100644 --- a/tests/test_placeholder.py +++ b/tests/test_placeholder.py @@ -6,7 +6,7 @@ from textual.widgets._placeholder import InvalidPlaceholderVariant 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 17/29] 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 311d467f5..2fbbc3a30 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -156,7 +156,6 @@ class Placeholder(Static): 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 0a8b001c625f1964ce3818d85880c4ab67f069ac Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 4 Dec 2022 17:17:27 +0700 Subject: [PATCH 18/29] fix for layer ordering --- src/textual/_arrange.py | 1 + .../snapshot_apps/order_independence.py | 71 +++++++++++++++++++ tests/snapshot_tests/test_snapshots.py | 10 +++ 3 files changed, 82 insertions(+) create mode 100644 tests/snapshot_tests/snapshot_apps/order_independence.py diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index 2bf706e0f..1ef708619 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -51,6 +51,7 @@ def arrange( styles = widget.styles for widgets in dock_layers.values(): + region = size.region layout_widgets, dock_widgets = partition(get_dock, widgets) diff --git a/tests/snapshot_tests/snapshot_apps/order_independence.py b/tests/snapshot_tests/snapshot_apps/order_independence.py new file mode 100644 index 000000000..0c682c859 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/order_independence.py @@ -0,0 +1,71 @@ +from textual.app import App, ComposeResult +from textual.screen import Screen +from textual.widgets import Header, Footer, Label +from textual.containers import Vertical, Container + + +class Overlay(Container): + def compose(self) -> ComposeResult: + yield Label("This should float over the top") + + +class Body1(Vertical): + def compose(self) -> ComposeResult: + yield Label("My God! It's full of stars! " * 300) + + +class Body2(Vertical): + def compose(self) -> ComposeResult: + yield Label("I'm sorry, Dave. I'm afraid I can't do that. " * 300) + + +class Good(Screen): + def compose(self) -> ComposeResult: + yield Header() + yield Overlay() + yield Body1() + yield Footer() + + +class Bad(Screen): + def compose(self) -> ComposeResult: + yield Overlay() + yield Header() + yield Body2() + yield Footer() + + +class Layers(App[None]): + + CSS = """ + Screen { + layers: base higher; + } + + Overlay { + layer: higher; + dock: top; + width: auto; + height: auto; + padding: 2; + border: solid yellow; + background: red; + color: yellow; + } + """ + + SCREENS = {"good": Good, "bad": Bad} + + BINDINGS = [("t", "toggle", "Toggle Screen")] + + def on_mount(self): + self.push_screen("good") + + def action_toggle(self): + self.switch_screen( + "bad" if self.screen.__class__.__name__ == "Good" else "good" + ) + + +if __name__ == "__main__": + Layers().run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index aae9669db..e287186c7 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -126,6 +126,16 @@ def test_multiple_css(snap_compare): assert snap_compare("snapshot_apps/multiple_css/multiple_css.py") +def test_order_independence(snap_compare): + # Interaction between multiple CSS files and app-level/classvar CSS + assert snap_compare("snapshot_apps/order_independence.py") + + +def test_order_independence_toggle(snap_compare): + # Interaction between multiple CSS files and app-level/classvar CSS + assert snap_compare("snapshot_apps/order_independence.py", press="t") + + # --- Other --- From eab69a773921b3ff1092b04ca07807ce75e3c373 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 4 Dec 2022 17:18:42 +0700 Subject: [PATCH 19/29] snapshots --- .../__snapshots__/test_snapshots.ambr | 320 ++++++++++++++++++ 1 file changed, 320 insertions(+) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index 977ab4286..a92a00f7a 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -6637,6 +6637,326 @@ ''' # --- +# name: test_order_independence + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Layers + + + + + + + + + + ┌────────────────────────────────────────┐ + ull of stars! My God! It's full of  +  It's full of stars! My God! It's  + This should float over the top My God! It's full of stars! My God! + tars! My God! It's full of stars! My +  of stars! My God! It's full of ▄▄ + └────────────────────────────────────────┘ It's full of stars! My God! It's  + full of stars! My God! It's full of stars! My God! It's full of stars! My God! + It's full of stars! My God! It's full of stars! My God! It's full of stars! My + God! It's full of stars! My God! It's full of stars! My God! It's full of  + stars! My God! It's full of stars! My God! It's full of stars! My God! It's  + full of stars! My God! It's full of stars! My God! It's full of stars! My God! + It's full of stars! My God! It's full of stars! My God! It's full of stars! My + God! It's full of stars! My God! It's full of stars! My God! It's full of  + stars! My God! It's full of stars! My God! It's full of stars! My God! It's  + full of stars! My God! It's full of stars! My God! It's full of stars! My God! + It's full of stars! My God! It's full of stars! My God! It's full of stars! My + God! It's full of stars! My God! It's full of stars! My God! It's full of  + stars! My God! It's full of stars! My God! It's full of stars! My God! It's  + full of stars! My God! It's full of stars! My God! It's full of stars! My God! + It's full of stars! My God! It's full of stars! My God! It's full of stars! My + God! It's full of stars! My God! It's full of stars! My God! It's full of  + stars! My God! It's full of stars! My God! It's full of stars! My God! It's  +  T  Toggle Screen  + + + + + ''' +# --- +# name: test_order_independence_toggle + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Layers + + + + + + + + + + ┌────────────────────────────────────────┐ + t. I'm sorry, Dave. I'm afraid I  +  I can't do that. I'm sorry, Dave.  + This should float over the tope. I'm afraid I can't do that. I'm ▂▂ + 'm sorry, Dave. I'm afraid I can't  + 't do that. I'm sorry, Dave. I'm  + └────────────────────────────────────────┘'m afraid I can't do that. I'm  + sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  + do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  + afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  + sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  + do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  + afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  + sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  + do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  + afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  + sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  + do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  + afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  + sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  + do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  + afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  + sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  +  T  Toggle Screen  + + + + + ''' +# --- # name: test_textlog_max_lines ''' From cc6f1c248d72226657c31979a8f9ca71521e0551 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 4 Dec 2022 17:22:51 +0700 Subject: [PATCH 20/29] tiny optimiztion --- src/textual/_arrange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/textual/_arrange.py b/src/textual/_arrange.py index 1ef708619..eb9818ee7 100644 --- a/src/textual/_arrange.py +++ b/src/textual/_arrange.py @@ -41,7 +41,6 @@ def arrange( placements: list[WidgetPlacement] = [] add_placement = placements.append - region = size.region _WidgetPlacement = WidgetPlacement top_z = TOP_Z @@ -50,8 +49,9 @@ def arrange( get_dock = attrgetter("styles.dock") styles = widget.styles + layer_region = size.region for widgets in dock_layers.values(): - region = size.region + region = layer_region layout_widgets, dock_widgets = partition(get_dock, widgets) From 962b7d64b41b9c2247f98a6ac5537bf78357c7f7 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Sun, 4 Dec 2022 17:25:09 +0700 Subject: [PATCH 21/29] pause --- tests/snapshot_tests/test_snapshots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index e287186c7..338d3fb5d 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -133,7 +133,7 @@ def test_order_independence(snap_compare): def test_order_independence_toggle(snap_compare): # Interaction between multiple CSS files and app-level/classvar CSS - assert snap_compare("snapshot_apps/order_independence.py", press="t") + assert snap_compare("snapshot_apps/order_independence.py", press="t,_") # --- Other --- From ab38d701f2b0cd4f0a6a53ed632618cb95125e26 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 5 Dec 2022 07:26:28 +0000 Subject: [PATCH 22/29] Change the default width of a label to auto See #1315 for the discussion that prompted this, #1316 for the issue where this particular aspect of it was isolated as something to address. I'm not 100% convinced that `Static` does the right thing here either, but I don't want to go changing too make things at once. However, I think a good argument can be made that `Label` should be `width: auto` by default, this seems to make perfect sense to me. I think everyone would expect, by default, that a label's width is the width of the content (plus any decoration, of course). --- CHANGELOG.md | 1 + src/textual/widgets/_label.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa06b03c0..89140e36d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Rebuilt `DirectoryTree` with new `Tree` control. +- The default `width` of a `Label` is now `auto`. ### Fixed diff --git a/src/textual/widgets/_label.py b/src/textual/widgets/_label.py index df519ae4f..732fc61c9 100644 --- a/src/textual/widgets/_label.py +++ b/src/textual/widgets/_label.py @@ -5,3 +5,11 @@ from ._static import Static class Label(Static): """A simple label widget for displaying text-oriented renderables.""" + + DEFAULT_CSS = """ + Static { + width: auto; + height: auto; + } + """ + """str: The default styling of a `Label`.""" From cd4b421b7484ed23243e901aeb29dcd44a4b94d8 Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 5 Dec 2022 07:40:28 +0000 Subject: [PATCH 23/29] Update snapshots to take into account new default Label width --- .../__snapshots__/test_snapshots.ambr | 242 +++++++++--------- 1 file changed, 120 insertions(+), 122 deletions(-) diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index a92a00f7a..c2ae2ab5f 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -6660,137 +6660,136 @@ font-weight: 700; } - .terminal-2104815115-matrix { + .terminal-404849936-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2104815115-title { + .terminal-404849936-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2104815115-r1 { fill: #ffff00 } - .terminal-2104815115-r2 { fill: #c5c8c6 } - .terminal-2104815115-r3 { fill: #e8e7e5 } - .terminal-2104815115-r4 { fill: #e1e1e1 } - .terminal-2104815115-r5 { fill: #14191f } - .terminal-2104815115-r6 { fill: #dde8f3;font-weight: bold } - .terminal-2104815115-r7 { fill: #ddedf9 } + .terminal-404849936-r1 { fill: #ffff00 } + .terminal-404849936-r2 { fill: #c5c8c6 } + .terminal-404849936-r3 { fill: #e8e7e5 } + .terminal-404849936-r4 { fill: #e1e1e1 } + .terminal-404849936-r5 { fill: #dde8f3;font-weight: bold } + .terminal-404849936-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Layers + Layers - - - - ┌────────────────────────────────────────┐ - ull of stars! My God! It's full of  -  It's full of stars! My God! It's  - This should float over the top My God! It's full of stars! My God! - tars! My God! It's full of stars! My -  of stars! My God! It's full of ▄▄ - └────────────────────────────────────────┘ It's full of stars! My God! It's  - full of stars! My God! It's full of stars! My God! It's full of stars! My God! - It's full of stars! My God! It's full of stars! My God! It's full of stars! My - God! It's full of stars! My God! It's full of stars! My God! It's full of  - stars! My God! It's full of stars! My God! It's full of stars! My God! It's  - full of stars! My God! It's full of stars! My God! It's full of stars! My God! - It's full of stars! My God! It's full of stars! My God! It's full of stars! My - God! It's full of stars! My God! It's full of stars! My God! It's full of  - stars! My God! It's full of stars! My God! It's full of stars! My God! It's  - full of stars! My God! It's full of stars! My God! It's full of stars! My God! - It's full of stars! My God! It's full of stars! My God! It's full of stars! My - God! It's full of stars! My God! It's full of stars! My God! It's full of  - stars! My God! It's full of stars! My God! It's full of stars! My God! It's  - full of stars! My God! It's full of stars! My God! It's full of stars! My God! - It's full of stars! My God! It's full of stars! My God! It's full of stars! My - God! It's full of stars! My God! It's full of stars! My God! It's full of  - stars! My God! It's full of stars! My God! It's full of stars! My God! It's  -  T  Toggle Screen  + + + + ┌────────────────────────────────────────┐ + ull of stars! My God! It's full of sta + + This should float over the top + + + └────────────────────────────────────────┘ + + + + + + + + + + + + + + + + +  T  Toggle Screen  @@ -6820,137 +6819,136 @@ font-weight: 700; } - .terminal-2660713569-matrix { + .terminal-1654293578-matrix { font-family: Fira Code, monospace; font-size: 20px; line-height: 24.4px; font-variant-east-asian: full-width; } - .terminal-2660713569-title { + .terminal-1654293578-title { font-size: 18px; font-weight: bold; font-family: arial; } - .terminal-2660713569-r1 { fill: #ffff00 } - .terminal-2660713569-r2 { fill: #c5c8c6 } - .terminal-2660713569-r3 { fill: #e8e7e5 } - .terminal-2660713569-r4 { fill: #e1e1e1 } - .terminal-2660713569-r5 { fill: #14191f } - .terminal-2660713569-r6 { fill: #dde8f3;font-weight: bold } - .terminal-2660713569-r7 { fill: #ddedf9 } + .terminal-1654293578-r1 { fill: #ffff00 } + .terminal-1654293578-r2 { fill: #c5c8c6 } + .terminal-1654293578-r3 { fill: #e8e7e5 } + .terminal-1654293578-r4 { fill: #e1e1e1 } + .terminal-1654293578-r5 { fill: #dde8f3;font-weight: bold } + .terminal-1654293578-r6 { fill: #ddedf9 } - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - Layers + Layers - - - - ┌────────────────────────────────────────┐ - t. I'm sorry, Dave. I'm afraid I  -  I can't do that. I'm sorry, Dave.  - This should float over the tope. I'm afraid I can't do that. I'm ▂▂ - 'm sorry, Dave. I'm afraid I can't  - 't do that. I'm sorry, Dave. I'm  - └────────────────────────────────────────┘'m afraid I can't do that. I'm  - sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  - do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  - afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  - sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  - do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  - afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  - sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  - do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  - afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  - sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  - do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  - afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  - sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  - do that. I'm sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm  - afraid I can't do that. I'm sorry, Dave. I'm afraid I can't do that. I'm  - sorry, Dave. I'm afraid I can't do that. I'm sorry, Dave. I'm afraid I can't  -  T  Toggle Screen  + + + + ┌────────────────────────────────────────┐ + t. I'm sorry, Dave. I'm afraid I can't + + This should float over the top + + + └────────────────────────────────────────┘ + + + + + + + + + + + + + + + + +  T  Toggle Screen  From 2e6e43e9b8040206bae8c29f8a453b441752749f Mon Sep 17 00:00:00 2001 From: Dave Pearson Date: Mon, 5 Dec 2022 11:28:12 +0000 Subject: [PATCH 24/29] Fix copy/paste-o in DEFAULT_CSS for Label --- src/textual/widgets/_label.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widgets/_label.py b/src/textual/widgets/_label.py index 732fc61c9..344c37013 100644 --- a/src/textual/widgets/_label.py +++ b/src/textual/widgets/_label.py @@ -7,7 +7,7 @@ class Label(Static): """A simple label widget for displaying text-oriented renderables.""" DEFAULT_CSS = """ - Static { + Label { width: auto; height: auto; } 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 25/29] 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 2fbbc3a30..33ad25e0c 100644 --- a/src/textual/widgets/_placeholder.py +++ b/src/textual/widgets/_placeholder.py @@ -3,10 +3,11 @@ from __future__ import annotations 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 @@ _PLACEHOLDER_BACKGROUND_COLORS = [ "#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 @@ class Placeholder(Static): 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 @@ class Placeholder(Static): ) -> 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 @@ class Placeholder(Static): + 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 26/29] Update CHANGELOG. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52b77e80e..61144b427 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 From 23c2c3edd3472af028cd7b557136d5304ccfb1bf Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 7 Dec 2022 14:41:30 +0000 Subject: [PATCH 27/29] fix for visiblity --- CHANGELOG.md | 1 + src/textual/_compositor.py | 65 +++++--- .../__snapshots__/test_snapshots.ambr | 157 ++++++++++++++++++ tests/snapshot_tests/snapshot_apps/vis.html | 34 ++++ .../snapshot_apps/visibility.py | 48 ++++++ tests/snapshot_tests/test_snapshots.py | 4 + 6 files changed, 282 insertions(+), 27 deletions(-) create mode 100644 tests/snapshot_tests/snapshot_apps/vis.html create mode 100644 tests/snapshot_tests/snapshot_apps/visibility.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 89140e36d..013ff9916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Type selectors can now contain numbers https://github.com/Textualize/textual/issues/1253 +- Fixed visibility not affecting children https://github.com/Textualize/textual/issues/1313 ## [0.5.0] - 2022-11-20 diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index ab9d69eca..b59710218 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -347,6 +347,7 @@ class Compositor: order: tuple[tuple[int, ...], ...], layer_order: int, clip: Region, + visible: bool, ) -> None: """Called recursively to place a widget and its children in the map. @@ -356,7 +357,12 @@ class Compositor: order (tuple[int, ...]): A tuple of ints to define the order. clip (Region): The clipping region (i.e. the viewport which contains it). """ - widgets.add(widget) + visibility = widget.styles.get_rule("visibility") + if visibility is not None: + visible = visibility == "visible" + + if visible: + widgets.add(widget) styles_offset = widget.styles.offset layout_offset = ( styles_offset.resolve(region.size, clip.size) @@ -420,32 +426,34 @@ class Compositor: widget_order, layer_order, sub_clip, + visible, ) layer_order -= 1 - # Add any scrollbars - for chrome_widget, chrome_region in widget._arrange_scrollbars( - container_region - ): - map[chrome_widget] = MapGeometry( - chrome_region + layout_offset, + if visible: + # Add any scrollbars + for chrome_widget, chrome_region in widget._arrange_scrollbars( + container_region + ): + map[chrome_widget] = MapGeometry( + chrome_region + layout_offset, + order, + clip, + container_size, + container_size, + chrome_region, + ) + + map[widget] = MapGeometry( + region + layout_offset, order, clip, + total_region.size, container_size, - container_size, - chrome_region, + virtual_region, ) - map[widget] = MapGeometry( - region + layout_offset, - order, - clip, - total_region.size, - container_size, - virtual_region, - ) - - else: + elif visible: # Add the widget to the map map[widget] = MapGeometry( region + layout_offset, @@ -457,7 +465,15 @@ class Compositor: ) # Add top level (root) widget - add_widget(root, size.region, size.region, ((0,),), layer_order, size.region) + add_widget( + root, + size.region, + size.region, + ((0,),), + layer_order, + size.region, + True, + ) return map, widgets @property @@ -630,11 +646,6 @@ class Compositor: if not self.map: return - def is_visible(widget: Widget) -> bool: - """Return True if the widget is (literally) visible by examining various - properties which affect whether it can be seen or not.""" - return widget.visible and widget.styles.opacity > 0 - _Region = Region visible_widgets = self.visible_widgets @@ -644,13 +655,13 @@ class Compositor: widget_regions = [ (widget, region, clip) for widget, (region, clip) in visible_widgets.items() - if crop_overlaps(clip) and is_visible(widget) + if crop_overlaps(clip) and widget.styles.opacity > 0 ] else: widget_regions = [ (widget, region, clip) for widget, (region, clip) in visible_widgets.items() - if is_visible(widget) + if widget.styles.opacity > 0 ] intersection = _Region.intersection diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr index c2ae2ab5f..53db5e9ec 100644 --- a/tests/snapshot_tests/__snapshots__/test_snapshots.ambr +++ b/tests/snapshot_tests/__snapshots__/test_snapshots.ambr @@ -7422,3 +7422,160 @@ ''' # --- +# name: test_visibility + ''' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Visibility + + + + + + + + + + ┌──────────────────────────────────────┐ + bar + ┌────────────────────────────────────┐┌────────────────────────────────────┐ + floatfloat + └────────────────────────────────────┘└────────────────────────────────────┘ + + + + + + + + + + + + + + + + + + + └──────────────────────────────────────┘ + + + + + ''' +# --- diff --git a/tests/snapshot_tests/snapshot_apps/vis.html b/tests/snapshot_tests/snapshot_apps/vis.html new file mode 100644 index 000000000..4e4ac19e1 --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/vis.html @@ -0,0 +1,34 @@ + + + + + + + + +
+
+
+ Hello, World + +
+ +
+ + +
+ + + + diff --git a/tests/snapshot_tests/snapshot_apps/visibility.py b/tests/snapshot_tests/snapshot_apps/visibility.py new file mode 100644 index 000000000..a5ccccb7e --- /dev/null +++ b/tests/snapshot_tests/snapshot_apps/visibility.py @@ -0,0 +1,48 @@ +from textual.app import App +from textual.containers import Vertical +from textual.widgets import Static + + +class Visibility(App): + """Check that visibility: hidden also makes children invisible;""" + + CSS = """ + Screen { + layout: horizontal; + } + Vertical { + width: 1fr; + border: solid red; + } + + #container1 { + visibility: hidden; + } + + .float { + border: solid blue; + } + + /* Make a child of a hidden widget visible again */ + #container1 .float { + visibility: visible; + } + """ + + def compose(self): + + yield Vertical( + Static("foo"), + Static("float", classes="float"), + id="container1", + ) + yield Vertical( + Static("bar"), + Static("float", classes="float"), + id="container2", + ) + + +if __name__ == "__main__": + app = Visibility() + app.run() diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index 338d3fb5d..7efcd3b2a 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -100,6 +100,10 @@ def test_fr_units(snap_compare): assert snap_compare("snapshot_apps/fr_units.py") +def test_visibility(snap_compare): + assert snap_compare("snapshot_apps/visibility.py") + + def test_tree_example(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "tree.py") From a25a60efbf752c5ee78aef1052e7bafa0443bd65 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 7 Dec 2022 15:01:50 +0000 Subject: [PATCH 28/29] remove html test --- tests/snapshot_tests/snapshot_apps/vis.html | 34 --------------------- 1 file changed, 34 deletions(-) delete mode 100644 tests/snapshot_tests/snapshot_apps/vis.html diff --git a/tests/snapshot_tests/snapshot_apps/vis.html b/tests/snapshot_tests/snapshot_apps/vis.html deleted file mode 100644 index 4e4ac19e1..000000000 --- a/tests/snapshot_tests/snapshot_apps/vis.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - -
-
-
- Hello, World - -
- -
- - -
- - - - From dd8a9562d08374732f52a955ab319af1f8bf4810 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Wed, 7 Dec 2022 16:02:51 +0000 Subject: [PATCH 29/29] fix for container height change --- docs/examples/guide/dom4.css | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/examples/guide/dom4.css b/docs/examples/guide/dom4.css index d9169ee17..8ac843abb 100644 --- a/docs/examples/guide/dom4.css +++ b/docs/examples/guide/dom4.css @@ -1,6 +1,7 @@ /* The top level dialog (a Container) */ #dialog { + height: 100%; margin: 4 8; background: $panel; color: $text;