mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' into style-error-improvements
This commit is contained in:
@@ -5,6 +5,8 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## [0.2.0] - Unreleased
|
||||||
|
|
||||||
## [0.1.15] - 2022-01-31
|
## [0.1.15] - 2022-01-31
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ App > Screen {
|
|||||||
background: $primary-darken-2;
|
background: $primary-darken-2;
|
||||||
color: $text-primary-darken-2 ;
|
color: $text-primary-darken-2 ;
|
||||||
border-right: outer $primary-darken-3;
|
border-right: outer $primary-darken-3;
|
||||||
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .user {
|
#sidebar .user {
|
||||||
@@ -48,19 +49,21 @@ App > Screen {
|
|||||||
background: $primary-darken-1;
|
background: $primary-darken-1;
|
||||||
color: $text-primary-darken-1;
|
color: $text-primary-darken-1;
|
||||||
border-right: outer $primary-darken-3;
|
border-right: outer $primary-darken-3;
|
||||||
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .content {
|
#sidebar .content {
|
||||||
background: $primary;
|
background: $primary;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
border-right: outer $primary-darken-3;
|
border-right: outer $primary-darken-3;
|
||||||
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header {
|
#header {
|
||||||
color: $text-primary-darken-1;
|
color: $text-primary-darken-1;
|
||||||
background: $primary-darken-1;
|
background: $primary-darken-1;
|
||||||
height: 3
|
height: 3;
|
||||||
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
@@ -84,6 +87,7 @@ Tweet {
|
|||||||
border: wide $panel-darken-2;
|
border: wide $panel-darken-2;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
@@ -152,6 +156,7 @@ TweetBody {
|
|||||||
background: $accent;
|
background: $accent;
|
||||||
height: 1;
|
height: 1;
|
||||||
border-top: hkey $accent-darken-2;
|
border-top: hkey $accent-darken-2;
|
||||||
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -165,6 +170,7 @@ OptionItem {
|
|||||||
transition: background 100ms linear;
|
transition: background 100ms linear;
|
||||||
border-right: outer $primary-darken-2;
|
border-right: outer $primary-darken-2;
|
||||||
border-left: hidden;
|
border-left: hidden;
|
||||||
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionItem:hover {
|
OptionItem:hover {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class Tweet(Widget):
|
|||||||
|
|
||||||
class OptionItem(Widget):
|
class OptionItem(Widget):
|
||||||
def render(self) -> Text:
|
def render(self) -> Text:
|
||||||
return Align.center(Text("Option", justify="center"), vertical="middle")
|
return Text("Option")
|
||||||
|
|
||||||
|
|
||||||
class Error(Widget):
|
class Error(Widget):
|
||||||
@@ -95,10 +95,9 @@ class BasicApp(App):
|
|||||||
"""Build layout here."""
|
"""Build layout here."""
|
||||||
self.mount(
|
self.mount(
|
||||||
header=Static(
|
header=Static(
|
||||||
Align.center(
|
Text.from_markup(
|
||||||
"[b]This is a [u]Textual[/u] app, running in the terminal",
|
"[b]This is a [u]Textual[/u] app, running in the terminal"
|
||||||
vertical="middle",
|
),
|
||||||
)
|
|
||||||
),
|
),
|
||||||
content=Widget(
|
content=Widget(
|
||||||
Tweet(
|
Tweet(
|
||||||
@@ -110,8 +109,8 @@ class BasicApp(App):
|
|||||||
# ),
|
# ),
|
||||||
),
|
),
|
||||||
Widget(
|
Widget(
|
||||||
Static(Syntax(CODE, "python"), classes={"code"}),
|
Static(Syntax(CODE, "python"), classes="code"),
|
||||||
classes={"scrollable"},
|
classes="scrollable",
|
||||||
),
|
),
|
||||||
Error(),
|
Error(),
|
||||||
Tweet(TweetBody()),
|
Tweet(TweetBody()),
|
||||||
@@ -121,12 +120,12 @@ class BasicApp(App):
|
|||||||
),
|
),
|
||||||
footer=Widget(),
|
footer=Widget(),
|
||||||
sidebar=Widget(
|
sidebar=Widget(
|
||||||
Widget(classes={"title"}),
|
Widget(classes="title"),
|
||||||
Widget(classes={"user"}),
|
Widget(classes="user"),
|
||||||
OptionItem(),
|
OptionItem(),
|
||||||
OptionItem(),
|
OptionItem(),
|
||||||
OptionItem(),
|
OptionItem(),
|
||||||
Widget(classes={"content"}),
|
Widget(classes="content"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,4 +139,7 @@ class BasicApp(App):
|
|||||||
self.panic(self.tree)
|
self.panic(self.tree)
|
||||||
|
|
||||||
|
|
||||||
BasicApp.run(css_file="basic.css", watch_css=True, log="textual.log")
|
app = BasicApp(css_file="basic.css", watch_css=True, log="textual.log")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
|
|||||||
0
sandbox/buttons.css
Normal file
0
sandbox/buttons.css
Normal file
24
sandbox/buttons.py
Normal file
24
sandbox/buttons.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from textual.app import App, ComposeResult
|
||||||
|
|
||||||
|
from textual.widgets import Button
|
||||||
|
from textual import layout
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonsApp(App[str]):
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield layout.Vertical(
|
||||||
|
Button("foo", id="foo"),
|
||||||
|
Button("bar", id="bar"),
|
||||||
|
Button("baz", id="baz"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
self.app.bell()
|
||||||
|
self.exit(event.button.id)
|
||||||
|
|
||||||
|
|
||||||
|
app = ButtonsApp(log="textual.log")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = app.run()
|
||||||
|
print(repr(result))
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#uber1 {
|
#uber1 {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
background: dark_green;
|
background: green;
|
||||||
|
overflow: hidden auto;
|
||||||
border: heavy white;
|
border: heavy white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ class BasicApp(App):
|
|||||||
)
|
)
|
||||||
first_child = Placeholder(id="child1", classes={"list-item"})
|
first_child = Placeholder(id="child1", classes={"list-item"})
|
||||||
uber1 = Widget(
|
uber1 = Widget(
|
||||||
first_child,
|
Placeholder(id="child1", classes="list-item"),
|
||||||
Placeholder(id="child2", classes={"list-item"}),
|
Placeholder(id="child2", classes="list-item"),
|
||||||
Placeholder(id="child3", classes={"list-item"}),
|
Placeholder(id="child3", classes="list-item"),
|
||||||
Placeholder(classes={"list-item"}),
|
Placeholder(classes="list-item"),
|
||||||
Placeholder(classes={"list-item"}),
|
Placeholder(classes="list-item"),
|
||||||
Placeholder(classes={"list-item"}),
|
Placeholder(classes="list-item"),
|
||||||
)
|
)
|
||||||
self.mount(uber1=uber1)
|
self.mount(uber1=uber1)
|
||||||
await first_child.focus()
|
await first_child.focus()
|
||||||
@@ -44,7 +44,7 @@ class BasicApp(App):
|
|||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
|
|
||||||
def action_quit(self):
|
def action_quit(self):
|
||||||
self.panic(self.screen.tree)
|
self.panic(self.app.tree)
|
||||||
|
|
||||||
def action_dump(self):
|
def action_dump(self):
|
||||||
self.panic(str(self.app.registry))
|
self.panic(str(self.app.registry))
|
||||||
@@ -83,4 +83,7 @@ class BasicApp(App):
|
|||||||
self.focused.styles.border = ("solid", "red")
|
self.focused.styles.border = ("solid", "red")
|
||||||
|
|
||||||
|
|
||||||
BasicApp.run(css_file="uber.css", log="textual.log", log_verbosity=1)
|
app = BasicApp(css_file="uber.css", log="textual.log", log_verbosity=1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
|
|||||||
@@ -1,239 +1,172 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
ANSI_COLOR_TO_RGB: dict[str, tuple[int, int, int]] = {
|
COLOR_NAME_TO_RGB: dict[str, tuple[int, int, int] | tuple[int, int, int, float]] = {
|
||||||
|
# Let's start with a specific pseudo-color::
|
||||||
|
"transparent": (0, 0, 0, 0),
|
||||||
|
# Then, the 16 common ANSI colors:
|
||||||
|
"ansi_black": (0, 0, 0),
|
||||||
|
"ansi_red": (128, 0, 0),
|
||||||
|
"ansi_green": (0, 128, 0),
|
||||||
|
"ansi_yellow": (128, 128, 0),
|
||||||
|
"ansi_blue": (0, 0, 128),
|
||||||
|
"ansi_magenta": (128, 0, 128),
|
||||||
|
"ansi_cyan": (0, 128, 128),
|
||||||
|
"ansi_white": (192, 192, 192),
|
||||||
|
"ansi_bright_black": (128, 128, 128),
|
||||||
|
"ansi_bright_red": (255, 0, 0),
|
||||||
|
"ansi_bright_green": (0, 255, 0),
|
||||||
|
"ansi_bright_yellow": (255, 255, 0),
|
||||||
|
"ansi_bright_blue": (0, 0, 255),
|
||||||
|
"ansi_bright_magenta": (255, 0, 255),
|
||||||
|
"ansi_bright_cyan": (0, 255, 255),
|
||||||
|
"ansi_bright_white": (255, 255, 255),
|
||||||
|
# And then, Web color keywords: (up to CSS Color Module Level 4)
|
||||||
"black": (0, 0, 0),
|
"black": (0, 0, 0),
|
||||||
"red": (128, 0, 0),
|
"silver": (192, 192, 192),
|
||||||
|
"gray": (128, 128, 128),
|
||||||
|
"white": (255, 255, 255),
|
||||||
|
"maroon": (128, 0, 0),
|
||||||
|
"red": (255, 0, 0),
|
||||||
|
"purple": (128, 0, 128),
|
||||||
|
"fuchsia": (255, 0, 255),
|
||||||
"green": (0, 128, 0),
|
"green": (0, 128, 0),
|
||||||
"yellow": (128, 128, 0),
|
"lime": (0, 255, 0),
|
||||||
"blue": (0, 0, 128),
|
"olive": (128, 128, 0),
|
||||||
"magenta": (128, 0, 128),
|
"yellow": (255, 255, 0),
|
||||||
"cyan": (0, 128, 128),
|
"navy": (0, 0, 128),
|
||||||
"white": (192, 192, 192),
|
"blue": (0, 0, 255),
|
||||||
"bright_black": (128, 128, 128),
|
"teal": (0, 128, 128),
|
||||||
"bright_red": (255, 0, 0),
|
"aqua": (0, 255, 255),
|
||||||
"bright_green": (0, 255, 0),
|
"orange": (255, 165, 0),
|
||||||
"bright_yellow": (255, 255, 0),
|
"aliceblue": (240, 248, 255),
|
||||||
"bright_blue": (0, 0, 255),
|
"antiquewhite": (250, 235, 215),
|
||||||
"bright_magenta": (255, 0, 255),
|
"aquamarine": (127, 255, 212),
|
||||||
"bright_cyan": (0, 255, 255),
|
"azure": (240, 255, 255),
|
||||||
"bright_white": (255, 255, 255),
|
"beige": (245, 245, 220),
|
||||||
"grey0": (0, 0, 0),
|
"bisque": (255, 228, 196),
|
||||||
"gray0": (0, 0, 0),
|
"blanchedalmond": (255, 235, 205),
|
||||||
"navy_blue": (0, 0, 95),
|
"blueviolet": (138, 43, 226),
|
||||||
"dark_blue": (0, 0, 135),
|
"brown": (165, 42, 42),
|
||||||
"blue3": (0, 0, 215),
|
"burlywood": (222, 184, 135),
|
||||||
"blue1": (0, 0, 255),
|
"cadetblue": (95, 158, 160),
|
||||||
"dark_green": (0, 95, 0),
|
"chartreuse": (127, 255, 0),
|
||||||
"deep_sky_blue4": (0, 95, 175),
|
"chocolate": (210, 105, 30),
|
||||||
"dodger_blue3": (0, 95, 215),
|
"coral": (255, 127, 80),
|
||||||
"dodger_blue2": (0, 95, 255),
|
"cornflowerblue": (100, 149, 237),
|
||||||
"green4": (0, 135, 0),
|
"cornsilk": (255, 248, 220),
|
||||||
"spring_green4": (0, 135, 95),
|
"crimson": (220, 20, 60),
|
||||||
"turquoise4": (0, 135, 135),
|
"cyan": (0, 255, 255),
|
||||||
"deep_sky_blue3": (0, 135, 215),
|
"darkblue": (0, 0, 139),
|
||||||
"dodger_blue1": (0, 135, 255),
|
"darkcyan": (0, 139, 139),
|
||||||
"green3": (0, 215, 0),
|
"darkgoldenrod": (184, 134, 11),
|
||||||
"spring_green3": (0, 215, 95),
|
"darkgray": (169, 169, 169),
|
||||||
"dark_cyan": (0, 175, 135),
|
"darkgreen": (0, 100, 0),
|
||||||
"light_sea_green": (0, 175, 175),
|
"darkgrey": (169, 169, 169),
|
||||||
"deep_sky_blue2": (0, 175, 215),
|
"darkkhaki": (189, 183, 107),
|
||||||
"deep_sky_blue1": (0, 175, 255),
|
"darkmagenta": (139, 0, 139),
|
||||||
"spring_green2": (0, 255, 95),
|
"darkolivegreen": (85, 107, 47),
|
||||||
"cyan3": (0, 215, 175),
|
"darkorange": (255, 140, 0),
|
||||||
"dark_turquoise": (0, 215, 215),
|
"darkorchid": (153, 50, 204),
|
||||||
"turquoise2": (0, 215, 255),
|
"darkred": (139, 0, 0),
|
||||||
"green1": (0, 255, 0),
|
"darksalmon": (233, 150, 122),
|
||||||
"spring_green1": (0, 255, 135),
|
"darkseagreen": (143, 188, 143),
|
||||||
"medium_spring_green": (0, 255, 175),
|
"darkslateblue": (72, 61, 139),
|
||||||
"cyan2": (0, 255, 215),
|
"darkslategray": (47, 79, 79),
|
||||||
"cyan1": (0, 255, 255),
|
"darkslategrey": (47, 79, 79),
|
||||||
"dark_red": (135, 0, 0),
|
"darkturquoise": (0, 206, 209),
|
||||||
"deep_pink4": (175, 0, 95),
|
"darkviolet": (148, 0, 211),
|
||||||
"purple4": (95, 0, 175),
|
"deeppink": (255, 20, 147),
|
||||||
"purple3": (95, 0, 215),
|
"deepskyblue": (0, 191, 255),
|
||||||
"blue_violet": (95, 0, 255),
|
"dimgray": (105, 105, 105),
|
||||||
"orange4": (135, 95, 0),
|
"dimgrey": (105, 105, 105),
|
||||||
"grey37": (95, 95, 95),
|
"dodgerblue": (30, 144, 255),
|
||||||
"gray37": (95, 95, 95),
|
"firebrick": (178, 34, 34),
|
||||||
"medium_purple4": (95, 95, 135),
|
"floralwhite": (255, 250, 240),
|
||||||
"slate_blue3": (95, 95, 215),
|
"forestgreen": (34, 139, 34),
|
||||||
"royal_blue1": (95, 95, 255),
|
"gainsboro": (220, 220, 220),
|
||||||
"chartreuse4": (95, 135, 0),
|
"ghostwhite": (248, 248, 255),
|
||||||
"dark_sea_green4": (95, 175, 95),
|
"gold": (255, 215, 0),
|
||||||
"pale_turquoise4": (95, 135, 135),
|
"goldenrod": (218, 165, 32),
|
||||||
"steel_blue": (95, 135, 175),
|
"greenyellow": (173, 255, 47),
|
||||||
"steel_blue3": (95, 135, 215),
|
"grey": (128, 128, 128),
|
||||||
"cornflower_blue": (95, 135, 255),
|
"honeydew": (240, 255, 240),
|
||||||
"chartreuse3": (95, 215, 0),
|
"hotpink": (255, 105, 180),
|
||||||
"cadet_blue": (95, 175, 175),
|
"indianred": (205, 92, 92),
|
||||||
"sky_blue3": (95, 175, 215),
|
"indigo": (75, 0, 130),
|
||||||
"steel_blue1": (95, 215, 255),
|
"ivory": (255, 255, 240),
|
||||||
"pale_green3": (135, 215, 135),
|
"khaki": (240, 230, 140),
|
||||||
"sea_green3": (95, 215, 135),
|
"lavender": (230, 230, 250),
|
||||||
"aquamarine3": (95, 215, 175),
|
"lavenderblush": (255, 240, 245),
|
||||||
"medium_turquoise": (95, 215, 215),
|
"lawngreen": (124, 252, 0),
|
||||||
"chartreuse2": (135, 215, 0),
|
"lemonchiffon": (255, 250, 205),
|
||||||
"sea_green2": (95, 255, 95),
|
"lightblue": (173, 216, 230),
|
||||||
"sea_green1": (95, 255, 175),
|
"lightcoral": (240, 128, 128),
|
||||||
"aquamarine1": (135, 255, 215),
|
"lightcyan": (224, 255, 255),
|
||||||
"dark_slate_gray2": (95, 255, 255),
|
"lightgoldenrodyellow": (250, 250, 210),
|
||||||
"dark_magenta": (135, 0, 175),
|
"lightgray": (211, 211, 211),
|
||||||
"dark_violet": (175, 0, 215),
|
"lightgreen": (144, 238, 144),
|
||||||
"purple": (175, 0, 255),
|
"lightgrey": (211, 211, 211),
|
||||||
"light_pink4": (135, 95, 95),
|
"lightpink": (255, 182, 193),
|
||||||
"plum4": (135, 95, 135),
|
"lightsalmon": (255, 160, 122),
|
||||||
"medium_purple3": (135, 95, 215),
|
"lightseagreen": (32, 178, 170),
|
||||||
"slate_blue1": (135, 95, 255),
|
"lightskyblue": (135, 206, 250),
|
||||||
"yellow4": (135, 175, 0),
|
"lightslategray": (119, 136, 153),
|
||||||
"wheat4": (135, 135, 95),
|
"lightslategrey": (119, 136, 153),
|
||||||
"grey53": (135, 135, 135),
|
"lightsteelblue": (176, 196, 222),
|
||||||
"gray53": (135, 135, 135),
|
"lightyellow": (255, 255, 224),
|
||||||
"light_slate_grey": (135, 135, 175),
|
"limegreen": (50, 205, 50),
|
||||||
"light_slate_gray": (135, 135, 175),
|
"linen": (250, 240, 230),
|
||||||
"medium_purple": (135, 135, 215),
|
"magenta": (255, 0, 255),
|
||||||
"light_slate_blue": (135, 135, 255),
|
"mediumaquamarine": (102, 205, 170),
|
||||||
"dark_olive_green3": (175, 215, 95),
|
"mediumblue": (0, 0, 205),
|
||||||
"dark_sea_green": (135, 175, 135),
|
"mediumorchid": (186, 85, 211),
|
||||||
"light_sky_blue3": (135, 175, 215),
|
"mediumpurple": (147, 112, 219),
|
||||||
"sky_blue2": (135, 175, 255),
|
"mediumseagreen": (60, 179, 113),
|
||||||
"dark_sea_green3": (175, 215, 135),
|
"mediumslateblue": (123, 104, 238),
|
||||||
"dark_slate_gray3": (135, 215, 215),
|
"mediumspringgreen": (0, 250, 154),
|
||||||
"sky_blue1": (135, 215, 255),
|
"mediumturquoise": (72, 209, 204),
|
||||||
"chartreuse1": (135, 255, 0),
|
"mediumvioletred": (199, 21, 133),
|
||||||
"light_green": (135, 255, 135),
|
"midnightblue": (25, 25, 112),
|
||||||
"pale_green1": (175, 255, 135),
|
"mintcream": (245, 255, 250),
|
||||||
"dark_slate_gray1": (135, 255, 255),
|
"mistyrose": (255, 228, 225),
|
||||||
"red3": (215, 0, 0),
|
"moccasin": (255, 228, 181),
|
||||||
"medium_violet_red": (175, 0, 135),
|
"navajowhite": (255, 222, 173),
|
||||||
"magenta3": (215, 0, 215),
|
"oldlace": (253, 245, 230),
|
||||||
"dark_orange3": (215, 95, 0),
|
"olivedrab": (107, 142, 35),
|
||||||
"indian_red": (215, 95, 95),
|
"orangered": (255, 69, 0),
|
||||||
"hot_pink3": (215, 95, 135),
|
"orchid": (218, 112, 214),
|
||||||
"medium_orchid3": (175, 95, 175),
|
"palegoldenrod": (238, 232, 170),
|
||||||
"medium_orchid": (175, 95, 215),
|
"palegreen": (152, 251, 152),
|
||||||
"medium_purple2": (175, 135, 215),
|
"paleturquoise": (175, 238, 238),
|
||||||
"dark_goldenrod": (175, 135, 0),
|
"palevioletred": (219, 112, 147),
|
||||||
"light_salmon3": (215, 135, 95),
|
"papayawhip": (255, 239, 213),
|
||||||
"rosy_brown": (175, 135, 135),
|
"peachpuff": (255, 218, 185),
|
||||||
"grey63": (175, 135, 175),
|
"peru": (205, 133, 63),
|
||||||
"gray63": (175, 135, 175),
|
"pink": (255, 192, 203),
|
||||||
"medium_purple1": (175, 135, 255),
|
"plum": (221, 160, 221),
|
||||||
"gold3": (215, 175, 0),
|
"powderblue": (176, 224, 230),
|
||||||
"dark_khaki": (175, 175, 95),
|
"rosybrown": (188, 143, 143),
|
||||||
"navajo_white3": (175, 175, 135),
|
"royalblue": (65, 105, 225),
|
||||||
"grey69": (175, 175, 175),
|
"saddlebrown": (139, 69, 19),
|
||||||
"gray69": (175, 175, 175),
|
"salmon": (250, 128, 114),
|
||||||
"light_steel_blue3": (175, 175, 215),
|
"sandybrown": (244, 164, 96),
|
||||||
"light_steel_blue": (175, 175, 255),
|
"seagreen": (46, 139, 87),
|
||||||
"yellow3": (215, 215, 0),
|
"seashell": (255, 245, 238),
|
||||||
"dark_sea_green2": (175, 255, 175),
|
"sienna": (160, 82, 45),
|
||||||
"light_cyan3": (175, 215, 215),
|
"skyblue": (135, 206, 235),
|
||||||
"light_sky_blue1": (175, 215, 255),
|
"slateblue": (106, 90, 205),
|
||||||
"green_yellow": (175, 255, 0),
|
"slategray": (112, 128, 144),
|
||||||
"dark_olive_green2": (175, 255, 95),
|
"slategrey": (112, 128, 144),
|
||||||
"dark_sea_green1": (215, 255, 175),
|
"snow": (255, 250, 250),
|
||||||
"pale_turquoise1": (175, 255, 255),
|
"springgreen": (0, 255, 127),
|
||||||
"deep_pink3": (215, 0, 135),
|
"steelblue": (70, 130, 180),
|
||||||
"magenta2": (255, 0, 215),
|
"tan": (210, 180, 140),
|
||||||
"hot_pink2": (215, 95, 175),
|
"thistle": (216, 191, 216),
|
||||||
"orchid": (215, 95, 215),
|
"tomato": (255, 99, 71),
|
||||||
"medium_orchid1": (255, 95, 255),
|
"turquoise": (64, 224, 208),
|
||||||
"orange3": (215, 135, 0),
|
"violet": (238, 130, 238),
|
||||||
"light_pink3": (215, 135, 135),
|
"wheat": (245, 222, 179),
|
||||||
"pink3": (215, 135, 175),
|
"whitesmoke": (245, 245, 245),
|
||||||
"plum3": (215, 135, 215),
|
"yellowgreen": (154, 205, 50),
|
||||||
"violet": (215, 135, 255),
|
"rebeccapurple": (102, 51, 153),
|
||||||
"light_goldenrod3": (215, 175, 95),
|
|
||||||
"tan": (215, 175, 135),
|
|
||||||
"misty_rose3": (215, 175, 175),
|
|
||||||
"thistle3": (215, 175, 215),
|
|
||||||
"plum2": (215, 175, 255),
|
|
||||||
"khaki3": (215, 215, 95),
|
|
||||||
"light_goldenrod2": (255, 215, 135),
|
|
||||||
"light_yellow3": (215, 215, 175),
|
|
||||||
"grey84": (215, 215, 215),
|
|
||||||
"gray84": (215, 215, 215),
|
|
||||||
"light_steel_blue1": (215, 215, 255),
|
|
||||||
"yellow2": (215, 255, 0),
|
|
||||||
"dark_olive_green1": (215, 255, 135),
|
|
||||||
"honeydew2": (215, 255, 215),
|
|
||||||
"light_cyan1": (215, 255, 255),
|
|
||||||
"red1": (255, 0, 0),
|
|
||||||
"deep_pink2": (255, 0, 95),
|
|
||||||
"deep_pink1": (255, 0, 175),
|
|
||||||
"magenta1": (255, 0, 255),
|
|
||||||
"orange_red1": (255, 95, 0),
|
|
||||||
"indian_red1": (255, 95, 135),
|
|
||||||
"hot_pink": (255, 95, 215),
|
|
||||||
"dark_orange": (255, 135, 0),
|
|
||||||
"salmon1": (255, 135, 95),
|
|
||||||
"light_coral": (255, 135, 135),
|
|
||||||
"pale_violet_red1": (255, 135, 175),
|
|
||||||
"orchid2": (255, 135, 215),
|
|
||||||
"orchid1": (255, 135, 255),
|
|
||||||
"orange1": (255, 175, 0),
|
|
||||||
"sandy_brown": (255, 175, 95),
|
|
||||||
"light_salmon1": (255, 175, 135),
|
|
||||||
"light_pink1": (255, 175, 175),
|
|
||||||
"pink1": (255, 175, 215),
|
|
||||||
"plum1": (255, 175, 255),
|
|
||||||
"gold1": (255, 215, 0),
|
|
||||||
"navajo_white1": (255, 215, 175),
|
|
||||||
"misty_rose1": (255, 215, 215),
|
|
||||||
"thistle1": (255, 215, 255),
|
|
||||||
"yellow1": (255, 255, 0),
|
|
||||||
"light_goldenrod1": (255, 255, 95),
|
|
||||||
"khaki1": (255, 255, 135),
|
|
||||||
"wheat1": (255, 255, 175),
|
|
||||||
"cornsilk1": (255, 255, 215),
|
|
||||||
"grey100": (255, 255, 255),
|
|
||||||
"gray100": (255, 255, 255),
|
|
||||||
"grey3": (8, 8, 8),
|
|
||||||
"gray3": (8, 8, 8),
|
|
||||||
"grey7": (18, 18, 18),
|
|
||||||
"gray7": (18, 18, 18),
|
|
||||||
"grey11": (28, 28, 28),
|
|
||||||
"gray11": (28, 28, 28),
|
|
||||||
"grey15": (38, 38, 38),
|
|
||||||
"gray15": (38, 38, 38),
|
|
||||||
"grey19": (48, 48, 48),
|
|
||||||
"gray19": (48, 48, 48),
|
|
||||||
"grey23": (58, 58, 58),
|
|
||||||
"gray23": (58, 58, 58),
|
|
||||||
"grey27": (68, 68, 68),
|
|
||||||
"gray27": (68, 68, 68),
|
|
||||||
"grey30": (78, 78, 78),
|
|
||||||
"gray30": (78, 78, 78),
|
|
||||||
"grey35": (88, 88, 88),
|
|
||||||
"gray35": (88, 88, 88),
|
|
||||||
"grey39": (98, 98, 98),
|
|
||||||
"gray39": (98, 98, 98),
|
|
||||||
"grey42": (108, 108, 108),
|
|
||||||
"gray42": (108, 108, 108),
|
|
||||||
"grey46": (118, 118, 118),
|
|
||||||
"gray46": (118, 118, 118),
|
|
||||||
"grey50": (128, 128, 128),
|
|
||||||
"gray50": (128, 128, 128),
|
|
||||||
"grey54": (138, 138, 138),
|
|
||||||
"gray54": (138, 138, 138),
|
|
||||||
"grey58": (148, 148, 148),
|
|
||||||
"gray58": (148, 148, 148),
|
|
||||||
"grey62": (158, 158, 158),
|
|
||||||
"gray62": (158, 158, 158),
|
|
||||||
"grey66": (168, 168, 168),
|
|
||||||
"gray66": (168, 168, 168),
|
|
||||||
"grey70": (178, 178, 178),
|
|
||||||
"gray70": (178, 178, 178),
|
|
||||||
"grey74": (188, 188, 188),
|
|
||||||
"gray74": (188, 188, 188),
|
|
||||||
"grey78": (198, 198, 198),
|
|
||||||
"gray78": (198, 198, 198),
|
|
||||||
"grey82": (208, 208, 208),
|
|
||||||
"gray82": (208, 208, 208),
|
|
||||||
"grey85": (218, 218, 218),
|
|
||||||
"gray85": (218, 218, 218),
|
|
||||||
"grey89": (228, 228, 228),
|
|
||||||
"gray89": (228, 228, 228),
|
|
||||||
"grey93": (238, 238, 238),
|
|
||||||
"gray93": (238, 238, 238),
|
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/textual/_layout.py
Normal file
40
src/textual/_layout.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import ClassVar, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
|
from .geometry import Region, Offset, Size
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
|
class WidgetPlacement(NamedTuple):
|
||||||
|
"""The position, size, and relative order of a widget within its parent."""
|
||||||
|
|
||||||
|
region: Region
|
||||||
|
widget: Widget | None = None # A widget of None means empty space
|
||||||
|
order: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
class Layout(ABC):
|
||||||
|
"""Responsible for arranging Widgets in a view and rendering them."""
|
||||||
|
|
||||||
|
name: ClassVar[str] = ""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def arrange(
|
||||||
|
self, parent: Widget, size: Size, scroll: Offset
|
||||||
|
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
||||||
|
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
parent (Widget): Parent widget.
|
||||||
|
size (Size): Size of container.
|
||||||
|
scroll (Offset): Offset to apply to the Widget placements.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Iterable[WidgetPlacement]: An iterable of widget location
|
||||||
|
"""
|
||||||
@@ -9,7 +9,7 @@ import warnings
|
|||||||
from asyncio import AbstractEventLoop
|
from asyncio import AbstractEventLoop
|
||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
from time import perf_counter
|
from time import perf_counter
|
||||||
from typing import Any, Iterable, TextIO, Type, TYPE_CHECKING
|
from typing import Any, Generic, Iterable, TextIO, Type, TypeVar, TYPE_CHECKING
|
||||||
|
|
||||||
import rich
|
import rich
|
||||||
import rich.repr
|
import rich.repr
|
||||||
@@ -67,6 +67,8 @@ DEFAULT_COLORS = ColorSystem(
|
|||||||
dark_surface="#292929",
|
dark_surface="#292929",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ComposeResult = Iterable[Widget]
|
||||||
|
|
||||||
|
|
||||||
class AppError(Exception):
|
class AppError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -76,8 +78,11 @@ class ActionError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
ReturnType = TypeVar("ReturnType")
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class App(DOMNode):
|
class App(Generic[ReturnType], DOMNode):
|
||||||
"""The base class for Textual Applications"""
|
"""The base class for Textual Applications"""
|
||||||
|
|
||||||
css = ""
|
css = ""
|
||||||
@@ -159,6 +164,8 @@ class App(DOMNode):
|
|||||||
|
|
||||||
self.devtools = DevtoolsClient()
|
self.devtools = DevtoolsClient()
|
||||||
|
|
||||||
|
self._return_value: ReturnType | None = None
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
title: Reactive[str] = Reactive("Textual")
|
title: Reactive[str] = Reactive("Textual")
|
||||||
@@ -166,6 +173,20 @@ class App(DOMNode):
|
|||||||
background: Reactive[str] = Reactive("black")
|
background: Reactive[str] = Reactive("black")
|
||||||
dark = Reactive(False)
|
dark = Reactive(False)
|
||||||
|
|
||||||
|
def exit(self, result: ReturnType | None = None) -> None:
|
||||||
|
"""Exit the app, and return the supplied result.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result (ReturnType | None, optional): Return value. Defaults to None.
|
||||||
|
"""
|
||||||
|
self._return_value = result
|
||||||
|
self.close_messages_no_wait()
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Yield child widgets for a container."""
|
||||||
|
return
|
||||||
|
yield
|
||||||
|
|
||||||
def get_css_variables(self) -> dict[str, str]:
|
def get_css_variables(self) -> dict[str, str]:
|
||||||
"""Get a mapping of variables used to pre-populate CSS.
|
"""Get a mapping of variables used to pre-populate CSS.
|
||||||
|
|
||||||
@@ -284,27 +305,9 @@ class App(DOMNode):
|
|||||||
keys, action, description, show=show, key_display=key_display
|
keys, action, description, show=show, key_display=key_display
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
def run(self, loop: AbstractEventLoop | None = None) -> ReturnType | None:
|
||||||
def run(
|
|
||||||
cls,
|
|
||||||
console: Console | None = None,
|
|
||||||
screen: bool = True,
|
|
||||||
driver: Type[Driver] | None = None,
|
|
||||||
loop: AbstractEventLoop | None = None,
|
|
||||||
**kwargs,
|
|
||||||
):
|
|
||||||
"""Run the app.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
console (Console, optional): Console object. Defaults to None.
|
|
||||||
screen (bool, optional): Enable application mode. Defaults to True.
|
|
||||||
driver (Type[Driver], optional): Driver class or None for default. Defaults to None.
|
|
||||||
loop (AbstractEventLoop): Event loop to run the application on. If not specified, uvloop will be used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def run_app() -> None:
|
async def run_app() -> None:
|
||||||
app = cls(screen=screen, driver_class=driver, **kwargs)
|
await self.process_messages()
|
||||||
await app.process_messages()
|
|
||||||
|
|
||||||
if loop:
|
if loop:
|
||||||
asyncio.set_event_loop(loop)
|
asyncio.set_event_loop(loop)
|
||||||
@@ -322,13 +325,15 @@ class App(DOMNode):
|
|||||||
finally:
|
finally:
|
||||||
event_loop.close()
|
event_loop.close()
|
||||||
|
|
||||||
|
return self._return_value
|
||||||
|
|
||||||
async def _on_css_change(self) -> None:
|
async def _on_css_change(self) -> None:
|
||||||
"""Called when the CSS changes (if watch_css is True)."""
|
"""Called when the CSS changes (if watch_css is True)."""
|
||||||
if self.css_file is not None:
|
if self.css_file is not None:
|
||||||
stylesheet = Stylesheet(variables=self.get_css_variables())
|
|
||||||
try:
|
try:
|
||||||
time = perf_counter()
|
time = perf_counter()
|
||||||
stylesheet.read(self.css_file)
|
self.stylesheet.read(self.css_file)
|
||||||
elapsed = (perf_counter() - time) * 1000
|
elapsed = (perf_counter() - time) * 1000
|
||||||
self.log(f"loaded {self.css_file} in {elapsed:.0f}ms")
|
self.log(f"loaded {self.css_file} in {elapsed:.0f}ms")
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
@@ -337,10 +342,12 @@ class App(DOMNode):
|
|||||||
self.log(error)
|
self.log(error)
|
||||||
else:
|
else:
|
||||||
self.reset_styles()
|
self.reset_styles()
|
||||||
self.stylesheet = stylesheet
|
|
||||||
self.stylesheet.update(self)
|
self.stylesheet.update(self)
|
||||||
self.screen.refresh(layout=True)
|
self.screen.refresh(layout=True)
|
||||||
|
|
||||||
|
def render(self) -> RenderableType:
|
||||||
|
return ""
|
||||||
|
|
||||||
def query(self, selector: str | None = None) -> DOMQuery:
|
def query(self, selector: str | None = None) -> DOMQuery:
|
||||||
"""Get a DOM query in the current screen.
|
"""Get a DOM query in the current screen.
|
||||||
|
|
||||||
@@ -498,7 +505,9 @@ class App(DOMNode):
|
|||||||
if self.css_file is not None:
|
if self.css_file is not None:
|
||||||
self.stylesheet.read(self.css_file)
|
self.stylesheet.read(self.css_file)
|
||||||
if self.css is not None:
|
if self.css is not None:
|
||||||
self.stylesheet.parse(self.css, path=f"<{self.__class__.__name__}>")
|
self.stylesheet.add_source(
|
||||||
|
self.css, path=f"<{self.__class__.__name__}>"
|
||||||
|
)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.on_exception(error)
|
self.on_exception(error)
|
||||||
self._print_error_renderables()
|
self._print_error_renderables()
|
||||||
@@ -521,6 +530,7 @@ class App(DOMNode):
|
|||||||
mount_event = events.Mount(sender=self)
|
mount_event = events.Mount(sender=self)
|
||||||
await self.dispatch_message(mount_event)
|
await self.dispatch_message(mount_event)
|
||||||
|
|
||||||
|
# TODO: don't override `self.console` here
|
||||||
self.console = Console(file=sys.__stdout__)
|
self.console = Console(file=sys.__stdout__)
|
||||||
self.title = self._title
|
self.title = self._title
|
||||||
self.refresh()
|
self.refresh()
|
||||||
@@ -547,6 +557,12 @@ class App(DOMNode):
|
|||||||
if self._log_file is not None:
|
if self._log_file is not None:
|
||||||
self._log_file.close()
|
self._log_file.close()
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
widgets = list(self.compose())
|
||||||
|
if widgets:
|
||||||
|
self.mount(*widgets)
|
||||||
|
self.screen.refresh()
|
||||||
|
|
||||||
async def on_idle(self) -> None:
|
async def on_idle(self) -> None:
|
||||||
"""Perform actions when there are no messages in the queue."""
|
"""Perform actions when there are no messages in the queue."""
|
||||||
if self._require_styles_update:
|
if self._require_styles_update:
|
||||||
@@ -558,6 +574,7 @@ class App(DOMNode):
|
|||||||
parent.children._append(child)
|
parent.children._append(child)
|
||||||
self.registry.add(child)
|
self.registry.add(child)
|
||||||
child.set_parent(parent)
|
child.set_parent(parent)
|
||||||
|
child.on_register(self)
|
||||||
child.start_messages()
|
child.start_messages()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from rich.style import Style
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
|
|
||||||
from ._color_constants import ANSI_COLOR_TO_RGB
|
from ._color_constants import COLOR_NAME_TO_RGB
|
||||||
from .geometry import clamp
|
from .geometry import clamp
|
||||||
|
|
||||||
|
|
||||||
@@ -123,6 +123,11 @@ class Color(NamedTuple):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_transparent(self) -> bool:
|
||||||
|
"""Check if the color is transparent, i.e. has 0 alpha."""
|
||||||
|
return self.a == 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def clamped(self) -> Color:
|
def clamped(self) -> Color:
|
||||||
"""Get a color with all components saturated to maximum and minimum values."""
|
"""Get a color with all components saturated to maximum and minimum values."""
|
||||||
@@ -253,9 +258,9 @@ class Color(NamedTuple):
|
|||||||
"""
|
"""
|
||||||
if isinstance(color_text, Color):
|
if isinstance(color_text, Color):
|
||||||
return color_text
|
return color_text
|
||||||
ansi_color = ANSI_COLOR_TO_RGB.get(color_text)
|
color_from_name = COLOR_NAME_TO_RGB.get(color_text)
|
||||||
if ansi_color is not None:
|
if color_from_name is not None:
|
||||||
return cls(*ansi_color)
|
return cls(*color_from_name)
|
||||||
color_match = RE_COLOR.match(color_text)
|
color_match = RE_COLOR.match(color_text)
|
||||||
if color_match is None:
|
if color_match is None:
|
||||||
raise ColorParseError(f"failed to parse {color_text!r} as a color")
|
raise ColorParseError(f"failed to parse {color_text!r} as a color")
|
||||||
@@ -329,6 +334,7 @@ class Color(NamedTuple):
|
|||||||
# Color constants
|
# Color constants
|
||||||
WHITE = Color(255, 255, 255)
|
WHITE = Color(255, 255, 255)
|
||||||
BLACK = Color(0, 0, 0)
|
BLACK = Color(0, 0, 0)
|
||||||
|
TRANSPARENT = Color(0, 0, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
class ColorPair(NamedTuple):
|
class ColorPair(NamedTuple):
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ from .transition import Transition
|
|||||||
from ..geometry import Spacing, SpacingDimensions, clamp
|
from ..geometry import Spacing, SpacingDimensions, clamp
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..layout import Layout
|
from .._layout import Layout
|
||||||
from .styles import DockGroup, Styles, StylesBase
|
from .styles import DockGroup, Styles, StylesBase
|
||||||
|
|
||||||
from .types import EdgeType
|
from .types import EdgeType
|
||||||
|
|||||||
@@ -686,31 +686,37 @@ class StylesBuilder:
|
|||||||
elif token_horizontal.value not in VALID_ALIGN_VERTICAL:
|
elif token_horizontal.value not in VALID_ALIGN_VERTICAL:
|
||||||
align_error(name, token_horizontal)
|
align_error(name, token_horizontal)
|
||||||
|
|
||||||
self.styles._rules["align_horizontal"] = token_horizontal.value
|
name = name.replace("-", "_")
|
||||||
self.styles._rules["align_vertical"] = token_vertical.value
|
self.styles._rules[f"{name}_horizontal"] = token_horizontal.value
|
||||||
|
self.styles._rules[f"{name}_vertical"] = token_vertical.value
|
||||||
|
|
||||||
def process_align_horizontal(self, name: str, tokens: list[Token]) -> None:
|
def process_align_horizontal(self, name: str, tokens: list[Token]) -> None:
|
||||||
try:
|
try:
|
||||||
value = self._process_enum(name, tokens, VALID_ALIGN_HORIZONTAL)
|
value = self._process_enum(name, tokens, VALID_ALIGN_HORIZONTAL)
|
||||||
self.styles._rules["align_horizontal"] = value
|
self.styles._rules[name.replace("-", "_")] = value
|
||||||
except StyleValueError:
|
except StyleValueError:
|
||||||
self.error(
|
self.error(
|
||||||
name,
|
name,
|
||||||
tokens[0],
|
tokens[0],
|
||||||
string_enum_help_text(
|
string_enum_help_text(
|
||||||
"align-horizontal", VALID_ALIGN_HORIZONTAL, context="css"
|
name, VALID_ALIGN_HORIZONTAL, context="css"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_align_vertical(self, name: str, tokens: list[Token]) -> None:
|
def process_align_vertical(self, name: str, tokens: list[Token]) -> None:
|
||||||
try:
|
try:
|
||||||
value = self._process_enum(name, tokens, VALID_ALIGN_VERTICAL)
|
value = self._process_enum(name, tokens, VALID_ALIGN_VERTICAL)
|
||||||
self.styles._rules["align_vertical"] = value
|
self.styles._rules[name.replace("-", "_")] = value
|
||||||
except StyleValueError:
|
except StyleValueError:
|
||||||
self.error(
|
self.error(
|
||||||
name,
|
name,
|
||||||
tokens[0],
|
tokens[0],
|
||||||
string_enum_help_text(
|
string_enum_help_text(
|
||||||
"align-vertical", VALID_ALIGN_VERTICAL, context="css"
|
name, VALID_ALIGN_VERTICAL, context="css"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
process_content_align = process_align
|
||||||
|
process_content_align_horizontal = process_align_horizontal
|
||||||
|
process_content_align_vertical = process_align_vertical
|
||||||
|
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ if __name__ == "__main__":
|
|||||||
console = Console()
|
console = Console()
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
try:
|
try:
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
except StylesheetParseError as e:
|
except StylesheetParseError as e:
|
||||||
console.print(e.errors)
|
console.print(e.errors)
|
||||||
print(stylesheet)
|
print(stylesheet)
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ else:
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..dom import DOMNode
|
from ..dom import DOMNode
|
||||||
from ..layout import Layout
|
from .._layout import Layout
|
||||||
|
|
||||||
|
|
||||||
class RulesMap(TypedDict, total=False):
|
class RulesMap(TypedDict, total=False):
|
||||||
@@ -126,6 +126,9 @@ class RulesMap(TypedDict, total=False):
|
|||||||
align_horizontal: AlignHorizontal
|
align_horizontal: AlignHorizontal
|
||||||
align_vertical: AlignVertical
|
align_vertical: AlignVertical
|
||||||
|
|
||||||
|
content_align_horizontal: AlignHorizontal
|
||||||
|
content_align_vertical: AlignVertical
|
||||||
|
|
||||||
|
|
||||||
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
||||||
RULE_NAMES_SET = frozenset(RULE_NAMES)
|
RULE_NAMES_SET = frozenset(RULE_NAMES)
|
||||||
@@ -166,7 +169,7 @@ class StylesBase(ABC):
|
|||||||
layout = LayoutProperty()
|
layout = LayoutProperty()
|
||||||
|
|
||||||
color = ColorProperty(Color(255, 255, 255))
|
color = ColorProperty(Color(255, 255, 255))
|
||||||
background = ColorProperty(Color(0, 0, 0))
|
background = ColorProperty(Color(0, 0, 0, 0))
|
||||||
text_style = StyleFlagsProperty()
|
text_style = StyleFlagsProperty()
|
||||||
|
|
||||||
opacity = FractionalProperty()
|
opacity = FractionalProperty()
|
||||||
@@ -207,9 +210,9 @@ class StylesBase(ABC):
|
|||||||
|
|
||||||
rich_style = StyleProperty()
|
rich_style = StyleProperty()
|
||||||
|
|
||||||
scrollbar_color = ColorProperty("bright_magenta")
|
scrollbar_color = ColorProperty("ansi_bright_magenta")
|
||||||
scrollbar_color_hover = ColorProperty("yellow")
|
scrollbar_color_hover = ColorProperty("ansi_yellow")
|
||||||
scrollbar_color_active = ColorProperty("bright_yellow")
|
scrollbar_color_active = ColorProperty("ansi_bright_yellow")
|
||||||
|
|
||||||
scrollbar_background = ColorProperty("#555555")
|
scrollbar_background = ColorProperty("#555555")
|
||||||
scrollbar_background_hover = ColorProperty("#444444")
|
scrollbar_background_hover = ColorProperty("#444444")
|
||||||
@@ -218,6 +221,9 @@ class StylesBase(ABC):
|
|||||||
align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
|
align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
|
||||||
align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
|
align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
|
||||||
|
|
||||||
|
content_align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
|
||||||
|
content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
|
||||||
|
|
||||||
def __eq__(self, styles: object) -> bool:
|
def __eq__(self, styles: object) -> bool:
|
||||||
"""Check that Styles contains the same rules."""
|
"""Check that Styles contains the same rules."""
|
||||||
if not isinstance(styles, StylesBase):
|
if not isinstance(styles, StylesBase):
|
||||||
@@ -673,6 +679,18 @@ class Styles(StylesBase):
|
|||||||
elif has_rule("align_horizontal"):
|
elif has_rule("align_horizontal"):
|
||||||
append_declaration("align-vertical", self.align_vertical)
|
append_declaration("align-vertical", self.align_vertical)
|
||||||
|
|
||||||
|
if has_rule("content_align_horizontal") and has_rule("content_align_vertical"):
|
||||||
|
append_declaration(
|
||||||
|
"content-align",
|
||||||
|
f"{self.content_align_horizontal} {self.content_align_vertical}",
|
||||||
|
)
|
||||||
|
elif has_rule("content_align_horizontal"):
|
||||||
|
append_declaration(
|
||||||
|
"content-align-horizontal", self.content_align_horizontal
|
||||||
|
)
|
||||||
|
elif has_rule("content_align_horizontal"):
|
||||||
|
append_declaration("content-align-vertical", self.content_align_vertical)
|
||||||
|
|
||||||
lines.sort()
|
lines.sort()
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
|
|||||||
@@ -120,21 +120,25 @@ class StylesheetErrors:
|
|||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Stylesheet:
|
class Stylesheet:
|
||||||
def __init__(self, *, variables: dict[str, str] | None = None) -> None:
|
def __init__(self, *, variables: dict[str, str] | None = None) -> None:
|
||||||
self.rules: list[RuleSet] = []
|
self._rules: list[RuleSet] = []
|
||||||
self.variables = variables or {}
|
self.variables = variables or {}
|
||||||
self.source: list[tuple[str, str]] = []
|
self.source: dict[str, str] = {}
|
||||||
|
self._require_parse = False
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield self.rules
|
yield self.rules
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def css(self) -> str:
|
def rules(self) -> list[RuleSet]:
|
||||||
return "\n\n".join(rule_set.css for rule_set in self.rules)
|
if self._require_parse:
|
||||||
|
self.parse()
|
||||||
|
self._require_parse = False
|
||||||
|
assert self._rules is not None
|
||||||
|
return self._rules
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def any_errors(self) -> bool:
|
def css(self) -> str:
|
||||||
"""Check if there are any errors."""
|
return "\n\n".join(rule_set.css for rule_set in self.rules)
|
||||||
return any(rule.errors for rule in self.rules)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def error_renderable(self) -> StylesheetErrors:
|
def error_renderable(self) -> StylesheetErrors:
|
||||||
@@ -148,6 +152,28 @@ class Stylesheet:
|
|||||||
"""
|
"""
|
||||||
self.variables = variables
|
self.variables = variables
|
||||||
|
|
||||||
|
def _parse_rules(self, css: str, path: str) -> list[RuleSet]:
|
||||||
|
"""Parse CSS and return rules.
|
||||||
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
css (str): String containing Textual CSS.
|
||||||
|
path (str): Path to CSS or unique identifier
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
StylesheetError: If the CSS is invalid.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list[RuleSet]: List of RuleSets.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rules = list(parse(css, path, variables=self.variables))
|
||||||
|
except TokenizeError:
|
||||||
|
raise
|
||||||
|
except Exception as error:
|
||||||
|
raise StylesheetError(f"failed to parse css; {error}")
|
||||||
|
return rules
|
||||||
|
|
||||||
def read(self, filename: str) -> None:
|
def read(self, filename: str) -> None:
|
||||||
"""Read Textual CSS file.
|
"""Read Textual CSS file.
|
||||||
|
|
||||||
@@ -165,19 +191,10 @@ class Stylesheet:
|
|||||||
path = os.path.abspath(filename)
|
path = os.path.abspath(filename)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise StylesheetError(f"unable to read {filename!r}; {error}")
|
raise StylesheetError(f"unable to read {filename!r}; {error}")
|
||||||
try:
|
self.source[path] = css
|
||||||
rules = list(parse(css, path, variables=self.variables))
|
self._require_parse = True
|
||||||
except TokenizeError:
|
|
||||||
raise
|
|
||||||
except Exception as error:
|
|
||||||
raise StylesheetError(f"failed to parse {filename!r}; {error!r}")
|
|
||||||
else:
|
|
||||||
self.source.append((css, path))
|
|
||||||
self.rules.extend(rules)
|
|
||||||
if self.any_errors:
|
|
||||||
raise StylesheetParseError(self.error_renderable)
|
|
||||||
|
|
||||||
def parse(self, css: str, *, path: str = "") -> None:
|
def add_source(self, css: str, path: str | None = None) -> None:
|
||||||
"""Parse CSS from a string.
|
"""Parse CSS from a string.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -188,26 +205,31 @@ class Stylesheet:
|
|||||||
StylesheetError: If the CSS could not be read.
|
StylesheetError: If the CSS could not be read.
|
||||||
StylesheetParseError: If the CSS is invalid.
|
StylesheetParseError: If the CSS is invalid.
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
rules = list(parse(css, path, variables=self.variables))
|
|
||||||
except TokenizeError:
|
|
||||||
raise
|
|
||||||
except Exception as error:
|
|
||||||
raise StylesheetError(f"failed to parse css; {error}")
|
|
||||||
else:
|
|
||||||
self.source.append((css, path))
|
|
||||||
self.rules.extend(rules)
|
|
||||||
if self.any_errors:
|
|
||||||
raise StylesheetParseError(self.error_renderable)
|
|
||||||
|
|
||||||
def _clone(self, stylesheet: Stylesheet) -> None:
|
if path is None:
|
||||||
"""Replace this stylesheet contents with another.
|
path = str(hash(css))
|
||||||
|
if path in self.source and self.source[path] == css:
|
||||||
|
# Path already in source, and CSS is identical
|
||||||
|
return
|
||||||
|
|
||||||
Args:
|
self.source[path] = css
|
||||||
stylesheet (Stylesheet): A Stylesheet.
|
self._require_parse = True
|
||||||
|
|
||||||
|
def parse(self) -> None:
|
||||||
|
"""Parse the source in the stylesheet.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
StylesheetParseError: If there are any CSS related errors.
|
||||||
"""
|
"""
|
||||||
self.rules = stylesheet.rules.copy()
|
rules: list[RuleSet] = []
|
||||||
self.source = stylesheet.source.copy()
|
add_rules = rules.extend
|
||||||
|
for path, css in self.source.items():
|
||||||
|
css_rules = self._parse_rules(css, path)
|
||||||
|
if any(rule.errors for rule in css_rules):
|
||||||
|
raise StylesheetParseError(self.error_renderable)
|
||||||
|
add_rules(css_rules)
|
||||||
|
self._rules = rules
|
||||||
|
self._require_parse = False
|
||||||
|
|
||||||
def reparse(self) -> None:
|
def reparse(self) -> None:
|
||||||
"""Re-parse source, applying new variables.
|
"""Re-parse source, applying new variables.
|
||||||
@@ -219,9 +241,11 @@ class Stylesheet:
|
|||||||
"""
|
"""
|
||||||
# Do this in a fresh Stylesheet so if there are errors we don't break self.
|
# Do this in a fresh Stylesheet so if there are errors we don't break self.
|
||||||
stylesheet = Stylesheet(variables=self.variables)
|
stylesheet = Stylesheet(variables=self.variables)
|
||||||
for css, path in self.source:
|
for path, css in self.source.items():
|
||||||
stylesheet.parse(css, path=path)
|
stylesheet.add_source(css, path)
|
||||||
self._clone(stylesheet)
|
stylesheet.parse()
|
||||||
|
self.rules = stylesheet.rules
|
||||||
|
self.source = stylesheet.source
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check_rule(cls, rule: RuleSet, node: DOMNode) -> Iterable[Specificity3]:
|
def _check_rule(cls, rule: RuleSet, node: DOMNode) -> Iterable[Specificity3]:
|
||||||
@@ -420,7 +444,7 @@ if __name__ == "__main__":
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(CSS)
|
stylesheet.add_source(CSS)
|
||||||
|
|
||||||
print(stylesheet.css)
|
print(stylesheet.css)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ DURATION = r"\d+\.?\d*(?:ms|s)"
|
|||||||
NUMBER = r"\-?\d+\.?\d*"
|
NUMBER = r"\-?\d+\.?\d*"
|
||||||
COLOR = r"\#[0-9a-fA-F]{8}|\#[0-9a-fA-F]{6}|rgb\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)|rgba\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)"
|
COLOR = r"\#[0-9a-fA-F]{8}|\#[0-9a-fA-F]{6}|rgb\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)|rgba\(\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*,\-?\d+\.?\d*\)"
|
||||||
KEY_VALUE = r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+"
|
KEY_VALUE = r"[a-zA-Z_-][a-zA-Z0-9_-]*=[0-9a-zA-Z_\-\/]+"
|
||||||
TOKEN = "[a-zA-Z_-]+"
|
TOKEN = "[a-zA-Z][a-zA-Z0-9_-]*"
|
||||||
STRING = r"\".*?\""
|
STRING = r"\".*?\""
|
||||||
VARIABLE_REF = r"\$[a-zA-Z0-9_\-]+"
|
VARIABLE_REF = r"\$[a-zA-Z0-9_\-]+"
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from rich.text import Text
|
|||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
from ._node_list import NodeList
|
from ._node_list import NodeList
|
||||||
|
from .color import Color
|
||||||
from .css._error_tools import friendly_list
|
from .css._error_tools import friendly_list
|
||||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||||
from .css.errors import StyleValueError
|
from .css.errors import StyleValueError
|
||||||
@@ -19,6 +20,7 @@ from .css.query import NoMatchingNodesError
|
|||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .app import App
|
||||||
from .css.query import DOMQuery
|
from .css.query import DOMQuery
|
||||||
from .screen import Screen
|
from .screen import Screen
|
||||||
|
|
||||||
@@ -40,42 +42,44 @@ class DOMNode(MessagePump):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
*,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
classes: set[str] | None = None,
|
classes: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._name = name
|
self._name = name
|
||||||
self._id = id
|
self._id = id
|
||||||
self._classes: set[str] = set() if classes is None else classes
|
self._classes: set[str] = set() if classes is None else set(classes.split())
|
||||||
self.children = NodeList()
|
self.children = NodeList()
|
||||||
self._css_styles: Styles = Styles(self)
|
self._css_styles: Styles = Styles(self)
|
||||||
self._inline_styles: Styles = Styles.parse(
|
self._inline_styles: Styles = Styles.parse(
|
||||||
self.INLINE_STYLES, repr(self), node=self
|
self.INLINE_STYLES, repr(self), node=self
|
||||||
)
|
)
|
||||||
self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
|
self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
|
||||||
self._default_styles = Styles.parse(self.DEFAULT_STYLES, f"{self.__class__}")
|
self._default_styles = Styles()
|
||||||
self._default_rules = self._default_styles.extract_rules((0, 0, 0))
|
self._default_rules = self._default_styles.extract_rules((0, 0, 0))
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
def on_register(self, app: App) -> None:
|
||||||
|
"""Called when the widget is registered
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app (App): Parent application.
|
||||||
|
"""
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield "name", self._name, None
|
yield "name", self._name, None
|
||||||
yield "id", self._id, None
|
yield "id", self._id, None
|
||||||
if self._classes:
|
if self._classes:
|
||||||
yield "classes", self._classes
|
yield "classes", " ".join(self._classes)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self) -> DOMNode:
|
def parent(self) -> DOMNode | None:
|
||||||
"""Get the parent node.
|
"""Get the parent node.
|
||||||
|
|
||||||
Raises:
|
|
||||||
NoParent: If this is the root node.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
DOMNode: The node which is the direct parent of this node.
|
DOMNode: The node which is the direct parent of this node.
|
||||||
"""
|
"""
|
||||||
if self._parent is None:
|
|
||||||
raise NoParent(f"{self} has no parent")
|
|
||||||
assert isinstance(self._parent, DOMNode)
|
|
||||||
return self._parent
|
return self._parent
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -222,19 +226,6 @@ class DOMNode(MessagePump):
|
|||||||
f"expected {friendly_list(VALID_VISIBILITY)})"
|
f"expected {friendly_list(VALID_VISIBILITY)})"
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def rich_text_style(self) -> Style:
|
|
||||||
"""Get the text style (added to parent style).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Style: Rich Style object.
|
|
||||||
"""
|
|
||||||
return (
|
|
||||||
self.parent.rich_text_style + self.styles.rich_style
|
|
||||||
if self.has_parent
|
|
||||||
else self.styles.rich_style
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tree(self) -> Tree:
|
def tree(self) -> Tree:
|
||||||
"""Get a Rich tree object which will recursively render the structure of the node tree.
|
"""Get a Rich tree object which will recursively render the structure of the node tree.
|
||||||
@@ -278,6 +269,52 @@ class DOMNode(MessagePump):
|
|||||||
add_children(tree, self)
|
add_children(tree, self)
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rich_text_style(self) -> Style:
|
||||||
|
"""Get the text style object.
|
||||||
|
|
||||||
|
A widget's style is influenced by its parent. For instance if a widgets background has an alpha,
|
||||||
|
then its parent's background color will show through. Additionally, widgets will inherit their
|
||||||
|
parent's text style (i.e. bold, italic etc).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Style: Rich Style object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# TODO: Feels like there may be opportunity for caching here.
|
||||||
|
|
||||||
|
background = Color(0, 0, 0, 0)
|
||||||
|
color = Color(255, 255, 255, 0)
|
||||||
|
style = Style()
|
||||||
|
for node in reversed(self.ancestors):
|
||||||
|
styles = node.styles
|
||||||
|
if styles.has_rule("background"):
|
||||||
|
background += styles.background
|
||||||
|
if styles.has_rule("color"):
|
||||||
|
color = styles.color
|
||||||
|
style += styles.text_style
|
||||||
|
|
||||||
|
style = Style(bgcolor=background.rich_color, color=color.rich_color) + style
|
||||||
|
return style
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ancestors(self) -> list[DOMNode]:
|
||||||
|
"""Get a list of Nodes by tracing ancestors all the way back to App."""
|
||||||
|
|
||||||
|
nodes: list[DOMNode] = [self]
|
||||||
|
add_node = nodes.append
|
||||||
|
node = self
|
||||||
|
while True:
|
||||||
|
node = node.parent
|
||||||
|
if node is None:
|
||||||
|
break
|
||||||
|
add_node(node)
|
||||||
|
return nodes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def displayed_children(self) -> list[DOMNode]:
|
||||||
|
return [child for child in self.children if child.display]
|
||||||
|
|
||||||
def get_pseudo_classes(self) -> Iterable[str]:
|
def get_pseudo_classes(self) -> Iterable[str]:
|
||||||
"""Get any pseudo classes applicable to this Node, e.g. hover, focus.
|
"""Get any pseudo classes applicable to this Node, e.g. hover, focus.
|
||||||
|
|
||||||
|
|||||||
@@ -1,41 +1,21 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import ClassVar, NamedTuple, TYPE_CHECKING
|
|
||||||
|
|
||||||
|
|
||||||
from .geometry import Region, Offset, Size
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
from .screen import Screen
|
|
||||||
|
|
||||||
|
|
||||||
class WidgetPlacement(NamedTuple):
|
class Vertical(Widget):
|
||||||
"""The position, size, and relative order of a widget within its parent."""
|
"""A container widget to align children vertically."""
|
||||||
|
|
||||||
region: Region
|
CSS = """
|
||||||
widget: Widget | None = None # A widget of None means empty space
|
Vertical {
|
||||||
order: int = 0
|
layout: vertical;
|
||||||
|
}
|
||||||
|
"""
|
||||||
class Layout(ABC):
|
|
||||||
"""Responsible for arranging Widgets in a view and rendering them."""
|
|
||||||
|
class Horizontal(Widget):
|
||||||
name: ClassVar[str] = ""
|
"""A container widget to align children horizontally."""
|
||||||
|
|
||||||
@abstractmethod
|
CSS = """
|
||||||
def arrange(
|
Horizontal {
|
||||||
self, parent: Widget, size: Size, scroll: Offset
|
layout: horizontal;
|
||||||
) -> tuple[list[WidgetPlacement], set[Widget]]:
|
}
|
||||||
"""Generate a layout map that defines where on the screen the widgets will be drawn.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parent (Widget): Parent widget.
|
|
||||||
size (Size): Size of container.
|
|
||||||
scroll (Offset): Offset to apply to the Widget placements.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Iterable[WidgetPlacement]: An iterable of widget location
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
|||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
from ..css.types import Edge
|
from ..css.types import Edge
|
||||||
from ..geometry import Offset, Region, Size
|
from ..geometry import Offset, Region, Size
|
||||||
from ..layout import Layout, WidgetPlacement
|
from .._layout import Layout, WidgetPlacement
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
@@ -50,9 +50,8 @@ class DockLayout(Layout):
|
|||||||
|
|
||||||
def get_docks(self, parent: Widget) -> list[Dock]:
|
def get_docks(self, parent: Widget) -> list[Dock]:
|
||||||
groups: dict[str, list[Widget]] = defaultdict(list)
|
groups: dict[str, list[Widget]] = defaultdict(list)
|
||||||
for child in parent.children:
|
for child in parent.displayed_children:
|
||||||
assert isinstance(child, Widget)
|
assert isinstance(child, Widget)
|
||||||
if child.display:
|
|
||||||
groups[child.styles.dock].append(child)
|
groups[child.styles.dock].append(child)
|
||||||
docks: list[Dock] = []
|
docks: list[Dock] = []
|
||||||
append_dock = docks.append
|
append_dock = docks.append
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from .horizontal import HorizontalLayout
|
from .horizontal import HorizontalLayout
|
||||||
from ..layout import Layout
|
from .._layout import Layout
|
||||||
from ..layouts.dock import DockLayout
|
from ..layouts.dock import DockLayout
|
||||||
from ..layouts.vertical import VerticalLayout
|
from ..layouts.vertical import VerticalLayout
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from typing import Iterable, NamedTuple, TYPE_CHECKING
|
|||||||
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
from ..geometry import Size, Offset, Region
|
from ..geometry import Size, Offset, Region
|
||||||
from ..layout import Layout, WidgetPlacement
|
from .._layout import Layout, WidgetPlacement
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from textual.geometry import Size, Offset, Region
|
from textual.geometry import Size, Offset, Region
|
||||||
from textual.layout import Layout, WidgetPlacement
|
from textual._layout import Layout, WidgetPlacement
|
||||||
|
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
|
|
||||||
@@ -39,7 +39,9 @@ class HorizontalLayout(Layout):
|
|||||||
|
|
||||||
x = box_models[0].margin.left if box_models else 0
|
x = box_models[0].margin.left if box_models else 0
|
||||||
|
|
||||||
for widget, box_model, margin in zip(parent.children, box_models, margins):
|
displayed_children = parent.displayed_children
|
||||||
|
|
||||||
|
for widget, box_model, margin in zip(displayed_children, box_models, margins):
|
||||||
content_width, content_height = box_model.size
|
content_width, content_height = box_model.size
|
||||||
offset_y = widget.styles.align_height(content_height, parent_size.height)
|
offset_y = widget.styles.align_height(content_height, parent_size.height)
|
||||||
region = Region(x, offset_y, content_width, content_height)
|
region = Region(x, offset_y, content_width, content_height)
|
||||||
@@ -53,4 +55,4 @@ class HorizontalLayout(Layout):
|
|||||||
total_region = Region(0, 0, max_width, max_height)
|
total_region = Region(0, 0, max_width, max_height)
|
||||||
add_placement(WidgetPlacement(total_region, None, 0))
|
add_placement(WidgetPlacement(total_region, None, 0))
|
||||||
|
|
||||||
return placements, set(parent.children)
|
return placements, set(displayed_children)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import cast, TYPE_CHECKING
|
|||||||
from .. import log
|
from .. import log
|
||||||
|
|
||||||
from ..geometry import Offset, Region, Size
|
from ..geometry import Offset, Region, Size
|
||||||
from ..layout import Layout, WidgetPlacement
|
from .._layout import Layout, WidgetPlacement
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
@@ -40,10 +40,13 @@ class VerticalLayout(Layout):
|
|||||||
|
|
||||||
y = box_models[0].margin.top if box_models else 0
|
y = box_models[0].margin.top if box_models else 0
|
||||||
|
|
||||||
for widget, box_model, margin in zip(parent.children, box_models, margins):
|
displayed_children = parent.displayed_children
|
||||||
|
|
||||||
|
for widget, box_model, margin in zip(displayed_children, box_models, margins):
|
||||||
content_width, content_height = box_model.size
|
content_width, content_height = box_model.size
|
||||||
offset_x = widget.styles.align_width(content_width, parent_size.width)
|
offset_x = widget.styles.align_width(content_width, parent_size.width)
|
||||||
region = Region(offset_x, y, content_width, content_height)
|
region = Region(offset_x, y, content_width, content_height)
|
||||||
|
# TODO: it seems that `max_height` is not used?
|
||||||
max_height = max(max_height, content_height)
|
max_height = max(max_height, content_height)
|
||||||
add_placement(WidgetPlacement(region, widget, 0))
|
add_placement(WidgetPlacement(region, widget, 0))
|
||||||
y += region.height + margin
|
y += region.height + margin
|
||||||
@@ -54,4 +57,4 @@ class VerticalLayout(Layout):
|
|||||||
total_region = Region(0, 0, max_width, max_height)
|
total_region = Region(0, 0, max_width, max_height)
|
||||||
add_placement(WidgetPlacement(total_region, None, 0))
|
add_placement(WidgetPlacement(total_region, None, 0))
|
||||||
|
|
||||||
return placements, set(parent.children)
|
return placements, set(displayed_children)
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ class MessagePump:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def close_messages_no_wait(self) -> None:
|
def close_messages_no_wait(self) -> None:
|
||||||
|
"""Request the message queue to exit."""
|
||||||
self._message_queue.put_nowait(MessagePriority(None))
|
self._message_queue.put_nowait(MessagePriority(None))
|
||||||
|
|
||||||
async def close_messages(self) -> None:
|
async def close_messages(self) -> None:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|||||||
Reactable = Union[Widget, App]
|
Reactable = Union[Widget, App]
|
||||||
|
|
||||||
|
|
||||||
ReactiveType = TypeVar("ReactiveType")
|
ReactiveType = TypeVar("ReactiveType", covariant=True)
|
||||||
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
@@ -36,7 +36,7 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
default: ReactiveType,
|
default: ReactiveType | Callable[[], ReactiveType],
|
||||||
*,
|
*,
|
||||||
layout: bool = False,
|
layout: bool = False,
|
||||||
repaint: bool = True,
|
repaint: bool = True,
|
||||||
@@ -58,7 +58,11 @@ class Reactive(Generic[ReactiveType]):
|
|||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.internal_name = f"_reactive_{name}"
|
self.internal_name = f"_reactive_{name}"
|
||||||
setattr(owner, self.internal_name, self._default)
|
setattr(
|
||||||
|
owner,
|
||||||
|
self.internal_name,
|
||||||
|
self._default() if callable(self._default) else self._default,
|
||||||
|
)
|
||||||
|
|
||||||
def __get__(self, obj: Reactable, obj_type: type[object]) -> ReactiveType:
|
def __get__(self, obj: Reactable, obj_type: type[object]) -> ReactiveType:
|
||||||
return getattr(obj, self.internal_name)
|
return getattr(obj, self.internal_name)
|
||||||
|
|||||||
@@ -11,17 +11,20 @@ from .geometry import Offset, Region
|
|||||||
from ._compositor import Compositor
|
from ._compositor import Compositor
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
from .renderables.gradient import VerticalGradient
|
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Screen(Widget):
|
class Screen(Widget):
|
||||||
"""A widget for the root of the app."""
|
"""A widget for the root of the app."""
|
||||||
|
|
||||||
DEFAULT_STYLES = """
|
CSS = """
|
||||||
|
|
||||||
|
Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: _default=top;
|
docks: _default=top;
|
||||||
|
background: $surface;
|
||||||
|
color: $text-surface;
|
||||||
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -35,12 +38,8 @@ class Screen(Widget):
|
|||||||
def watch_dark(self, dark: bool) -> None:
|
def watch_dark(self, dark: bool) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
|
||||||
def is_transparent(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return VerticalGradient("red", "blue")
|
return self.app.render()
|
||||||
|
|
||||||
def get_offset(self, widget: Widget) -> Offset:
|
def get_offset(self, widget: Widget) -> Offset:
|
||||||
"""Get the absolute offset of a given Widget.
|
"""Get the absolute offset of a given Widget.
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class ScrollBarRender:
|
|||||||
position: float = 0,
|
position: float = 0,
|
||||||
thickness: int = 1,
|
thickness: int = 1,
|
||||||
vertical: bool = True,
|
vertical: bool = True,
|
||||||
style: StyleType = "bright_magenta on #555555",
|
style: StyleType = "ansi_bright_magenta on #555555",
|
||||||
) -> None:
|
) -> None:
|
||||||
self.virtual_size = virtual_size
|
self.virtual_size = virtual_size
|
||||||
self.window_size = window_size
|
self.window_size = window_size
|
||||||
@@ -89,7 +89,7 @@ class ScrollBarRender:
|
|||||||
thickness: int = 1,
|
thickness: int = 1,
|
||||||
vertical: bool = True,
|
vertical: bool = True,
|
||||||
back_color: Color = Color.parse("#555555"),
|
back_color: Color = Color.parse("#555555"),
|
||||||
bar_color: Color = Color.parse("bright_magenta"),
|
bar_color: Color = Color.parse("ansi_bright_magenta"),
|
||||||
) -> Segments:
|
) -> Segments:
|
||||||
|
|
||||||
if vertical:
|
if vertical:
|
||||||
@@ -181,7 +181,7 @@ class ScrollBarRender:
|
|||||||
vertical=self.vertical,
|
vertical=self.vertical,
|
||||||
thickness=thickness,
|
thickness=thickness,
|
||||||
back_color=_style.bgcolor or Color.parse("#555555"),
|
back_color=_style.bgcolor or Color.parse("#555555"),
|
||||||
bar_color=_style.color or Color.parse("bright_magenta"),
|
bar_color=_style.color or Color.parse("ansi_bright_magenta"),
|
||||||
)
|
)
|
||||||
yield bar
|
yield bar
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,13 @@ from .dom import DOMNode
|
|||||||
from .geometry import clamp, Offset, Region, Size
|
from .geometry import clamp, Offset, Region, Size
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from . import messages
|
from . import messages
|
||||||
from .layout import Layout
|
from ._layout import Layout
|
||||||
from .reactive import Reactive, watch
|
from .reactive import Reactive, watch
|
||||||
from .renderables.opacity import Opacity
|
from .renderables.opacity import Opacity
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .app import App, ComposeResult
|
||||||
from .scrollbar import (
|
from .scrollbar import (
|
||||||
ScrollBar,
|
ScrollBar,
|
||||||
ScrollTo,
|
ScrollTo,
|
||||||
@@ -64,8 +65,7 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
can_focus: bool = False
|
can_focus: bool = False
|
||||||
|
|
||||||
DEFAULT_STYLES = """
|
CSS = """
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -73,7 +73,7 @@ class Widget(DOMNode):
|
|||||||
*children: Widget,
|
*children: Widget,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
classes: set[str] | None = None,
|
classes: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
self._size = Size(0, 0)
|
self._size = Size(0, 0)
|
||||||
@@ -104,6 +104,26 @@ class Widget(DOMNode):
|
|||||||
show_vertical_scrollbar = Reactive(False, layout=True)
|
show_vertical_scrollbar = Reactive(False, layout=True)
|
||||||
show_horizontal_scrollbar = Reactive(False, layout=True)
|
show_horizontal_scrollbar = Reactive(False, layout=True)
|
||||||
|
|
||||||
|
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
|
||||||
|
self.app.register(self, *anon_widgets, **widgets)
|
||||||
|
self.screen.refresh()
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
"""Yield child widgets for a container."""
|
||||||
|
return
|
||||||
|
yield
|
||||||
|
|
||||||
|
def on_register(self, app: App) -> None:
|
||||||
|
"""Called when the instance is registered.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app (App): App instance.
|
||||||
|
"""
|
||||||
|
# Parser the Widget's CSS
|
||||||
|
self.app.stylesheet.add_source(
|
||||||
|
self.CSS, f"{__file__}:<{self.__class__.__name__}>"
|
||||||
|
)
|
||||||
|
|
||||||
def get_box_model(self, container: Size, viewport: Size) -> BoxModel:
|
def get_box_model(self, container: Size, viewport: Size) -> BoxModel:
|
||||||
"""Process the box model for this widget.
|
"""Process the box model for this widget.
|
||||||
|
|
||||||
@@ -411,12 +431,18 @@ class Widget(DOMNode):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
renderable = self.render()
|
renderable = self.render()
|
||||||
|
|
||||||
styles = self.styles
|
styles = self.styles
|
||||||
parent_styles = self.parent.styles
|
parent_styles = self.parent.styles
|
||||||
|
|
||||||
parent_text_style = self.parent.rich_text_style
|
parent_text_style = self.parent.rich_text_style
|
||||||
text_style = styles.rich_style
|
text_style = styles.rich_style
|
||||||
|
|
||||||
|
content_align = (styles.content_align_horizontal, styles.content_align_vertical)
|
||||||
|
if content_align != ("left", "top"):
|
||||||
|
horizontal, vertical = content_align
|
||||||
|
renderable = Align(renderable, horizontal, vertical=vertical)
|
||||||
|
|
||||||
renderable_text_style = parent_text_style + text_style
|
renderable_text_style = parent_text_style + text_style
|
||||||
if renderable_text_style:
|
if renderable_text_style:
|
||||||
renderable = Styled(renderable, renderable_text_style)
|
renderable = Styled(renderable, renderable_text_style)
|
||||||
@@ -475,8 +501,7 @@ class Widget(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: ``True`` if there is background color, otherwise ``False``.
|
bool: ``True`` if there is background color, otherwise ``False``.
|
||||||
"""
|
"""
|
||||||
return False
|
return self.is_container and self.styles.background.is_transparent
|
||||||
return self.layout is not None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def console(self) -> Console:
|
def console(self) -> Console:
|
||||||
@@ -612,8 +637,10 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
# Default displays a pretty repr in the center of the screen
|
# Default displays a pretty repr in the center of the screen
|
||||||
|
|
||||||
label = self.css_identifier_styled
|
if self.is_container:
|
||||||
return Align.center(label, vertical="middle")
|
return ""
|
||||||
|
|
||||||
|
return self.css_identifier_styled
|
||||||
|
|
||||||
async def action(self, action: str, *params) -> None:
|
async def action(self, action: str, *params) -> None:
|
||||||
await self.app.action(action, self)
|
await self.app.action(action, self)
|
||||||
@@ -674,6 +701,12 @@ class Widget(DOMNode):
|
|||||||
async def on_key(self, event: events.Key) -> None:
|
async def on_key(self, event: events.Key) -> None:
|
||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
|
|
||||||
|
def on_mount(self, event: events.Mount) -> None:
|
||||||
|
widgets = list(self.compose())
|
||||||
|
if widgets:
|
||||||
|
self.mount(*widgets)
|
||||||
|
self.screen.refresh()
|
||||||
|
|
||||||
def on_leave(self) -> None:
|
def on_leave(self) -> None:
|
||||||
self.mouse_over = False
|
self.mouse_over = False
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from ._footer import Footer
|
from ._footer import Footer
|
||||||
from ._header import Header
|
from ._header import Header
|
||||||
from ._button import Button, ButtonPressed
|
from ._button import Button
|
||||||
from ._placeholder import Placeholder
|
from ._placeholder import Placeholder
|
||||||
from ._static import Static
|
from ._static import Static
|
||||||
from ._tree_control import TreeControl, TreeClick, TreeNode, NodeID
|
from ._tree_control import TreeControl, TreeClick, TreeNode, NodeID
|
||||||
@@ -8,7 +8,6 @@ from ._directory_tree import DirectoryTree, FileClick
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Button",
|
"Button",
|
||||||
"ButtonPressed",
|
|
||||||
"DirectoryTree",
|
"DirectoryTree",
|
||||||
"FileClick",
|
"FileClick",
|
||||||
"Footer",
|
"Footer",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from rich.align import Align
|
from typing import cast
|
||||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
|
||||||
from rich.style import StyleType
|
from rich.console import RenderableType
|
||||||
|
from rich.text import Text
|
||||||
|
|
||||||
from .. import events
|
from .. import events
|
||||||
from ..message import Message
|
from ..message import Message
|
||||||
@@ -10,58 +11,65 @@ from ..reactive import Reactive
|
|||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
|
|
||||||
class ButtonPressed(Message, bubble=True):
|
class Button(Widget, can_focus=True):
|
||||||
pass
|
"""A simple clickable button."""
|
||||||
|
|
||||||
|
CSS = """
|
||||||
|
|
||||||
class Expand:
|
Button {
|
||||||
def __init__(self, renderable: RenderableType) -> None:
|
width: auto;
|
||||||
self.renderable = renderable
|
height: 3;
|
||||||
|
padding: 0 2;
|
||||||
|
background: $primary;
|
||||||
|
color: $text-primary;
|
||||||
|
content-align: center middle;
|
||||||
|
border: tall $primary-lighten-3;
|
||||||
|
|
||||||
def __rich_console__(
|
margin: 1 0;
|
||||||
self, console: Console, options: ConsoleOptions
|
text-style: bold;
|
||||||
) -> RenderResult:
|
}
|
||||||
width = options.max_width
|
|
||||||
height = options.height or 1
|
|
||||||
yield from console.render(
|
|
||||||
self.renderable, options.update_dimensions(width, height)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
Button:hover {
|
||||||
|
background:$primary-darken-2;
|
||||||
|
color: $text-primary-darken-2;
|
||||||
|
border: tall $primary-lighten-1;
|
||||||
|
}
|
||||||
|
|
||||||
class ButtonRenderable:
|
"""
|
||||||
def __init__(self, label: RenderableType, style: StyleType = "") -> None:
|
|
||||||
self.label = label
|
|
||||||
self.style = style
|
|
||||||
|
|
||||||
def __rich_console__(
|
class Pressed(Message, bubble=True):
|
||||||
self, console: Console, options: ConsoleOptions
|
@property
|
||||||
) -> RenderResult:
|
def button(self) -> Button:
|
||||||
width = options.max_width
|
return cast(Button, self.sender)
|
||||||
height = options.height or 1
|
|
||||||
|
|
||||||
yield Align.center(
|
|
||||||
self.label, vertical="middle", style=self.style, width=width, height=height
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Button(Widget):
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
label: RenderableType,
|
label: RenderableType | None = None,
|
||||||
|
disabled: bool = False,
|
||||||
|
*,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
style: StyleType = "white on dark_blue",
|
id: str | None = None,
|
||||||
|
classes: str | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(name=name)
|
super().__init__(name=name, id=id, classes=classes)
|
||||||
self.name = name or str(label)
|
|
||||||
self.button_style = style
|
|
||||||
|
|
||||||
self.label = label
|
self.label = self.css_identifier_styled if label is None else label
|
||||||
|
self.disabled = disabled
|
||||||
|
if disabled:
|
||||||
|
self.add_class("-disabled")
|
||||||
|
|
||||||
label: Reactive[RenderableType] = Reactive("")
|
label: Reactive[RenderableType] = Reactive("")
|
||||||
|
|
||||||
|
def validate_label(self, label: RenderableType) -> RenderableType:
|
||||||
|
"""Parse markup for self.label"""
|
||||||
|
if isinstance(label, str):
|
||||||
|
return Text.from_markup(label)
|
||||||
|
return label
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return ButtonRenderable(self.label, style=self.button_style)
|
return self.label
|
||||||
|
|
||||||
async def on_click(self, event: events.Click) -> None:
|
async def on_click(self, event: events.Click) -> None:
|
||||||
event.prevent_default().stop()
|
event.stop()
|
||||||
await self.emit(ButtonPressed(self))
|
if not self.disabled:
|
||||||
|
await self.emit(Button.Pressed(self))
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class Static(Widget):
|
|||||||
*,
|
*,
|
||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
id: str | None = None,
|
id: str | None = None,
|
||||||
classes: set[str] | None = None,
|
classes: str | None = None,
|
||||||
style: StyleType = "",
|
style: StyleType = "",
|
||||||
padding: PaddingDimensions = 0,
|
padding: PaddingDimensions = 0,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -864,7 +864,7 @@ class TestParseLayout:
|
|||||||
css = "#some-widget { layout: dock; }"
|
css = "#some-widget { layout: dock; }"
|
||||||
|
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
assert isinstance(styles.layout, DockLayout)
|
assert isinstance(styles.layout, DockLayout)
|
||||||
@@ -874,7 +874,8 @@ class TestParseLayout:
|
|||||||
|
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
with pytest.raises(StylesheetParseError) as ex:
|
with pytest.raises(StylesheetParseError) as ex:
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
stylesheet.parse()
|
||||||
|
|
||||||
assert ex.value.errors is not None
|
assert ex.value.errors is not None
|
||||||
|
|
||||||
@@ -886,7 +887,7 @@ class TestParseText:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
assert styles.color == Color.parse("green")
|
assert styles.color == Color.parse("green")
|
||||||
@@ -897,7 +898,7 @@ class TestParseText:
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
assert styles.background == Color.parse("red")
|
assert styles.background == Color.parse("red")
|
||||||
@@ -933,7 +934,7 @@ class TestParseOffset:
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
|
|
||||||
@@ -972,7 +973,7 @@ class TestParseOffset:
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
|
|
||||||
@@ -1002,7 +1003,7 @@ class TestParseTransition:
|
|||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
|
|
||||||
@@ -1017,7 +1018,7 @@ class TestParseTransition:
|
|||||||
def test_no_delay_specified(self):
|
def test_no_delay_specified(self):
|
||||||
css = f"#some-widget {{ transition: offset-x 1 in_out_cubic; }}"
|
css = f"#some-widget {{ transition: offset-x 1 in_out_cubic; }}"
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
styles = stylesheet.rules[0].styles
|
styles = stylesheet.rules[0].styles
|
||||||
|
|
||||||
@@ -1032,9 +1033,11 @@ class TestParseTransition:
|
|||||||
|
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
with pytest.raises(StylesheetParseError) as ex:
|
with pytest.raises(StylesheetParseError) as ex:
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
stylesheet.parse()
|
||||||
|
|
||||||
stylesheet_errors = stylesheet.rules[0].errors
|
rules = stylesheet._parse_rules(css, "foo")
|
||||||
|
stylesheet_errors = rules[0].errors
|
||||||
|
|
||||||
assert len(stylesheet_errors) == 1
|
assert len(stylesheet_errors) == 1
|
||||||
assert stylesheet_errors[0][0].value == invalid_func_name
|
assert stylesheet_errors[0][0].value == invalid_func_name
|
||||||
@@ -1056,7 +1059,7 @@ class TestParseOpacity:
|
|||||||
def test_opacity_to_styles(self, css_value, styles_value):
|
def test_opacity_to_styles(self, css_value, styles_value):
|
||||||
css = f"#some-widget {{ opacity: {css_value} }}"
|
css = f"#some-widget {{ opacity: {css_value} }}"
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
|
|
||||||
assert stylesheet.rules[0].styles.opacity == styles_value
|
assert stylesheet.rules[0].styles.opacity == styles_value
|
||||||
assert not stylesheet.rules[0].errors
|
assert not stylesheet.rules[0].errors
|
||||||
@@ -1066,15 +1069,17 @@ class TestParseOpacity:
|
|||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
|
|
||||||
with pytest.raises(StylesheetParseError):
|
with pytest.raises(StylesheetParseError):
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
assert stylesheet.rules[0].errors
|
stylesheet.parse()
|
||||||
|
rules = stylesheet._parse_rules(css, "foo")
|
||||||
|
assert rules[0].errors
|
||||||
|
|
||||||
|
|
||||||
class TestParseMargin:
|
class TestParseMargin:
|
||||||
def test_margin_partial(self):
|
def test_margin_partial(self):
|
||||||
css = "#foo {margin: 1; margin-top: 2; margin-right: 3; margin-bottom: -1;}"
|
css = "#foo {margin: 1; margin-top: 2; margin-right: 3; margin-bottom: -1;}"
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
assert stylesheet.rules[0].styles.margin == Spacing(2, 3, -1, 1)
|
assert stylesheet.rules[0].styles.margin == Spacing(2, 3, -1, 1)
|
||||||
|
|
||||||
|
|
||||||
@@ -1082,5 +1087,5 @@ class TestParsePadding:
|
|||||||
def test_padding_partial(self):
|
def test_padding_partial(self):
|
||||||
css = "#foo {padding: 1; padding-top: 2; padding-right: 3; padding-bottom: -1;}"
|
css = "#foo {padding: 1; padding-top: 2; padding-right: 3; padding-bottom: -1;}"
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet()
|
||||||
stylesheet.parse(css)
|
stylesheet.add_source(css)
|
||||||
assert stylesheet.rules[0].styles.padding == Spacing(2, 3, -1, 1)
|
assert stylesheet.rules[0].styles.padding == Spacing(2, 3, -1, 1)
|
||||||
|
|||||||
55
tests/css/test_stylesheet.py
Normal file
55
tests/css/test_stylesheet.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from contextlib import nullcontext as does_not_raise
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from textual.color import Color
|
||||||
|
from textual.css.stylesheet import Stylesheet, StylesheetParseError
|
||||||
|
from textual.css.tokenizer import TokenizeError
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"css_value,expectation,expected_color",
|
||||||
|
[
|
||||||
|
# Valid values:
|
||||||
|
["transparent", does_not_raise(), Color(0, 0, 0, 0)],
|
||||||
|
["ansi_red", does_not_raise(), Color(128, 0, 0)],
|
||||||
|
["ansi_bright_magenta", does_not_raise(), Color(255, 0, 255)],
|
||||||
|
["red", does_not_raise(), Color(255, 0, 0)],
|
||||||
|
["lime", does_not_raise(), Color(0, 255, 0)],
|
||||||
|
["coral", does_not_raise(), Color(255, 127, 80)],
|
||||||
|
["aqua", does_not_raise(), Color(0, 255, 255)],
|
||||||
|
["deepskyblue", does_not_raise(), Color(0, 191, 255)],
|
||||||
|
["rebeccapurple", does_not_raise(), Color(102, 51, 153)],
|
||||||
|
["#ffcc00", does_not_raise(), Color(255, 204, 0)],
|
||||||
|
["#ffcc0033", does_not_raise(), Color(255, 204, 0, 0.2)],
|
||||||
|
["rgb(200,90,30)", does_not_raise(), Color(200, 90, 30)],
|
||||||
|
["rgba(200,90,30,0.3)", does_not_raise(), Color(200, 90, 30, 0.3)],
|
||||||
|
# Some invalid ones:
|
||||||
|
["coffee", pytest.raises(StylesheetParseError), None], # invalid color name
|
||||||
|
["ansi_dark_cyan", pytest.raises(StylesheetParseError), None],
|
||||||
|
["red 4", pytest.raises(StylesheetParseError), None], # space in it
|
||||||
|
["1", pytest.raises(StylesheetParseError), None], # invalid value
|
||||||
|
["()", pytest.raises(TokenizeError), None], # invalid tokens
|
||||||
|
# TODO: implement hex colors with 3 chars? @link https://devdocs.io/css/color_value
|
||||||
|
["#09f", pytest.raises(TokenizeError), None],
|
||||||
|
# TODO: allow spaces in rgb/rgba expressions?
|
||||||
|
["rgb(200, 90, 30)", pytest.raises(TokenizeError), None],
|
||||||
|
["rgba(200,90,30, 0.4)", pytest.raises(TokenizeError), None],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_color_property_parsing(css_value, expectation, expected_color):
|
||||||
|
stylesheet = Stylesheet()
|
||||||
|
css = """
|
||||||
|
* {
|
||||||
|
background: ${COLOR};
|
||||||
|
}
|
||||||
|
""".replace(
|
||||||
|
"${COLOR}", css_value
|
||||||
|
)
|
||||||
|
|
||||||
|
with expectation:
|
||||||
|
stylesheet.add_source(css)
|
||||||
|
stylesheet.parse()
|
||||||
|
|
||||||
|
if expected_color:
|
||||||
|
css_rule = stylesheet.rules[0]
|
||||||
|
assert css_rule.styles.background == expected_color
|
||||||
30
tests/layouts/test_common_layout_features.py
Normal file
30
tests/layouts/test_common_layout_features.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from textual.screen import Screen
|
||||||
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"layout,display,expected_in_displayed_children",
|
||||||
|
[
|
||||||
|
("dock", "block", True),
|
||||||
|
("horizontal", "block", True),
|
||||||
|
("vertical", "block", True),
|
||||||
|
("dock", "none", False),
|
||||||
|
("horizontal", "none", False),
|
||||||
|
("vertical", "none", False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_nodes_take_display_property_into_account_when_they_display_their_children(
|
||||||
|
layout: str, display: str, expected_in_displayed_children: bool
|
||||||
|
):
|
||||||
|
widget = Widget(name="widget that might not be visible 🥷 ")
|
||||||
|
widget.styles.display = display
|
||||||
|
|
||||||
|
screen = Screen()
|
||||||
|
screen.styles.layout = layout
|
||||||
|
screen.add_child(widget)
|
||||||
|
|
||||||
|
displayed_children = screen.displayed_children
|
||||||
|
assert isinstance(displayed_children, list)
|
||||||
|
assert (widget in screen.displayed_children) is expected_in_displayed_children
|
||||||
Reference in New Issue
Block a user