|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | | -from rich import box |
4 | | -from rich.align import Align |
5 | | -from rich.console import RenderableType |
6 | | -from rich.panel import Panel |
7 | | -from rich.pretty import Pretty |
8 | | -import rich.repr |
9 | | -from rich.style import Style |
| 3 | +from itertools import cycle |
10 | 4 |
|
11 | 5 | from .. import events |
12 | | -from ..reactive import Reactive |
13 | | -from ..widget import Widget |
| 6 | +from ..containers import Container |
| 7 | +from ..css._error_tools import friendly_list |
| 8 | +from ..reactive import Reactive, reactive |
| 9 | +from ..widget import Widget, RenderResult |
| 10 | +from ..widgets import Label |
| 11 | +from .._typing import Literal |
14 | 12 |
|
| 13 | +PlaceholderVariant = Literal["default", "size", "text"] |
| 14 | +_VALID_PLACEHOLDER_VARIANTS_ORDERED: list[PlaceholderVariant] = [ |
| 15 | + "default", |
| 16 | + "size", |
| 17 | + "text", |
| 18 | +] |
| 19 | +_VALID_PLACEHOLDER_VARIANTS: set[PlaceholderVariant] = set( |
| 20 | + _VALID_PLACEHOLDER_VARIANTS_ORDERED |
| 21 | +) |
| 22 | +_PLACEHOLDER_BACKGROUND_COLORS = [ |
| 23 | + "#881177", |
| 24 | + "#aa3355", |
| 25 | + "#cc6666", |
| 26 | + "#ee9944", |
| 27 | + "#eedd00", |
| 28 | + "#99dd55", |
| 29 | + "#44dd88", |
| 30 | + "#22ccbb", |
| 31 | + "#00bbcc", |
| 32 | + "#0099cc", |
| 33 | + "#3366bb", |
| 34 | + "#663399", |
| 35 | +] |
| 36 | +_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." |
15 | 37 |
|
16 | | -@rich.repr.auto(angular=False) |
17 | | -class Placeholder(Widget, can_focus=True): |
18 | 38 |
|
19 | | - has_focus: Reactive[bool] = Reactive(False) |
20 | | - mouse_over: Reactive[bool] = Reactive(False) |
| 39 | +class InvalidPlaceholderVariant(Exception): |
| 40 | + pass |
| 41 | + |
| 42 | + |
| 43 | +class _PlaceholderLabel(Widget): |
| 44 | + def __init__(self, content, classes) -> None: |
| 45 | + super().__init__(classes=classes) |
| 46 | + self._content = content |
| 47 | + |
| 48 | + def render(self) -> RenderResult: |
| 49 | + return self._content |
| 50 | + |
| 51 | + |
| 52 | +class Placeholder(Container): |
| 53 | + """A simple placeholder widget to use before you build your custom widgets. |
| 54 | +
|
| 55 | + This placeholder has a couple of variants that show different data. |
| 56 | + Clicking the placeholder cycles through the available variants, but a placeholder |
| 57 | + can also be initialised in a specific variant. |
| 58 | +
|
| 59 | + The variants available are: |
| 60 | + default: shows an identifier label or the ID of the placeholder. |
| 61 | + size: shows the size of the placeholder. |
| 62 | + text: shows some Lorem Ipsum text on the placeholder. |
| 63 | + """ |
| 64 | + |
| 65 | + DEFAULT_CSS = """ |
| 66 | + Placeholder { |
| 67 | + align: center middle; |
| 68 | + } |
| 69 | +
|
| 70 | + Placeholder.-text { |
| 71 | + padding: 1; |
| 72 | + } |
| 73 | +
|
| 74 | + _PlaceholderLabel { |
| 75 | + height: auto; |
| 76 | + } |
| 77 | +
|
| 78 | + Placeholder > _PlaceholderLabel { |
| 79 | + content-align: center middle; |
| 80 | + } |
| 81 | +
|
| 82 | + Placeholder.-default > _PlaceholderLabel.-size, |
| 83 | + Placeholder.-default > _PlaceholderLabel.-text, |
| 84 | + Placeholder.-size > _PlaceholderLabel.-default, |
| 85 | + Placeholder.-size > _PlaceholderLabel.-text, |
| 86 | + Placeholder.-text > _PlaceholderLabel.-default, |
| 87 | + Placeholder.-text > _PlaceholderLabel.-size { |
| 88 | + display: none; |
| 89 | + } |
| 90 | +
|
| 91 | + Placeholder.-default > _PlaceholderLabel.-default, |
| 92 | + Placeholder.-size > _PlaceholderLabel.-size, |
| 93 | + Placeholder.-text > _PlaceholderLabel.-text { |
| 94 | + display: block; |
| 95 | + } |
| 96 | + """ |
| 97 | + # Consecutive placeholders get assigned consecutive colors. |
| 98 | + _COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) |
| 99 | + _SIZE_RENDER_TEMPLATE = "[b]{} x {}[/b]" |
| 100 | + |
| 101 | + variant: Reactive[PlaceholderVariant] = reactive("default") |
| 102 | + |
| 103 | + @classmethod |
| 104 | + def reset_color_cycle(cls) -> None: |
| 105 | + """Reset the placeholder background color cycle.""" |
| 106 | + cls._COLORS = cycle(_PLACEHOLDER_BACKGROUND_COLORS) |
21 | 107 |
|
22 | 108 | def __init__( |
23 | | - # parent class constructor signature: |
24 | 109 | self, |
25 | | - *children: Widget, |
| 110 | + label: str | None = None, |
| 111 | + variant: PlaceholderVariant = "default", |
| 112 | + *, |
26 | 113 | name: str | None = None, |
27 | 114 | id: str | None = None, |
28 | 115 | classes: str | None = None, |
29 | | - # ...and now for our own class specific params: |
30 | | - title: str | None = None, |
31 | 116 | ) -> None: |
32 | | - super().__init__(*children, name=name, id=id, classes=classes) |
33 | | - self.title = title |
34 | | - |
35 | | - def __rich_repr__(self) -> rich.repr.Result: |
36 | | - yield from super().__rich_repr__() |
37 | | - yield "has_focus", self.has_focus, False |
38 | | - yield "mouse_over", self.mouse_over, False |
39 | | - |
40 | | - def render(self) -> RenderableType: |
41 | | - # Apply colours only inside render_styled |
42 | | - # Pass the full RICH style object into `render` - not the `Styles` |
43 | | - return Panel( |
44 | | - Align.center( |
45 | | - Pretty(self, no_wrap=True, overflow="ellipsis"), |
46 | | - vertical="middle", |
47 | | - ), |
48 | | - title=self.title or self.__class__.__name__, |
49 | | - border_style="green" if self.mouse_over else "blue", |
50 | | - box=box.HEAVY if self.has_focus else box.ROUNDED, |
| 117 | + """Create a Placeholder widget. |
| 118 | +
|
| 119 | + Args: |
| 120 | + label (str | None, optional): The label to identify the placeholder. |
| 121 | + If no label is present, uses the placeholder ID instead. Defaults to None. |
| 122 | + variant (PlaceholderVariant, optional): The variant of the placeholder. |
| 123 | + Defaults to "default". |
| 124 | + name (str | None, optional): The name of the placeholder. Defaults to None. |
| 125 | + id (str | None, optional): The ID of the placeholder in the DOM. |
| 126 | + Defaults to None. |
| 127 | + classes (str | None, optional): A space separated string with the CSS classes |
| 128 | + of the placeholder, if any. Defaults to None. |
| 129 | + """ |
| 130 | + # Create and cache labels for all the variants. |
| 131 | + self._default_label = _PlaceholderLabel( |
| 132 | + label if label else f"#{id}" if id else "Placeholder", |
| 133 | + "-default", |
| 134 | + ) |
| 135 | + self._size_label = _PlaceholderLabel( |
| 136 | + "", |
| 137 | + "-size", |
| 138 | + ) |
| 139 | + self._text_label = _PlaceholderLabel( |
| 140 | + _LOREM_IPSUM_PLACEHOLDER_TEXT, |
| 141 | + "-text", |
51 | 142 | ) |
| 143 | + super().__init__( |
| 144 | + self._default_label, |
| 145 | + self._size_label, |
| 146 | + self._text_label, |
| 147 | + name=name, |
| 148 | + id=id, |
| 149 | + classes=classes, |
| 150 | + ) |
| 151 | + |
| 152 | + self.styles.background = f"{next(Placeholder._COLORS)} 70%" |
| 153 | + |
| 154 | + self.variant = self.validate_variant(variant) |
| 155 | + # Set a cycle through the variants with the correct starting point. |
| 156 | + self._variants_cycle = cycle(_VALID_PLACEHOLDER_VARIANTS_ORDERED) |
| 157 | + while next(self._variants_cycle) != self.variant: |
| 158 | + pass |
52 | 159 |
|
53 | | - async def on_focus(self, event: events.Focus) -> None: |
54 | | - self.has_focus = True |
| 160 | + def cycle_variant(self) -> None: |
| 161 | + """Get the next variant in the cycle.""" |
| 162 | + self.variant = next(self._variants_cycle) |
| 163 | + |
| 164 | + def watch_variant( |
| 165 | + self, old_variant: PlaceholderVariant, variant: PlaceholderVariant |
| 166 | + ) -> None: |
| 167 | + self.remove_class(f"-{old_variant}") |
| 168 | + self.add_class(f"-{variant}") |
55 | 169 |
|
56 | | - async def on_blur(self, event: events.Blur) -> None: |
57 | | - self.has_focus = False |
| 170 | + def validate_variant(self, variant: PlaceholderVariant) -> PlaceholderVariant: |
| 171 | + """Validate the variant to which the placeholder was set.""" |
| 172 | + if variant not in _VALID_PLACEHOLDER_VARIANTS: |
| 173 | + raise InvalidPlaceholderVariant( |
| 174 | + "Valid placeholder variants are " |
| 175 | + + f"{friendly_list(_VALID_PLACEHOLDER_VARIANTS)}" |
| 176 | + ) |
| 177 | + return variant |
58 | 178 |
|
59 | | - async def on_enter(self, event: events.Enter) -> None: |
60 | | - self.mouse_over = True |
| 179 | + def on_click(self) -> None: |
| 180 | + """Click handler to cycle through the placeholder variants.""" |
| 181 | + self.cycle_variant() |
61 | 182 |
|
62 | | - async def on_leave(self, event: events.Leave) -> None: |
63 | | - self.mouse_over = False |
| 183 | + def on_resize(self, event: events.Resize) -> None: |
| 184 | + """Update the placeholder "size" variant with the new placeholder size.""" |
| 185 | + self._size_label._content = self._SIZE_RENDER_TEMPLATE.format(*self.size) |
| 186 | + if self.variant == "size": |
| 187 | + self._size_label.refresh(layout=True) |
0 commit comments