Added compose event, more color docs

This commit is contained in:
Will McGugan
2022-09-12 12:47:14 +01:00
parent 32079fc8f7
commit 4a459dbd20
12 changed files with 165 additions and 28 deletions

View File

@@ -0,0 +1,17 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
class WidgetApp(App):
def compose(self) -> ComposeResult:
self.widget = Static("Textual")
yield self.widget
def on_mount(self) -> None:
self.widget.styles.background = "darkblue"
self.widget.styles.border = ("heavy", "white")
app = WidgetApp()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,24 @@
from textual.app import App, ComposeResult
from textual.color import Color
from textual.widgets import Static
class ColorApp(App):
def compose(self) -> ComposeResult:
self.widget1 = Static("Textual One")
yield self.widget1
self.widget2 = Static("Textual Two")
yield self.widget2
self.widget3 = Static("Textual Three")
yield self.widget3
def on_mount(self) -> None:
self.widget1.styles.background = "#9932CC"
self.widget2.styles.background = "hsl(150,42.9%,49.4%)"
self.widget2.styles.color = "blue"
self.widget3.styles.background = Color(191, 78, 96)
app = ColorApp()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,18 @@
from textual.app import App, ComposeResult
from textual.color import Color
from textual.widgets import Static
class ColorApp(App):
def compose(self) -> ComposeResult:
self.widgets = [Static(f"Textual {n+1}") for n in range(10)]
yield from self.widgets
def on_mount(self) -> None:
for index, widget in enumerate(self.widgets, 1):
widget.styles.background = Color(191, 78, 96, index * 0.1)
app = ColorApp()
if __name__ == "__main__":
app.run()

View File

@@ -0,0 +1,17 @@
from textual.app import App, ComposeResult
from textual.widgets import Static
class WidgetApp(App):
def compose(self) -> ComposeResult:
self.widget = Static("Textual")
yield self.widget
def on_mount(self) -> None:
self.widget.styles.background = "darkblue"
self.widget.styles.border = ("heavy", "white")
app = WidgetApp()
if __name__ == "__main__":
app.run()

View File

@@ -28,27 +28,79 @@ The second line sets [border](../styles/border.md) to a tuple of `("heavy", "whi
## Styling widgets
The screen is a special-case for a widget, in that it always matches the dimensions of your terminal window. Widgets provide more flexibility for styling.
The following example adds a static widget which we will apply some styles to:
```python title="widget.py" hl_lines="11-12"
--8<-- "docs/examples/guide/styles/widget.py"
```
The compose method stores a reference to the widget before yielding it. In our mount handler we can use that reference to set some styles. We set the same styles as the screen example, but this time on the widget. Here is the result:
```{.textual path="docs/examples/guide/styles/widget.py"}
```
Widgets will occupy the full width of the screen and as many lines as required to fit in the vertical direction, which is why we see a wide box on the top of the screen.
Note how the combined height of the widget is three rows (lines) in the terminal. This is because a border adds two rows (and two column). If you were to remove the line that sets the border style, the widget would occupy only a single row.
Widgets will also wrap text by default. If you were to replace `"Textual"` with a long paragraph of text, it will wrap lines and the widget will extend downwards.
## Colors
There are a number of style attribute which accept colors. The most commonly used arel be [color](../styles/color.md) which sets the default color of text on a widget, and [background](..styles/background/md) which sets the background color (under the text).
There are a number of style attribute which accept colors. The most commonly used are [color](../styles/color.md) which sets the default color of text on a widget, and [background](..styles/background/md) which sets the background color (under the text).
You can set a color value to one of a number of pre-defined color constants, such as "crimson", "lime", and "palegreen". You can find a full list in the [Color reference](../reference/color.md#textual.color--named-colors).
Here's how you would set the screen background to lime:
```python
self.screen.background = "lime"
widget.styles.background = "lime"
```
In addition to color names, there are other ways you can specify a color in Textual (which give you access to all 16.7 million colors a typical monitor can display).
In addition to color names, you can also use any of the following ways of expressing a color:
- RGB hex colors starts with a `#` followed by three pairs of one or two hex digits; one for the red, green, and blue color components. For example, `#f00` is an intense red color, and `#9932CC` is *dark orchid*.
- RGB decimal color start with `rgb` followed by a tuple of three numbers in the range 0 to 255. For example `rgb(255,0,0)` is intense red, and `rgb(153,50,204)` is *dark orchid*.
- HSL colors start with `hsl` followed by a tuple of three colors in the range 0 to 255, representing the Hue Saturation and Lightness. For example `hsl(0,100%,50%)` is intense red and `hsl(280,60%,49%)` is *dark orchid*
The background and color styles will also accept a [color][textual.color.Color] object which is convenient if you want to create colors dynamically.
The following example adds three widgets and sets color styles.
```python title="colors01.py" hl_lines="16-19"
--8<-- "docs/examples/guide/styles/colors01.py"
```
Here is the output:
```{.textual path="docs/examples/guide/styles/colors01.py"}
```
### Alpha component
Textual (and computers in general) represents color as a tuple of three values for the red, green, and blue components which when combined create all of the 16.7 million colors a typical computer screen can display.
Textual support a common fourth value called *alpha* can be thought of as how transparent a color is. If you set this value on a background color then Textual will blend the background color with the background underneath it. If you set alpha on the text, then it will blend the text with its background.
There are a few ways you can set alpha on a color in Textual.
- You can set the alpha value of a color by adding a fourth pair (or digit) to a hex color. The extra digits set an opacity of 0 for completely transparent to 255 (completely opaque). Any values between 0 and 255 will be translucent. For example `"#9932CC7f"` is a dark orchid which is roughly 50% translucent.
- You can set alpha with the `rgba` format, which is identical to `rgb` with the additional of a fourth value that should be between 0 and 1, where 0 is invisible and 1 is opaque.
- You can add a fourth value to a [Color][textual.color.Color] object. For example `Color(192, 78, 96, 0.5)` creates a translucent dark orchid.
The following examples shows what happens when you set alpha on background colors:
```python title="colors01.py" hl_lines="12-13"
--8<-- "docs/examples/guide/styles/colors02.py"
```
We set the `background` style to a color with an alpha that ranges from 0.1 to 1. Notice how with an alpha of 0.1 the background almost matches the screen, but at 1.0 it is a solid color.
```{.textual path="docs/examples/guide/styles/colors02.py"}
```
## Box Model

View File

@@ -1087,6 +1087,8 @@ class App(Generic[ReturnType], DOMNode):
process_messages = super()._process_messages
async def run_process_messages():
compose_event = events.Compose(sender=self)
await self._dispatch_message(compose_event)
mount_event = events.Mount(sender=self)
await self._dispatch_message(mount_event)
@@ -1156,10 +1158,9 @@ class App(Generic[ReturnType], DOMNode):
self.set_timer(screenshot_timer, on_screenshot, name="screenshot timer")
def _on_mount(self) -> None:
def _on_compose(self) -> None:
widgets = self.compose()
if widgets:
self.mount_all(widgets)
self.mount_all(widgets)
def _on_idle(self) -> None:
"""Perform actions when there are no messages in the queue."""
@@ -1338,7 +1339,7 @@ class App(Generic[ReturnType], DOMNode):
async def on_event(self, event: events.Event) -> None:
# Handle input events that haven't been forwarded
# If the event has been forwarded it may have bubbled up back to the App
if isinstance(event, events.Mount):
if isinstance(event, events.Compose):
screen = Screen(id="_default")
self._register(self, screen)
self._screen_stack.append(screen)

View File

@@ -57,20 +57,20 @@ from .geometry import clamp
_TRUECOLOR = ColorType.TRUECOLOR
class HLS(NamedTuple):
class HSL(NamedTuple):
"""A color in HLS format."""
h: float
"""Hue"""
l: float
"""Lightness"""
s: float
"""Saturation"""
l: float
"""Lightness"""
@property
def css(self) -> str:
"""HLS in css format."""
h, l, s = self
"""HSL in css format."""
h, s, l = self
def as_str(number: float) -> str:
return f"{number:.1f}".rstrip("0").rstrip(".")
@@ -161,7 +161,7 @@ class Color(NamedTuple):
return cls(r, g, b)
@classmethod
def from_hls(cls, h: float, l: float, s: float) -> Color:
def from_hsl(cls, h: float, s: float, l: float) -> Color:
"""Create a color from HLS components.
Args:
@@ -246,14 +246,15 @@ class Color(NamedTuple):
return (r, g, b)
@property
def hls(self) -> HLS:
"""Get the color as HLS.
def hsl(self) -> HSL:
"""Get the color as HSL.
Returns:
HLS: Color in HLS format.
HSL: Color in HSL format.
"""
r, g, b = self.normalized
return HLS(*rgb_to_hls(r, g, b))
h, l, s = rgb_to_hls(r, g, b)
return HSL(h, s, l)
@property
def brightness(self) -> float:
@@ -438,14 +439,14 @@ class Color(NamedTuple):
h = float(h) % 360 / 360
s = percentage_string_to_float(s)
l = percentage_string_to_float(l)
color = Color.from_hls(h, l, s)
color = Color.from_hsl(h, s, l)
elif hsla is not None:
h, s, l, a = hsla.split(",")
h = float(h) % 360 / 360
s = percentage_string_to_float(s)
l = percentage_string_to_float(l)
a = clamp(float(a), 0.0, 1.0)
color = Color.from_hls(h, l, s).with_alpha(a)
color = Color.from_hsl(h, s, l).with_alpha(a)
else:
raise AssertionError("Can't get here if RE_COLOR matches")
return color

View File

@@ -15,7 +15,7 @@ NUMBER_OF_SHADES = 3
# Where no content exists
DEFAULT_DARK_BACKGROUND = "#000000"
# What text usually goes on top off
DEFAULT_DARK_SURFACE = "#292929"
DEFAULT_DARK_SURFACE = "#121212"
DEFAULT_LIGHT_SURFACE = "#f5f5f5"
DEFAULT_LIGHT_BACKGROUND = "#efefef"

View File

@@ -116,6 +116,10 @@ class Resize(Event, bubble=False):
yield "container_size", self.container_size, self.size
class Compose(Event, bubble=False, verbose=True):
"""Sent to a widget to request it to compose and mount children."""
class Mount(Event, bubble=False, verbose=True):
"""Sent when a widget is *mounted* and may receive messages."""

View File

@@ -1497,6 +1497,10 @@ class Widget(DOMNode):
else:
await self.action(binding.action)
def _on_compose(self, event: events.Compose) -> None:
widgets = self.compose()
self.app.mount_all(widgets)
def _on_mount(self, event: events.Mount) -> None:
widgets = self.compose()
self.mount(*widgets)

View File

@@ -30,8 +30,8 @@ class Welcome(Static):
Welcome {
width: 100%;
height: 100%;
margin: 1 2;
padding: 1 2;
background: $surface;
}
Welcome Container {
@@ -53,6 +53,5 @@ class Welcome(Static):
"""
def compose(self) -> ComposeResult:
yield Container(Static(Markdown(WELCOME_MD), id="text"), id="md")
yield Button("OK", id="close", variant="success")

View File

@@ -38,12 +38,12 @@ def test_rgb():
def test_hls():
red = Color(200, 20, 32)
print(red.hls)
assert red.hls == pytest.approx(
(0.9888888888888889, 0.43137254901960786, 0.818181818181818)
print(red.hsl)
assert red.hsl == pytest.approx(
(0.9888888888888889, 0.818181818181818, 0.43137254901960786)
)
assert Color.from_hls(
0.9888888888888889, 0.43137254901960786, 0.818181818181818
assert Color.from_hsl(
0.9888888888888889, 0.818181818181818, 0.43137254901960786
).normalized == pytest.approx(red.normalized, rel=1e-5)