mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' of github.com:Textualize/textual into easing-examples
This commit is contained in:
@@ -4,18 +4,18 @@
|
||||
|
||||
* {
|
||||
transition: color 300ms linear, background 300ms linear;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
*:hover {
|
||||
/* tint: 30% red;
|
||||
/* tint: 30% red;
|
||||
/* outline: heavy red; */
|
||||
}
|
||||
|
||||
App > Screen {
|
||||
|
||||
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
color: $text-surface;
|
||||
layers: base sidebar;
|
||||
|
||||
color: $text-background;
|
||||
@@ -23,12 +23,12 @@ App > Screen {
|
||||
layout: vertical;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
}
|
||||
|
||||
#tree-container {
|
||||
overflow-y: auto;
|
||||
height: 20;
|
||||
height: 20;
|
||||
margin: 1 3;
|
||||
background: $panel;
|
||||
padding: 1 2;
|
||||
@@ -37,7 +37,7 @@ App > Screen {
|
||||
DirectoryTree {
|
||||
padding: 0 1;
|
||||
height: auto;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,10 @@ DirectoryTree {
|
||||
DataTable {
|
||||
/*border:heavy red;*/
|
||||
/* tint: 10% green; */
|
||||
/* opacity: 50%; */
|
||||
/* text-opacity: 50%; */
|
||||
padding: 1;
|
||||
margin: 1 2;
|
||||
height: 24;
|
||||
margin: 1 2;
|
||||
height: 24;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
@@ -59,7 +59,7 @@ DataTable {
|
||||
width: 30;
|
||||
margin-bottom: 1;
|
||||
offset-x: -100%;
|
||||
|
||||
|
||||
transition: offset 500ms in_out_cubic;
|
||||
layer: sidebar;
|
||||
}
|
||||
@@ -97,8 +97,8 @@ DataTable {
|
||||
Tweet {
|
||||
height:12;
|
||||
width: 100%;
|
||||
margin: 0 2;
|
||||
|
||||
margin: 0 2;
|
||||
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
layout: vertical;
|
||||
@@ -121,9 +121,9 @@ Tweet {
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
.code {
|
||||
.code {
|
||||
height: auto;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -133,12 +133,12 @@ TweetHeader {
|
||||
color: $text-accent
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
TweetBody {
|
||||
width: 100%;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
}
|
||||
|
||||
Tweet.scroll-horizontal TweetBody {
|
||||
@@ -158,7 +158,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
/* padding: 1 0 0 0 ; */
|
||||
|
||||
transition: background 400ms in_out_cubic, color 400ms in_out_cubic;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
@@ -178,7 +178,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
color: $text-accent;
|
||||
background: $accent;
|
||||
height: 1;
|
||||
|
||||
|
||||
content-align: center middle;
|
||||
dock:bottom;
|
||||
}
|
||||
@@ -213,7 +213,7 @@ Error {
|
||||
color: $text-error;
|
||||
border-top: tall $error-darken-2;
|
||||
border-bottom: tall $error-darken-2;
|
||||
|
||||
|
||||
padding: 0;
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
@@ -226,21 +226,21 @@ Warning {
|
||||
color: $text-warning-fade-1;
|
||||
border-top: tall $warning-darken-2;
|
||||
border-bottom: tall $warning-darken-2;
|
||||
|
||||
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
Success {
|
||||
width: 100%;
|
||||
|
||||
height:auto;
|
||||
|
||||
height:auto;
|
||||
box-sizing: border-box;
|
||||
background: $success;
|
||||
color: $text-success-fade-1;
|
||||
|
||||
color: $text-success-fade-1;
|
||||
|
||||
border-top: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
|
||||
text-style: bold ;
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@ Stopwatch {
|
||||
|
||||
TimeDisplay {
|
||||
content-align: center middle;
|
||||
opacity: 60%;
|
||||
text-opacity: 60%;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 16;
|
||||
width: 16;
|
||||
}
|
||||
|
||||
#start {
|
||||
@@ -30,14 +30,14 @@ Button {
|
||||
dock: right;
|
||||
}
|
||||
|
||||
.started {
|
||||
.started {
|
||||
text-style: bold;
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
}
|
||||
|
||||
.started TimeDisplay {
|
||||
opacity: 100%;
|
||||
text-opacity: 100%;
|
||||
}
|
||||
|
||||
.started #start {
|
||||
|
||||
@@ -8,12 +8,12 @@ Stopwatch {
|
||||
|
||||
TimeDisplay {
|
||||
content-align: center middle;
|
||||
opacity: 60%;
|
||||
text-opacity: 60%;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 16;
|
||||
width: 16;
|
||||
}
|
||||
|
||||
#start {
|
||||
|
||||
@@ -9,12 +9,12 @@ Stopwatch {
|
||||
|
||||
TimeDisplay {
|
||||
content-align: center middle;
|
||||
opacity: 60%;
|
||||
text-opacity: 60%;
|
||||
height: 3;
|
||||
}
|
||||
|
||||
Button {
|
||||
width: 16;
|
||||
width: 16;
|
||||
}
|
||||
|
||||
#start {
|
||||
@@ -30,14 +30,14 @@ Button {
|
||||
dock: right;
|
||||
}
|
||||
|
||||
.started {
|
||||
.started {
|
||||
text-style: bold;
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
}
|
||||
|
||||
.started TimeDisplay {
|
||||
opacity: 100%;
|
||||
text-opacity: 100%;
|
||||
}
|
||||
|
||||
.started #start {
|
||||
|
||||
31
docs/examples/styles/opacity.css
Normal file
31
docs/examples/styles/opacity.css
Normal file
@@ -0,0 +1,31 @@
|
||||
#zero-opacity {
|
||||
opacity: 0%;
|
||||
}
|
||||
|
||||
#quarter-opacity {
|
||||
opacity: 25%;
|
||||
}
|
||||
|
||||
#half-opacity {
|
||||
opacity: 50%;
|
||||
}
|
||||
|
||||
#three-quarter-opacity {
|
||||
opacity: 75%;
|
||||
}
|
||||
|
||||
#full-opacity {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
Screen {
|
||||
background: antiquewhite;
|
||||
}
|
||||
|
||||
Static {
|
||||
height: 1fr;
|
||||
border: outer dodgerblue;
|
||||
background: lightseagreen;
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
}
|
||||
14
docs/examples/styles/opacity.py
Normal file
14
docs/examples/styles/opacity.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from textual.app import App
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class OpacityApp(App):
|
||||
def compose(self):
|
||||
yield Static("opacity: 0%", id="zero-opacity")
|
||||
yield Static("opacity: 25%", id="quarter-opacity")
|
||||
yield Static("opacity: 50%", id="half-opacity")
|
||||
yield Static("opacity: 75%", id="three-quarter-opacity")
|
||||
yield Static("opacity: 100%", id="full-opacity")
|
||||
|
||||
|
||||
app = OpacityApp(css_path="opacity.css")
|
||||
25
docs/examples/styles/text_opacity.css
Normal file
25
docs/examples/styles/text_opacity.css
Normal file
@@ -0,0 +1,25 @@
|
||||
#zero-opacity {
|
||||
text-opacity: 0%;
|
||||
}
|
||||
|
||||
#quarter-opacity {
|
||||
text-opacity: 25%;
|
||||
}
|
||||
|
||||
#half-opacity {
|
||||
text-opacity: 50%;
|
||||
}
|
||||
|
||||
#three-quarter-opacity {
|
||||
text-opacity: 75%;
|
||||
}
|
||||
|
||||
#full-opacity {
|
||||
text-opacity: 100%;
|
||||
}
|
||||
|
||||
Static {
|
||||
height: 1fr;
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
}
|
||||
14
docs/examples/styles/text_opacity.py
Normal file
14
docs/examples/styles/text_opacity.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from textual.app import App
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class TextOpacityApp(App):
|
||||
def compose(self):
|
||||
yield Static("text-opacity: 0%", id="zero-opacity")
|
||||
yield Static("text-opacity: 25%", id="quarter-opacity")
|
||||
yield Static("text-opacity: 50%", id="half-opacity")
|
||||
yield Static("text-opacity: 75%", id="three-quarter-opacity")
|
||||
yield Static("text-opacity: 100%", id="full-opacity")
|
||||
|
||||
|
||||
app = TextOpacityApp(css_path="text_opacity.css")
|
||||
54
docs/styles/opacity.md
Normal file
54
docs/styles/opacity.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Opacity
|
||||
|
||||
The `opacity` property can be used to make a widget partially or fully transparent.
|
||||
|
||||
|
||||
## Syntax
|
||||
|
||||
```
|
||||
opacity: <FRACTIONAL>;
|
||||
```
|
||||
|
||||
### Values
|
||||
|
||||
As a fractional property, `opacity` can be set to either a float (between 0 and 1),
|
||||
or a percentage, e.g. `45%`.
|
||||
Float values will be clamped between 0 and 1.
|
||||
Percentage values will be clamped between 0% and 100%.
|
||||
|
||||
## Example
|
||||
|
||||
This example shows, from top to bottom, increasing opacity values.
|
||||
|
||||
=== "opacity.py"
|
||||
|
||||
```python
|
||||
--8<-- "docs/examples/styles/opacity.py"
|
||||
```
|
||||
|
||||
=== "opacity.css"
|
||||
|
||||
```scss
|
||||
--8<-- "docs/examples/styles/opacity.css"
|
||||
```
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/styles/opacity.py"}
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
```sass
|
||||
/* Fade the widget to 50% against its parent's background */
|
||||
Widget {
|
||||
opacity: 50%;
|
||||
}
|
||||
```
|
||||
|
||||
## Python
|
||||
|
||||
```python
|
||||
# Fade the widget to 50% against its parent's background
|
||||
widget.styles.opacity = "50%"
|
||||
```
|
||||
53
docs/styles/text_opacity.md
Normal file
53
docs/styles/text_opacity.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Text-opacity
|
||||
|
||||
The `text-opacity` blends the color of the content of a widget with the color of the background.
|
||||
|
||||
## Syntax
|
||||
|
||||
```
|
||||
text-opacity: <FRACTIONAL>;
|
||||
```
|
||||
|
||||
### Values
|
||||
|
||||
As a fractional property, `text-opacity` can be set to either a float (between 0 and 1),
|
||||
or a percentage, e.g. `45%`.
|
||||
Float values will be clamped between 0 and 1.
|
||||
Percentage values will be clamped between 0% and 100%.
|
||||
|
||||
## Example
|
||||
|
||||
This example shows, from top to bottom, increasing text-opacity values.
|
||||
|
||||
=== "text_opacity.py"
|
||||
|
||||
```python
|
||||
--8<-- "docs/examples/styles/text_opacity.py"
|
||||
```
|
||||
|
||||
=== "text_opacity.css"
|
||||
|
||||
```css
|
||||
--8<-- "docs/examples/styles/text_opacity.css"
|
||||
```
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/styles/text_opacity.py"}
|
||||
```
|
||||
|
||||
## CSS
|
||||
|
||||
```sass
|
||||
/* Set the text to be "half-faded" against the background of the widget */
|
||||
Widget {
|
||||
text-opacity: 50%;
|
||||
}
|
||||
```
|
||||
|
||||
## Python
|
||||
|
||||
```python
|
||||
# Set the text to be "half-faded" against the background of the widget
|
||||
widget.styles.text_opacity = "50%"
|
||||
```
|
||||
@@ -4,33 +4,33 @@
|
||||
|
||||
* {
|
||||
transition: color 300ms linear, background 300ms linear;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
*:hover {
|
||||
/* tint: 30% red;
|
||||
/* tint: 30% red;
|
||||
/* outline: heavy red; */
|
||||
}
|
||||
|
||||
App > Screen {
|
||||
|
||||
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
color: $text-surface;
|
||||
layers: sidebar;
|
||||
|
||||
color: $text-background;
|
||||
background: $background;
|
||||
layout: vertical;
|
||||
|
||||
|
||||
}
|
||||
|
||||
DataTable {
|
||||
/*border:heavy red;*/
|
||||
/* tint: 10% green; */
|
||||
/* opacity: 50%; */
|
||||
/* text-opacity: 50%; */
|
||||
padding: 1;
|
||||
margin: 1 2;
|
||||
height: 12;
|
||||
margin: 1 2;
|
||||
height: 12;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
@@ -39,7 +39,7 @@ DataTable {
|
||||
dock: left;
|
||||
width: 30;
|
||||
offset-x: -100%;
|
||||
|
||||
|
||||
transition: offset 500ms in_out_cubic;
|
||||
layer: sidebar;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ DataTable {
|
||||
background: $secondary-background;
|
||||
height: 1;
|
||||
content-align: center middle;
|
||||
|
||||
|
||||
dock: top;
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@ DataTable {
|
||||
Tweet {
|
||||
height:12;
|
||||
width: 100%;
|
||||
|
||||
|
||||
|
||||
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
layout: vertical;
|
||||
@@ -100,7 +100,7 @@ Tweet {
|
||||
|
||||
|
||||
.scrollable {
|
||||
|
||||
|
||||
overflow-y: scroll;
|
||||
margin: 1 2;
|
||||
height: 20;
|
||||
@@ -108,9 +108,9 @@ Tweet {
|
||||
layout: vertical;
|
||||
}
|
||||
|
||||
.code {
|
||||
.code {
|
||||
height: auto;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -120,12 +120,12 @@ TweetHeader {
|
||||
color: $text-accent
|
||||
}
|
||||
|
||||
TweetBody {
|
||||
TweetBody {
|
||||
width: 100%;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
height: auto;
|
||||
padding: 0 1 0 0;
|
||||
}
|
||||
|
||||
Tweet.scroll-horizontal TweetBody {
|
||||
@@ -145,7 +145,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
/* padding: 1 0 0 0 ; */
|
||||
|
||||
transition: background 400ms in_out_cubic, color 400ms in_out_cubic;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
@@ -165,7 +165,7 @@ Tweet.scroll-horizontal TweetBody {
|
||||
color: $text-accent;
|
||||
background: $accent;
|
||||
height: 1;
|
||||
|
||||
|
||||
content-align: center middle;
|
||||
dock:bottom;
|
||||
}
|
||||
@@ -200,7 +200,7 @@ Error {
|
||||
color: $text-error;
|
||||
border-top: tall $error-darken-2;
|
||||
border-bottom: tall $error-darken-2;
|
||||
|
||||
|
||||
padding: 0;
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
@@ -213,21 +213,21 @@ Warning {
|
||||
color: $text-warning-fade-1;
|
||||
border-top: tall $warning-darken-2;
|
||||
border-bottom: tall $warning-darken-2;
|
||||
|
||||
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
Success {
|
||||
width: 100%;
|
||||
|
||||
height:auto;
|
||||
|
||||
height:auto;
|
||||
box-sizing: border-box;
|
||||
background: $success;
|
||||
color: $text-success-fade-1;
|
||||
|
||||
color: $text-success-fade-1;
|
||||
|
||||
border-top: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
|
||||
text-style: bold ;
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ nav:
|
||||
- "styles/min_height.md"
|
||||
- "styles/min_width.md"
|
||||
- "styles/offset.md"
|
||||
- "styles/opacity.md"
|
||||
- "styles/outline.md"
|
||||
- "styles/overflow.md"
|
||||
- "styles/padding.md"
|
||||
@@ -59,6 +60,7 @@ nav:
|
||||
- "styles/scrollbar_size.md"
|
||||
- "styles/text_align.md"
|
||||
- "styles/text_style.md"
|
||||
- "styles/text_opacity.md"
|
||||
- "styles/tint.md"
|
||||
- "styles/visibility.md"
|
||||
- "styles/width.md"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
Button {
|
||||
padding-left: 1;
|
||||
padding-right: 1;
|
||||
margin: 3;
|
||||
text-opacity: 30%;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
Screen {
|
||||
background: lightcoral;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
color: $text-panel;
|
||||
background: $panel;
|
||||
dock: left;
|
||||
width: 30;
|
||||
offset-x: -100%;
|
||||
transition: offset 500ms in_out_cubic 2s;
|
||||
layer: sidebar;
|
||||
}
|
||||
|
||||
#sidebar.-active {
|
||||
offset-x: 0;
|
||||
background: darkslategrey;
|
||||
}
|
||||
|
||||
.box1 {
|
||||
background: orangered;
|
||||
height: 12;
|
||||
width: 30;
|
||||
}
|
||||
|
||||
.box2 {
|
||||
background: blueviolet;
|
||||
height: 6;
|
||||
width: 12;
|
||||
background: darkmagenta;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@@ -1,48 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import RenderableType
|
||||
|
||||
from textual import events
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class Box(Widget, can_focus=True):
|
||||
CSS = "#box {background: blue;}"
|
||||
|
||||
def __init__(
|
||||
self, id: str | None = None, classes: str | None = None, *children: Widget
|
||||
):
|
||||
super().__init__(*children, id=id, classes=classes)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return "Box"
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class JustABox(App):
|
||||
def on_load(self):
|
||||
self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar")
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
self.box = Box(classes="box1")
|
||||
yield self.box
|
||||
yield Box(classes="box2")
|
||||
yield Widget(id="sidebar")
|
||||
|
||||
def key_a(self):
|
||||
self.animator.animate(
|
||||
self.box.styles,
|
||||
"opacity",
|
||||
value=0.0,
|
||||
duration=2.0,
|
||||
delay=2.0,
|
||||
on_complete=self.box.remove,
|
||||
)
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
await self.dispatch_key(event)
|
||||
yield Static("Hello, world!", classes="box1")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = JustABox(css_path="just_a_box.css", watch_css=True)
|
||||
app = JustABox(css_path="../darren/just_a_box.css", watch_css=True)
|
||||
app.run()
|
||||
|
||||
@@ -54,7 +54,7 @@ Widget:hover {
|
||||
}
|
||||
|
||||
#footer {
|
||||
opacity: 1;
|
||||
text-opacity: 1;
|
||||
color: $text;
|
||||
background: $background;
|
||||
height: 3;
|
||||
@@ -62,5 +62,5 @@ Widget:hover {
|
||||
}
|
||||
|
||||
#footer.dim {
|
||||
opacity: 0.5;
|
||||
text-opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ DirectoryTree {
|
||||
DataTable {
|
||||
/*border:heavy red;*/
|
||||
/* tint: 10% green; */
|
||||
/* opacity: 50%; */
|
||||
/* text-opacity: 50%; */
|
||||
padding: 1;
|
||||
margin: 1 2;
|
||||
height: 24;
|
||||
|
||||
@@ -130,7 +130,7 @@ class BasicApp(App, css_path="basic.css"):
|
||||
classes="scrollable",
|
||||
),
|
||||
table,
|
||||
Widget(DirectoryTree("~/projects/textual"), id="tree-container"),
|
||||
Widget(DirectoryTree("~/"), id="tree-container"),
|
||||
Error(),
|
||||
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
||||
Warning(),
|
||||
|
||||
45
src/textual/_opacity.py
Normal file
45
src/textual/_opacity.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from typing import Iterable
|
||||
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
from textual.color import Color
|
||||
|
||||
|
||||
def _apply_opacity(
|
||||
segments: Iterable[Segment],
|
||||
base_background: Color,
|
||||
opacity: float,
|
||||
) -> Iterable[Segment]:
|
||||
"""Takes an iterable of foreground Segments and blends them into the supplied
|
||||
background color, yielding copies of the Segments with blended foreground and
|
||||
background colors applied.
|
||||
|
||||
Args:
|
||||
segments (Iterable[Segment]): The segments in the foreground.
|
||||
base_background (Color): The background color to blend foreground into.
|
||||
opacity (float): The blending factor. A value of 1.0 means output segments will
|
||||
have identical foreground and background colors to input segments.
|
||||
"""
|
||||
_Segment = Segment
|
||||
from_rich_color = Color.from_rich_color
|
||||
from_color = Style.from_color
|
||||
blend = base_background.blend
|
||||
for segment in segments:
|
||||
text, style, _ = segment
|
||||
if not style:
|
||||
yield segment
|
||||
continue
|
||||
|
||||
blended_style = style
|
||||
if style.color:
|
||||
color = from_rich_color(style.color)
|
||||
blended_foreground = blend(color, factor=opacity)
|
||||
blended_style += from_color(color=blended_foreground.rich_color)
|
||||
|
||||
if style.bgcolor:
|
||||
bgcolor = from_rich_color(style.bgcolor)
|
||||
blended_background = blend(bgcolor, factor=opacity)
|
||||
blended_style += from_color(bgcolor=blended_background.rich_color)
|
||||
|
||||
yield _Segment(text, blended_style)
|
||||
30
src/textual/_path.py
Normal file
30
src/textual/_path.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
from pathlib import Path, PurePath
|
||||
|
||||
|
||||
def _make_path_object_relative(path: str | PurePath, obj: object) -> Path:
|
||||
"""Convert the supplied path to a Path object that is relative to a given Python object.
|
||||
If the supplied path is absolute, it will simply be converted to a Path object.
|
||||
Used, for example, to return the path of a CSS file relative to a Textual App instance.
|
||||
|
||||
Args:
|
||||
path (str | Path): A path.
|
||||
obj (object): A Python object to resolve the path relative to.
|
||||
|
||||
Returns:
|
||||
Path: A resolved Path object, relative to obj
|
||||
"""
|
||||
path = Path(path)
|
||||
|
||||
# If the path supplied by the user is absolute, we can use it directly
|
||||
if path.is_absolute():
|
||||
return path
|
||||
|
||||
# Otherwise (relative path), resolve it relative to obj...
|
||||
subclass_module = sys.modules[obj.__module__]
|
||||
subclass_path = Path(inspect.getfile(subclass_module))
|
||||
resolved_path = (subclass_path.parent / path).resolve()
|
||||
return resolved_path
|
||||
@@ -7,11 +7,12 @@ from rich.segment import Segment
|
||||
from rich.style import Style
|
||||
|
||||
from ._border import get_box, render_row
|
||||
from ._opacity import _apply_opacity
|
||||
from ._segment_tools import line_crop, line_pad, line_trim
|
||||
from ._types import Lines
|
||||
from .color import Color
|
||||
from .geometry import Region, Size, Spacing
|
||||
from .renderables.opacity import Opacity
|
||||
from .renderables.text_opacity import TextOpacity
|
||||
from .renderables.tint import Tint
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
@@ -237,10 +238,13 @@ class StylesCache:
|
||||
Returns:
|
||||
list[Segment]: New list of segments
|
||||
"""
|
||||
if styles.opacity != 1.0:
|
||||
segments = Opacity.process_segments(segments, styles.opacity)
|
||||
if styles.text_opacity != 1.0:
|
||||
segments = TextOpacity.process_segments(segments, styles.text_opacity)
|
||||
if styles.tint.a:
|
||||
segments = Tint.process_segments(segments, styles.tint)
|
||||
if styles.opacity != 1.0:
|
||||
segments = _apply_opacity(segments, base_background, styles.opacity)
|
||||
segments = list(segments)
|
||||
return segments if isinstance(segments, list) else list(segments)
|
||||
|
||||
line: Iterable[Segment]
|
||||
|
||||
@@ -9,10 +9,9 @@ import sys
|
||||
import warnings
|
||||
from contextlib import redirect_stdout, redirect_stderr
|
||||
from datetime import datetime
|
||||
from pathlib import PurePath
|
||||
from pathlib import PurePath, Path
|
||||
from time import perf_counter
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Generic,
|
||||
Iterable,
|
||||
@@ -25,6 +24,7 @@ from typing import (
|
||||
from weakref import WeakSet, WeakValueDictionary
|
||||
|
||||
from ._ansi_sequences import SYNC_END, SYNC_START
|
||||
from ._path import _make_path_object_relative
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
@@ -63,8 +63,6 @@ from .renderables.blank import Blank
|
||||
from .screen import Screen
|
||||
from .widget import Widget
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .css.query import DOMQuery
|
||||
|
||||
PLATFORM = platform.system()
|
||||
WINDOWS = PLATFORM == "Windows"
|
||||
@@ -147,7 +145,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
CSS = """
|
||||
App {
|
||||
background: $background;
|
||||
color: $text-background;
|
||||
color: $text-background;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -225,7 +223,15 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
self.stylesheet = Stylesheet(variables=self.get_css_variables())
|
||||
self._require_stylesheet_update: set[DOMNode] = set()
|
||||
self.css_path = css_path or self.CSS_PATH
|
||||
|
||||
# We want the CSS path to be resolved from the location of the App subclass
|
||||
css_path = css_path or self.CSS_PATH
|
||||
if css_path is not None:
|
||||
if isinstance(css_path, str):
|
||||
css_path = Path(css_path)
|
||||
css_path = _make_path_object_relative(css_path, self) if css_path else None
|
||||
|
||||
self.css_path = css_path
|
||||
|
||||
self._registry: WeakSet[DOMNode] = WeakSet()
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import runpy
|
||||
from typing import cast, TYPE_CHECKING
|
||||
|
||||
from importlib_metadata import version
|
||||
@@ -57,16 +59,12 @@ def import_app(import_name: str) -> App:
|
||||
lib, _colon, name = import_name.partition(":")
|
||||
|
||||
if lib.endswith(".py"):
|
||||
# We're assuming the user wants to load a .py file
|
||||
path = os.path.abspath(lib)
|
||||
try:
|
||||
with open(lib) as python_file:
|
||||
py_code = python_file.read()
|
||||
global_vars = runpy.run_path(path)
|
||||
except Exception as error:
|
||||
raise AppFail(str(error))
|
||||
|
||||
global_vars: dict[str, object] = {}
|
||||
exec(py_code, global_vars)
|
||||
|
||||
if name:
|
||||
# User has given a name, use that
|
||||
try:
|
||||
|
||||
@@ -332,7 +332,7 @@ class StylesBuilder:
|
||||
"visibility", valid_values=list(VALID_VISIBILITY), context="css"
|
||||
)
|
||||
|
||||
def process_opacity(self, name: str, tokens: list[Token]) -> None:
|
||||
def _process_fractional(self, name: str, tokens: list[Token]) -> None:
|
||||
if not tokens:
|
||||
return
|
||||
token = tokens[0]
|
||||
@@ -342,16 +342,17 @@ class StylesBuilder:
|
||||
else:
|
||||
token_name = token.name
|
||||
value = token.value
|
||||
rule_name = name.replace("-", "_")
|
||||
if token_name == "scalar" and value.endswith("%"):
|
||||
try:
|
||||
opacity = percentage_string_to_float(value)
|
||||
self.styles.set_rule(name, opacity)
|
||||
text_opacity = percentage_string_to_float(value)
|
||||
self.styles.set_rule(rule_name, text_opacity)
|
||||
except ValueError:
|
||||
error = True
|
||||
elif token_name == "number":
|
||||
try:
|
||||
opacity = clamp(float(value), 0, 1)
|
||||
self.styles.set_rule(name, opacity)
|
||||
text_opacity = clamp(float(value), 0, 1)
|
||||
self.styles.set_rule(rule_name, text_opacity)
|
||||
except ValueError:
|
||||
error = True
|
||||
else:
|
||||
@@ -360,6 +361,9 @@ class StylesBuilder:
|
||||
if error:
|
||||
self.error(name, token, fractional_property_help_text(name, context="css"))
|
||||
|
||||
process_opacity = _process_fractional
|
||||
process_text_opacity = _process_fractional
|
||||
|
||||
def _process_space(self, name: str, tokens: list[Token]) -> None:
|
||||
space: list[int] = []
|
||||
append = space.append
|
||||
|
||||
@@ -88,6 +88,7 @@ class RulesMap(TypedDict, total=False):
|
||||
text_style: Style
|
||||
|
||||
opacity: float
|
||||
text_opacity: float
|
||||
|
||||
padding: Spacing
|
||||
margin: Spacing
|
||||
@@ -185,6 +186,7 @@ class StylesBase(ABC):
|
||||
"color",
|
||||
"background",
|
||||
"opacity",
|
||||
"text_opacity",
|
||||
"tint",
|
||||
"scrollbar_color",
|
||||
"scrollbar_color_hover",
|
||||
@@ -205,6 +207,7 @@ class StylesBase(ABC):
|
||||
text_style = StyleFlagsProperty()
|
||||
|
||||
opacity = FractionalProperty()
|
||||
text_opacity = FractionalProperty()
|
||||
|
||||
padding = SpacingProperty()
|
||||
margin = SpacingProperty()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
@@ -12,7 +11,7 @@ from ._callback import invoke
|
||||
class FileMonitor:
|
||||
"""Monitors a file for changes and invokes a callback when it does."""
|
||||
|
||||
def __init__(self, path: str | PurePath, callback: Callable) -> None:
|
||||
def __init__(self, path: PurePath, callback: Callable) -> None:
|
||||
self.path = path
|
||||
self.callback = callback
|
||||
self._modified = self._get_modified()
|
||||
|
||||
@@ -31,7 +31,7 @@ def _get_blended_style_cached(
|
||||
)
|
||||
|
||||
|
||||
class Opacity:
|
||||
class TextOpacity:
|
||||
"""Blend foreground in to background."""
|
||||
|
||||
def __init__(self, renderable: RenderableType, opacity: float = 1.0) -> None:
|
||||
@@ -96,7 +96,7 @@ if __name__ == "__main__":
|
||||
)
|
||||
console.print(panel)
|
||||
|
||||
opacity_panel = Opacity(panel, opacity=0.5)
|
||||
opacity_panel = TextOpacity(panel, opacity=0.5)
|
||||
console.print(opacity_panel)
|
||||
|
||||
def frange(start, end, step):
|
||||
@@ -29,14 +29,14 @@ class HeaderClock(Widget):
|
||||
"""Display a clock on the right of the header."""
|
||||
|
||||
CSS = """
|
||||
HeaderClock {
|
||||
HeaderClock {
|
||||
dock: right;
|
||||
width: auto;
|
||||
padding: 0 1;
|
||||
background: $secondary-background-lighten-1;
|
||||
color: $text-secondary-background;
|
||||
opacity: 85%;
|
||||
content-align: center middle;
|
||||
text-opacity: 85%;
|
||||
content-align: center middle;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -51,9 +51,9 @@ class HeaderTitle(Widget):
|
||||
"""Display the title / subtitle in the header."""
|
||||
|
||||
CSS = """
|
||||
HeaderTitle {
|
||||
HeaderTitle {
|
||||
content-align: center middle;
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -79,7 +79,7 @@ class Header(Widget):
|
||||
height: 1;
|
||||
}
|
||||
Header.tall {
|
||||
height: 3;
|
||||
height: 3;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ from textual import events
|
||||
from textual._layout_resolve import layout_resolve, Edge
|
||||
from textual.keys import Keys
|
||||
from textual.reactive import Reactive
|
||||
from textual.renderables.opacity import Opacity
|
||||
from textual.renderables.text_opacity import TextOpacity
|
||||
from textual.renderables.underline_bar import UnderlineBar
|
||||
from textual.widget import Widget
|
||||
|
||||
@@ -125,7 +125,7 @@ class TabsRenderable:
|
||||
style=inactive_tab_style
|
||||
+ Style.from_meta({"@click": f"range_clicked('{tab.name}')"}),
|
||||
)
|
||||
dimmed_tab_content = Opacity(
|
||||
dimmed_tab_content = TextOpacity(
|
||||
tab_content, opacity=self.inactive_text_opacity
|
||||
)
|
||||
segments = console.render(dimmed_tab_content)
|
||||
|
||||
@@ -1099,15 +1099,15 @@ class TestParseOpacity:
|
||||
],
|
||||
)
|
||||
def test_opacity_to_styles(self, css_value, styles_value):
|
||||
css = f"#some-widget {{ opacity: {css_value} }}"
|
||||
css = f"#some-widget {{ text-opacity: {css_value} }}"
|
||||
stylesheet = Stylesheet()
|
||||
stylesheet.add_source(css)
|
||||
|
||||
assert stylesheet.rules[0].styles.opacity == styles_value
|
||||
assert stylesheet.rules[0].styles.text_opacity == styles_value
|
||||
assert not stylesheet.rules[0].errors
|
||||
|
||||
def test_opacity_invalid_value(self):
|
||||
css = "#some-widget { opacity: 123x }"
|
||||
css = "#some-widget { text-opacity: 123x }"
|
||||
stylesheet = Stylesheet()
|
||||
|
||||
with pytest.raises(StylesheetParseError):
|
||||
|
||||
@@ -120,7 +120,7 @@ def test_render_styles_border():
|
||||
|
||||
def test_get_opacity_default():
|
||||
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
||||
assert styles.opacity == 1.0
|
||||
assert styles.text_opacity == 1.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -136,14 +136,14 @@ def test_get_opacity_default():
|
||||
)
|
||||
def test_opacity_set_then_get(set_value, expected):
|
||||
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
||||
styles.opacity = set_value
|
||||
assert styles.opacity == expected
|
||||
styles.text_opacity = set_value
|
||||
assert styles.text_opacity == expected
|
||||
|
||||
|
||||
def test_opacity_set_invalid_type_error():
|
||||
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
||||
with pytest.raises(StyleValueError):
|
||||
styles.opacity = "invalid value"
|
||||
styles.text_opacity = "invalid value"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -2,7 +2,7 @@ import pytest
|
||||
from rich.text import Text
|
||||
|
||||
from tests.utilities.render import render
|
||||
from textual.renderables.opacity import Opacity
|
||||
from textual.renderables.text_opacity import TextOpacity
|
||||
|
||||
STOP = "\x1b[0m"
|
||||
|
||||
@@ -12,39 +12,39 @@ def text():
|
||||
return Text("Hello, world!", style="#ff0000 on #00ff00", end="")
|
||||
|
||||
|
||||
def test_simple_opacity(text):
|
||||
def test_simple_text_opacity(text):
|
||||
blended_red_on_green = "\x1b[38;2;127;127;0;48;2;0;255;0m"
|
||||
assert render(Opacity(text, opacity=.5)) == (
|
||||
assert render(TextOpacity(text, opacity=.5)) == (
|
||||
f"{blended_red_on_green}Hello, world!{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_value_zero_sets_foreground_color_to_background_color(text):
|
||||
foreground = background = "0;255;0"
|
||||
assert render(Opacity(text, opacity=0)) == (
|
||||
assert render(TextOpacity(text, opacity=0)) == (
|
||||
f"\x1b[38;2;{foreground};48;2;{background}mHello, world!{STOP}"
|
||||
)
|
||||
|
||||
|
||||
def test_opacity_value_of_one_noop(text):
|
||||
assert render(Opacity(text, opacity=1)) == render(text)
|
||||
def test_text_opacity_value_of_one_noop(text):
|
||||
assert render(TextOpacity(text, opacity=1)) == render(text)
|
||||
|
||||
|
||||
def test_ansi_colors_noop():
|
||||
ansi_colored_text = Text("Hello, world!", style="red on green", end="")
|
||||
assert render(Opacity(ansi_colored_text, opacity=.5)) == render(ansi_colored_text)
|
||||
assert render(TextOpacity(ansi_colored_text, opacity=.5)) == render(ansi_colored_text)
|
||||
|
||||
|
||||
def test_opacity_no_style_noop():
|
||||
def test_text_opacity_no_style_noop():
|
||||
text_no_style = Text("Hello, world!", end="")
|
||||
assert render(Opacity(text_no_style, opacity=.2)) == render(text_no_style)
|
||||
assert render(TextOpacity(text_no_style, opacity=.2)) == render(text_no_style)
|
||||
|
||||
|
||||
def test_opacity_only_fg_noop():
|
||||
def test_text_opacity_only_fg_noop():
|
||||
text_only_fg = Text("Hello, world!", style="#ff0000", end="")
|
||||
assert render(Opacity(text_only_fg, opacity=.5)) == render(text_only_fg)
|
||||
assert render(TextOpacity(text_only_fg, opacity=.5)) == render(text_only_fg)
|
||||
|
||||
|
||||
def test_opacity_only_bg_noop():
|
||||
def test_text_opacity_only_bg_noop():
|
||||
text_only_bg = Text("Hello, world!", style="on #ff0000", end="")
|
||||
assert render(Opacity(text_only_bg, opacity=.5)) == render(text_only_bg)
|
||||
assert render(TextOpacity(text_only_bg, opacity=.5)) == render(text_only_bg)
|
||||
Reference in New Issue
Block a user