mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -14,30 +14,45 @@
|
|||||||
|
|
||||||
App > Screen {
|
App > Screen {
|
||||||
|
|
||||||
background: $surface;
|
|
||||||
color: $text-surface;
|
|
||||||
layers: sidebar;
|
|
||||||
|
|
||||||
color: $text-background;
|
|
||||||
background: $background;
|
background: $background;
|
||||||
|
color: $text;
|
||||||
|
layers: base sidebar;
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tree-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 20;
|
||||||
|
margin: 1 2;
|
||||||
|
background: $surface;
|
||||||
|
padding: 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
DirectoryTree {
|
||||||
|
padding: 0 1;
|
||||||
|
height: auto;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
DataTable {
|
DataTable {
|
||||||
/*border:heavy red;*/
|
/*border:heavy red;*/
|
||||||
/* tint: 10% green; */
|
/* tint: 10% green; */
|
||||||
/* text-opacity: 50%; */
|
/* text-opacity: 50%; */
|
||||||
padding: 1;
|
padding: 1;
|
||||||
margin: 1 2;
|
margin: 1 2;
|
||||||
height: 12;
|
height: 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
color: $text-panel;
|
|
||||||
background: $panel;
|
background: $panel;
|
||||||
|
color: $text;
|
||||||
dock: left;
|
dock: left;
|
||||||
width: 30;
|
width: 30;
|
||||||
|
margin-bottom: 1;
|
||||||
offset-x: -100%;
|
offset-x: -100%;
|
||||||
|
|
||||||
transition: offset 500ms in_out_cubic;
|
transition: offset 500ms in_out_cubic;
|
||||||
@@ -51,7 +66,7 @@ DataTable {
|
|||||||
#sidebar .title {
|
#sidebar .title {
|
||||||
height: 1;
|
height: 1;
|
||||||
background: $primary-background-darken-1;
|
background: $primary-background-darken-1;
|
||||||
color: $text-primary-background-darken-1;
|
color: $text;
|
||||||
border-right: wide $background;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
@@ -59,35 +74,29 @@ DataTable {
|
|||||||
#sidebar .user {
|
#sidebar .user {
|
||||||
height: 8;
|
height: 8;
|
||||||
background: $panel-darken-1;
|
background: $panel-darken-1;
|
||||||
color: $text-panel-darken-1;
|
color: $text;
|
||||||
border-right: wide $background;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .content {
|
#sidebar .content {
|
||||||
background: $panel-darken-2;
|
background: $panel-darken-2;
|
||||||
color: $text-surface;
|
color: $text;
|
||||||
border-right: wide $background;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header {
|
|
||||||
color: $text-secondary-background;
|
|
||||||
background: $secondary-background;
|
|
||||||
height: 1;
|
|
||||||
content-align: center middle;
|
|
||||||
|
|
||||||
dock: top;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Tweet {
|
Tweet {
|
||||||
height:12;
|
height:12;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin: 0 2;
|
||||||
|
|
||||||
|
margin:0 2;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
color: $text-panel;
|
color: $text;
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
/* border: outer $primary; */
|
/* border: outer $primary; */
|
||||||
padding: 1;
|
padding: 1;
|
||||||
@@ -96,14 +105,15 @@ Tweet {
|
|||||||
/* scrollbar-gutter: stable; */
|
/* scrollbar-gutter: stable; */
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
|
overflow-x: auto;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
margin: 1 2;
|
margin: 1 2;
|
||||||
height: 20;
|
height: 24;
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
}
|
}
|
||||||
@@ -117,13 +127,13 @@ Tweet {
|
|||||||
TweetHeader {
|
TweetHeader {
|
||||||
height:1;
|
height:1;
|
||||||
background: $accent;
|
background: $accent;
|
||||||
color: $text-accent
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
TweetBody {
|
TweetBody {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
color: $text-panel;
|
color: $text;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0 1 0 0;
|
padding: 0 1 0 0;
|
||||||
}
|
}
|
||||||
@@ -134,7 +144,7 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
|
|
||||||
.button {
|
.button {
|
||||||
background: $accent;
|
background: $accent;
|
||||||
color: $text-accent;
|
color: $text;
|
||||||
width:20;
|
width:20;
|
||||||
height: 3;
|
height: 3;
|
||||||
/* border-top: hidden $accent-darken-3; */
|
/* border-top: hidden $accent-darken-3; */
|
||||||
@@ -150,7 +160,7 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
background: $accent-lighten-1;
|
background: $accent-lighten-1;
|
||||||
color: $text-accent-lighten-1;
|
color: $text;
|
||||||
width: 20;
|
width: 20;
|
||||||
height: 3;
|
height: 3;
|
||||||
border: tall $accent-darken-1;
|
border: tall $accent-darken-1;
|
||||||
@@ -162,7 +172,7 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
color: $text-accent;
|
color: $text;
|
||||||
background: $accent;
|
background: $accent;
|
||||||
height: 1;
|
height: 1;
|
||||||
|
|
||||||
@@ -185,7 +195,7 @@ OptionItem {
|
|||||||
|
|
||||||
OptionItem:hover {
|
OptionItem:hover {
|
||||||
height: 3;
|
height: 3;
|
||||||
color: $text-primary;
|
color: $text;
|
||||||
background: $primary-darken-1;
|
background: $primary-darken-1;
|
||||||
/* border-top: hkey $accent2-darken-3;
|
/* border-top: hkey $accent2-darken-3;
|
||||||
border-bottom: hkey $accent2-darken-3; */
|
border-bottom: hkey $accent2-darken-3; */
|
||||||
@@ -197,7 +207,7 @@ Error {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height:3;
|
height:3;
|
||||||
background: $error;
|
background: $error;
|
||||||
color: $text-error;
|
color: $text;
|
||||||
border-top: tall $error-darken-2;
|
border-top: tall $error-darken-2;
|
||||||
border-bottom: tall $error-darken-2;
|
border-bottom: tall $error-darken-2;
|
||||||
|
|
||||||
@@ -210,7 +220,7 @@ Warning {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height:3;
|
height:3;
|
||||||
background: $warning;
|
background: $warning;
|
||||||
color: $text-warning-fade-1;
|
color: $text;
|
||||||
border-top: tall $warning-darken-2;
|
border-top: tall $warning-darken-2;
|
||||||
border-bottom: tall $warning-darken-2;
|
border-bottom: tall $warning-darken-2;
|
||||||
|
|
||||||
@@ -224,7 +234,7 @@ Success {
|
|||||||
height:auto;
|
height:auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: $success;
|
background: $success;
|
||||||
color: $text-success-fade-1;
|
color: $text;
|
||||||
|
|
||||||
border-top: hkey $success-darken-2;
|
border-top: hkey $success-darken-2;
|
||||||
border-bottom: hkey $success-darken-2;
|
border-bottom: hkey $success-darken-2;
|
||||||
|
|||||||
@@ -6,42 +6,55 @@ from rich.text import Text
|
|||||||
from textual.app import App, ComposeResult
|
from textual.app import App, ComposeResult
|
||||||
from textual.reactive import Reactive
|
from textual.reactive import Reactive
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Static, DataTable
|
from textual.widgets import Static, DataTable, DirectoryTree, Header, Footer
|
||||||
|
from textual.layout import Container
|
||||||
|
|
||||||
CODE = '''
|
CODE = '''
|
||||||
class Offset(NamedTuple):
|
from __future__ import annotations
|
||||||
"""A point defined by x and y coordinates."""
|
|
||||||
|
|
||||||
x: int = 0
|
from typing import Iterable, TypeVar
|
||||||
y: int = 0
|
|
||||||
|
|
||||||
@property
|
T = TypeVar("T")
|
||||||
def is_origin(self) -> bool:
|
|
||||||
"""Check if the point is at the origin (0, 0)"""
|
|
||||||
return self == (0, 0)
|
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
|
||||||
return self != (0, 0)
|
|
||||||
|
|
||||||
def __add__(self, other: object) -> Offset:
|
def loop_first(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
|
||||||
if isinstance(other, tuple):
|
"""Iterate and generate a tuple with a flag for first value."""
|
||||||
_x, _y = self
|
iter_values = iter(values)
|
||||||
x, y = other
|
try:
|
||||||
return Offset(_x + x, _y + y)
|
value = next(iter_values)
|
||||||
return NotImplemented
|
except StopIteration:
|
||||||
|
return
|
||||||
|
yield True, value
|
||||||
|
for value in iter_values:
|
||||||
|
yield False, value
|
||||||
|
|
||||||
def __sub__(self, other: object) -> Offset:
|
|
||||||
if isinstance(other, tuple):
|
|
||||||
_x, _y = self
|
|
||||||
x, y = other
|
|
||||||
return Offset(_x - x, _y - y)
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __mul__(self, other: object) -> Offset:
|
def loop_last(values: Iterable[T]) -> Iterable[tuple[bool, T]]:
|
||||||
if isinstance(other, (float, int)):
|
"""Iterate and generate a tuple with a flag for last value."""
|
||||||
x, y = self
|
iter_values = iter(values)
|
||||||
return Offset(int(x * other), int(y * other))
|
try:
|
||||||
return NotImplemented
|
previous_value = next(iter_values)
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
for value in iter_values:
|
||||||
|
yield False, previous_value
|
||||||
|
previous_value = value
|
||||||
|
yield True, previous_value
|
||||||
|
|
||||||
|
|
||||||
|
def loop_first_last(values: Iterable[T]) -> Iterable[tuple[bool, bool, T]]:
|
||||||
|
"""Iterate and generate a tuple with a flag for first and last value."""
|
||||||
|
iter_values = iter(values)
|
||||||
|
try:
|
||||||
|
previous_value = next(iter_values)
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
first = True
|
||||||
|
for value in iter_values:
|
||||||
|
yield first, False, previous_value
|
||||||
|
first = False
|
||||||
|
previous_value = value
|
||||||
|
yield first, True, previous_value
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
||||||
@@ -96,25 +109,28 @@ class BasicApp(App, css_path="basic.css"):
|
|||||||
|
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
"""Bind keys here."""
|
"""Bind keys here."""
|
||||||
self.bind("s", "toggle_class('#sidebar', '-active')")
|
self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar")
|
||||||
|
self.bind("d", "toggle_dark", description="Dark mode")
|
||||||
|
self.bind("q", "quit", description="Quit")
|
||||||
|
self.bind("f", "query_test", description="Query test")
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
yield Header()
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
|
||||||
table = DataTable()
|
table = DataTable()
|
||||||
self.scroll_to_target = Tweet(TweetBody())
|
self.scroll_to_target = Tweet(TweetBody())
|
||||||
|
|
||||||
yield Static(
|
yield Container(
|
||||||
Text.from_markup(
|
|
||||||
"[b]This is a [u]Textual[/u] app, running in the terminal"
|
|
||||||
),
|
|
||||||
id="header",
|
|
||||||
)
|
|
||||||
yield from (
|
|
||||||
Tweet(TweetBody()),
|
Tweet(TweetBody()),
|
||||||
Widget(
|
Widget(
|
||||||
Static(Syntax(CODE, "python"), classes="code"),
|
Static(
|
||||||
|
Syntax(CODE, "python", line_numbers=True, indent_guides=True),
|
||||||
|
classes="code",
|
||||||
|
),
|
||||||
classes="scrollable",
|
classes="scrollable",
|
||||||
),
|
),
|
||||||
table,
|
table,
|
||||||
|
Widget(DirectoryTree("~/"), id="tree-container"),
|
||||||
Error(),
|
Error(),
|
||||||
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
Tweet(TweetBody(), classes="scrollbar-size-custom"),
|
||||||
Warning(),
|
Warning(),
|
||||||
@@ -126,7 +142,6 @@ class BasicApp(App, css_path="basic.css"):
|
|||||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||||
Tweet(TweetBody(), classes="scroll-horizontal"),
|
Tweet(TweetBody(), classes="scroll-horizontal"),
|
||||||
)
|
)
|
||||||
yield Widget(id="footer")
|
|
||||||
yield Widget(
|
yield Widget(
|
||||||
Widget(classes="title"),
|
Widget(classes="title"),
|
||||||
Widget(classes="user"),
|
Widget(classes="user"),
|
||||||
@@ -136,6 +151,7 @@ class BasicApp(App, css_path="basic.css"):
|
|||||||
Widget(classes="content"),
|
Widget(classes="content"),
|
||||||
id="sidebar",
|
id="sidebar",
|
||||||
)
|
)
|
||||||
|
yield Footer()
|
||||||
|
|
||||||
table.add_column("Foo", width=20)
|
table.add_column("Foo", width=20)
|
||||||
table.add_column("Bar", width=20)
|
table.add_column("Bar", width=20)
|
||||||
@@ -147,12 +163,32 @@ class BasicApp(App, css_path="basic.css"):
|
|||||||
for n in range(100):
|
for n in range(100):
|
||||||
table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)])
|
table.add_row(*[f"Cell ([b]{n}[/b], {col})" for col in range(6)])
|
||||||
|
|
||||||
|
def on_mount(self):
|
||||||
|
self.sub_title = "Widget demo"
|
||||||
|
|
||||||
async def on_key(self, event) -> None:
|
async def on_key(self, event) -> None:
|
||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
|
|
||||||
def key_d(self):
|
def action_toggle_dark(self):
|
||||||
self.dark = not self.dark
|
self.dark = not self.dark
|
||||||
|
|
||||||
|
def action_query_test(self):
|
||||||
|
query = self.query("Tweet")
|
||||||
|
self.log(query)
|
||||||
|
self.log(query.nodes)
|
||||||
|
self.log(query)
|
||||||
|
self.log(query.nodes)
|
||||||
|
|
||||||
|
query.set_styles("outline: outer red;")
|
||||||
|
|
||||||
|
query = query.exclude(".scroll-horizontal")
|
||||||
|
self.log(query)
|
||||||
|
self.log(query.nodes)
|
||||||
|
|
||||||
|
# query = query.filter(".rubbish")
|
||||||
|
# self.log(query)
|
||||||
|
# self.log(query.first())
|
||||||
|
|
||||||
async def key_q(self):
|
async def key_q(self):
|
||||||
await self.shutdown()
|
await self.shutdown()
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Button {
|
|||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: $primary-lighten-2;
|
background: $primary-lighten-2;
|
||||||
color: $text-primary-lighten-2;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
#number-0 {
|
#number-0 {
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
Screen {
|
||||||
|
background: $surface-darken-1;
|
||||||
|
}
|
||||||
|
|
||||||
#tree-view {
|
#tree-view {
|
||||||
display: none;
|
display: none;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
@@ -9,7 +13,7 @@ CodeBrowser.-show-tree #tree-view {
|
|||||||
dock: left;
|
dock: left;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
background: $panel;
|
background: $surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
CodeBrowser{
|
CodeBrowser{
|
||||||
@@ -18,6 +22,7 @@ CodeBrowser{
|
|||||||
|
|
||||||
DirectoryTree {
|
DirectoryTree {
|
||||||
padding-right: 1;
|
padding-right: 1;
|
||||||
|
padding-right: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#code {
|
#code {
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ class CodeBrowser(App):
|
|||||||
line_numbers=True,
|
line_numbers=True,
|
||||||
word_wrap=True,
|
word_wrap=True,
|
||||||
indent_guides=True,
|
indent_guides=True,
|
||||||
theme="monokai",
|
theme="github-dark",
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
code_view.update(Traceback(theme="monokai", width=None))
|
code_view.update(Traceback(theme="github-dark", width=None))
|
||||||
self.sub_title = "ERROR"
|
self.sub_title = "ERROR"
|
||||||
else:
|
else:
|
||||||
code_view.update(syntax)
|
code_view.update(syntax)
|
||||||
|
|||||||
@@ -14,16 +14,11 @@
|
|||||||
|
|
||||||
App > Screen {
|
App > Screen {
|
||||||
|
|
||||||
background: $surface;
|
|
||||||
color: $text-surface;
|
|
||||||
layers: base sidebar;
|
|
||||||
|
|
||||||
color: $text-background;
|
|
||||||
background: $background;
|
background: $background;
|
||||||
|
color: $text;
|
||||||
|
layers: base sidebar;
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#tree-container {
|
#tree-container {
|
||||||
@@ -53,8 +48,8 @@ DataTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
color: $text-panel;
|
|
||||||
background: $panel;
|
background: $panel;
|
||||||
|
color: $text;
|
||||||
dock: left;
|
dock: left;
|
||||||
width: 30;
|
width: 30;
|
||||||
margin-bottom: 1;
|
margin-bottom: 1;
|
||||||
@@ -71,7 +66,7 @@ DataTable {
|
|||||||
#sidebar .title {
|
#sidebar .title {
|
||||||
height: 1;
|
height: 1;
|
||||||
background: $primary-background-darken-1;
|
background: $primary-background-darken-1;
|
||||||
color: $text-primary-background-darken-1;
|
color: $text;
|
||||||
border-right: wide $background;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
@@ -79,14 +74,14 @@ DataTable {
|
|||||||
#sidebar .user {
|
#sidebar .user {
|
||||||
height: 8;
|
height: 8;
|
||||||
background: $panel-darken-1;
|
background: $panel-darken-1;
|
||||||
color: $text-panel-darken-1;
|
color: $text;
|
||||||
border-right: wide $background;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .content {
|
#sidebar .content {
|
||||||
background: $panel-darken-2;
|
background: $panel-darken-2;
|
||||||
color: $text-surface;
|
color: $text;
|
||||||
border-right: wide $background;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
@@ -101,7 +96,7 @@ Tweet {
|
|||||||
|
|
||||||
margin:0 2;
|
margin:0 2;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
color: $text-panel;
|
color: $text;
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
/* border: outer $primary; */
|
/* border: outer $primary; */
|
||||||
padding: 1;
|
padding: 1;
|
||||||
@@ -132,13 +127,13 @@ Tweet {
|
|||||||
TweetHeader {
|
TweetHeader {
|
||||||
height:1;
|
height:1;
|
||||||
background: $accent;
|
background: $accent;
|
||||||
color: $text-accent
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
TweetBody {
|
TweetBody {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
color: $text-panel;
|
color: $text;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 0 1 0 0;
|
padding: 0 1 0 0;
|
||||||
}
|
}
|
||||||
@@ -149,7 +144,7 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
|
|
||||||
.button {
|
.button {
|
||||||
background: $accent;
|
background: $accent;
|
||||||
color: $text-accent;
|
color: $text;
|
||||||
width:20;
|
width:20;
|
||||||
height: 3;
|
height: 3;
|
||||||
/* border-top: hidden $accent-darken-3; */
|
/* border-top: hidden $accent-darken-3; */
|
||||||
@@ -165,7 +160,7 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
background: $accent-lighten-1;
|
background: $accent-lighten-1;
|
||||||
color: $text-accent-lighten-1;
|
color: $text;
|
||||||
width: 20;
|
width: 20;
|
||||||
height: 3;
|
height: 3;
|
||||||
border: tall $accent-darken-1;
|
border: tall $accent-darken-1;
|
||||||
@@ -177,7 +172,7 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
color: $text-accent;
|
color: $text;
|
||||||
background: $accent;
|
background: $accent;
|
||||||
height: 1;
|
height: 1;
|
||||||
|
|
||||||
@@ -200,7 +195,7 @@ OptionItem {
|
|||||||
|
|
||||||
OptionItem:hover {
|
OptionItem:hover {
|
||||||
height: 3;
|
height: 3;
|
||||||
color: $text-primary;
|
color: $text;
|
||||||
background: $primary-darken-1;
|
background: $primary-darken-1;
|
||||||
/* border-top: hkey $accent2-darken-3;
|
/* border-top: hkey $accent2-darken-3;
|
||||||
border-bottom: hkey $accent2-darken-3; */
|
border-bottom: hkey $accent2-darken-3; */
|
||||||
@@ -212,7 +207,7 @@ Error {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height:3;
|
height:3;
|
||||||
background: $error;
|
background: $error;
|
||||||
color: $text-error;
|
color: $text;
|
||||||
border-top: tall $error-darken-2;
|
border-top: tall $error-darken-2;
|
||||||
border-bottom: tall $error-darken-2;
|
border-bottom: tall $error-darken-2;
|
||||||
|
|
||||||
@@ -225,7 +220,7 @@ Warning {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height:3;
|
height:3;
|
||||||
background: $warning;
|
background: $warning;
|
||||||
color: $text-warning-fade-1;
|
color: $text;
|
||||||
border-top: tall $warning-darken-2;
|
border-top: tall $warning-darken-2;
|
||||||
border-bottom: tall $warning-darken-2;
|
border-bottom: tall $warning-darken-2;
|
||||||
|
|
||||||
@@ -239,7 +234,7 @@ Success {
|
|||||||
height:auto;
|
height:auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: $success;
|
background: $success;
|
||||||
color: $text-success-fade-1;
|
color: $text;
|
||||||
|
|
||||||
border-top: hkey $success-darken-2;
|
border-top: hkey $success-darken-2;
|
||||||
border-bottom: hkey $success-darken-2;
|
border-bottom: hkey $success-darken-2;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Button {
|
|||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: $primary-lighten-2;
|
background: $primary-lighten-2;
|
||||||
color: $text-primary-lighten-2;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
#number-0 {
|
#number-0 {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class CenterApp(App):
|
|||||||
|
|
||||||
Static {
|
Static {
|
||||||
background: $panel;
|
background: $panel;
|
||||||
color: $text-panel;
|
color: $text;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
sandbox/will/design.css
Normal file
23
sandbox/will/design.css
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Screen {
|
||||||
|
background: $surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
Container {
|
||||||
|
height: auto;
|
||||||
|
background: $boost;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Panel {
|
||||||
|
height: auto;
|
||||||
|
background: $boost;
|
||||||
|
margin: 1 2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Content {
|
||||||
|
background: $boost;
|
||||||
|
padding: 1 2;
|
||||||
|
margin: 1 2;
|
||||||
|
color: auto 95%;
|
||||||
|
}
|
||||||
35
sandbox/will/design.py
Normal file
35
sandbox/will/design.py
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
from textual.app import App
|
||||||
|
from textual.layout import Container
|
||||||
|
from textual.widgets import Header, Footer, Static
|
||||||
|
|
||||||
|
|
||||||
|
class Content(Static):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Panel(Container):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Panel2(Container):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DesignApp(App):
|
||||||
|
BINDINGS = [("d", "toggle_dark", "Toggle dark mode")]
|
||||||
|
|
||||||
|
def compose(self):
|
||||||
|
yield Header()
|
||||||
|
yield Footer()
|
||||||
|
yield Container(
|
||||||
|
Content("content"),
|
||||||
|
Panel(
|
||||||
|
Content("more content"),
|
||||||
|
Content("more content"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
app = DesignApp(css_path="design.css")
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
@@ -250,7 +250,7 @@ class StylesCache:
|
|||||||
line: Iterable[Segment]
|
line: Iterable[Segment]
|
||||||
# Draw top or bottom borders (A)
|
# Draw top or bottom borders (A)
|
||||||
if (border_top and y == 0) or (border_bottom and y == height - 1):
|
if (border_top and y == 0) or (border_bottom and y == height - 1):
|
||||||
border_color = background + (
|
border_color = base_background + (
|
||||||
border_top_color if y == 0 else border_bottom_color
|
border_top_color if y == 0 else border_bottom_color
|
||||||
)
|
)
|
||||||
box_segments = get_box(
|
box_segments = get_box(
|
||||||
@@ -271,9 +271,9 @@ class StylesCache:
|
|||||||
pad_bottom and y >= height - gutter.bottom
|
pad_bottom and y >= height - gutter.bottom
|
||||||
):
|
):
|
||||||
background_style = from_color(bgcolor=background.rich_color)
|
background_style = from_color(bgcolor=background.rich_color)
|
||||||
left_style = from_color(color=border_left_color.rich_color)
|
left_style = from_color(color=(background + border_left_color).rich_color)
|
||||||
left = get_box(border_left, inner, outer, left_style)[1][0]
|
left = get_box(border_left, inner, outer, left_style)[1][0]
|
||||||
right_style = from_color(color=border_right_color.rich_color)
|
right_style = from_color(color=(background + border_right_color).rich_color)
|
||||||
right = get_box(border_right, inner, outer, right_style)[1][2]
|
right = get_box(border_right, inner, outer, right_style)[1][2]
|
||||||
if border_left and border_right:
|
if border_left and border_right:
|
||||||
line = [left, Segment(" " * (width - 2), background_style), right]
|
line = [left, Segment(" " * (width - 2), background_style), right]
|
||||||
@@ -296,9 +296,13 @@ class StylesCache:
|
|||||||
|
|
||||||
if border_left or border_right:
|
if border_left or border_right:
|
||||||
# Add left / right border
|
# Add left / right border
|
||||||
left_style = from_color((background + border_left_color).rich_color)
|
left_style = from_color(
|
||||||
|
(base_background + border_left_color).rich_color
|
||||||
|
)
|
||||||
left = get_box(border_left, inner, outer, left_style)[1][0]
|
left = get_box(border_left, inner, outer, left_style)[1][0]
|
||||||
right_style = from_color((background + border_right_color).rich_color)
|
right_style = from_color(
|
||||||
|
(base_background + border_right_color).rich_color
|
||||||
|
)
|
||||||
right = get_box(border_right, inner, outer, right_style)[1][2]
|
right = get_box(border_right, inner, outer, right_style)[1][2]
|
||||||
|
|
||||||
if border_left and border_right:
|
if border_left and border_right:
|
||||||
@@ -327,9 +331,9 @@ class StylesCache:
|
|||||||
|
|
||||||
elif outline_left or outline_right:
|
elif outline_left or outline_right:
|
||||||
# Lines in side outline
|
# Lines in side outline
|
||||||
left_style = from_color((background + outline_left_color).rich_color)
|
left_style = from_color((base_background + outline_left_color).rich_color)
|
||||||
left = get_box(outline_left, inner, outer, left_style)[1][0]
|
left = get_box(outline_left, inner, outer, left_style)[1][0]
|
||||||
right_style = from_color((background + outline_right_color).rich_color)
|
right_style = from_color((base_background + outline_right_color).rich_color)
|
||||||
right = get_box(outline_right, inner, outer, right_style)[1][2]
|
right = get_box(outline_right, inner, outer, right_style)[1][2]
|
||||||
line = line_trim(list(line), outline_left != "", outline_right != "")
|
line = line_trim(list(line), outline_left != "", outline_right != "")
|
||||||
if outline_left and outline_right:
|
if outline_left and outline_right:
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
App {
|
App {
|
||||||
background: $background;
|
background: $background;
|
||||||
color: $text-background;
|
color: $text;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -541,6 +541,10 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
self._handle_exception(error)
|
self._handle_exception(error)
|
||||||
|
|
||||||
|
def action_toggle_dark(self) -> None:
|
||||||
|
"""Action to toggle dark mode."""
|
||||||
|
self.dark = not self.dark
|
||||||
|
|
||||||
def action_screenshot(self, filename: str | None, path: str = "~/") -> None:
|
def action_screenshot(self, filename: str | None, path: str = "~/") -> None:
|
||||||
"""Action to save a screenshot."""
|
"""Action to save a screenshot."""
|
||||||
self.bell()
|
self.bell()
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class BorderApp(App):
|
|||||||
border: solid $secondary;
|
border: solid $secondary;
|
||||||
height: auto;
|
height: auto;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
color: $text-panel;
|
color: $text;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,20 @@ EasingButtons {
|
|||||||
|
|
||||||
#duration-input {
|
#duration-input {
|
||||||
width: 30;
|
width: 30;
|
||||||
|
background: $boost;
|
||||||
|
padding: 0 1;
|
||||||
|
border: tall transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#duration-input:focus {
|
||||||
|
border: tall $accent;
|
||||||
}
|
}
|
||||||
|
|
||||||
#inputs {
|
#inputs {
|
||||||
padding: 1;
|
padding: 1;
|
||||||
height: auto;
|
height: auto;
|
||||||
dock: top;
|
dock: top;
|
||||||
|
background: $boost;
|
||||||
}
|
}
|
||||||
|
|
||||||
Bar {
|
Bar {
|
||||||
@@ -36,6 +44,11 @@ Bar {
|
|||||||
#opacity-widget {
|
#opacity-widget {
|
||||||
padding: 1;
|
padding: 1;
|
||||||
background: $warning;
|
background: $warning;
|
||||||
color: $text-warning;
|
color: $text;
|
||||||
border: wide $background;
|
border: wide $background;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#label {
|
||||||
|
width: auto;
|
||||||
|
padding: 1;
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ class EasingApp(App):
|
|||||||
|
|
||||||
yield EasingButtons()
|
yield EasingButtons()
|
||||||
yield layout.Vertical(
|
yield layout.Vertical(
|
||||||
layout.Vertical(Static("Animation Duration:"), duration_input, id="inputs"),
|
layout.Horizontal(
|
||||||
|
Static("Animation Duration:", id="label"), duration_input, id="inputs"
|
||||||
|
),
|
||||||
layout.Horizontal(
|
layout.Horizontal(
|
||||||
self.animated_bar,
|
self.animated_bar,
|
||||||
layout.Container(self.opacity_widget, id="other"),
|
layout.Container(self.opacity_widget, id="other"),
|
||||||
|
|||||||
@@ -249,6 +249,17 @@ class Color(NamedTuple):
|
|||||||
else f"#{r:02X}{g:02X}{b:02X}{int(a*255):02X}"
|
else f"#{r:02X}{g:02X}{b:02X}{int(a*255):02X}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hex6(self) -> str:
|
||||||
|
"""The color in CSS hex form, with 6 digits for RGB. Alpha is ignored.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A CSS hex-style color, e.g. "#46b3de"
|
||||||
|
|
||||||
|
"""
|
||||||
|
r, g, b, a = self.clamped
|
||||||
|
return f"#{r:02X}{g:02X}{b:02X}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def css(self) -> str:
|
def css(self) -> str:
|
||||||
"""The color in CSS rgb or rgba form.
|
"""The color in CSS rgb or rgba form.
|
||||||
@@ -279,12 +290,13 @@ class Color(NamedTuple):
|
|||||||
r, g, b, _ = self
|
r, g, b, _ = self
|
||||||
return Color(r, g, b, alpha)
|
return Color(r, g, b, alpha)
|
||||||
|
|
||||||
def blend(self, destination: Color, factor: float) -> Color:
|
def blend(self, destination: Color, factor: float, alpha: float = 1) -> Color:
|
||||||
"""Generate a new color between two colors.
|
"""Generate a new color between two colors.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
destination (Color): Another color.
|
destination (Color): Another color.
|
||||||
factor (float): A blend factor, 0 -> 1
|
factor (float): A blend factor, 0 -> 1.
|
||||||
|
alpha (float | None): New alpha for result. Defaults to 1.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Color: A new color.
|
Color: A new color.
|
||||||
@@ -299,6 +311,7 @@ class Color(NamedTuple):
|
|||||||
int(r1 + (r2 - r1) * factor),
|
int(r1 + (r2 - r1) * factor),
|
||||||
int(g1 + (g2 - g1) * factor),
|
int(g1 + (g2 - g1) * factor),
|
||||||
int(b1 + (b2 - b1) * factor),
|
int(b1 + (b2 - b1) * factor),
|
||||||
|
alpha,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __add__(self, other: object) -> Color:
|
def __add__(self, other: object) -> Color:
|
||||||
@@ -396,29 +409,31 @@ class Color(NamedTuple):
|
|||||||
return color
|
return color
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
@lru_cache(maxsize=1024)
|
||||||
def darken(self, amount: float) -> Color:
|
def darken(self, amount: float, alpha: float | None = None) -> Color:
|
||||||
"""Darken the color by a given amount.
|
"""Darken the color by a given amount.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
amount (float): Value between 0-1 to reduce luminance by.
|
amount (float): Value between 0-1 to reduce luminance by.
|
||||||
|
alpha (float | None, optional): Alpha component for new color or None to copy alpha. Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Color: New color.
|
Color: New color.
|
||||||
"""
|
"""
|
||||||
l, a, b = rgb_to_lab(self)
|
l, a, b = rgb_to_lab(self)
|
||||||
l -= amount * 100
|
l -= amount * 100
|
||||||
return lab_to_rgb(Lab(l, a, b)).clamped
|
return lab_to_rgb(Lab(l, a, b), self.a if alpha is None else alpha).clamped
|
||||||
|
|
||||||
def lighten(self, amount: float) -> Color:
|
def lighten(self, amount: float, alpha: float | None = None) -> Color:
|
||||||
"""Lighten the color by a given amount.
|
"""Lighten the color by a given amount.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
amount (float): Value between 0-1 to increase luminance by.
|
amount (float): Value between 0-1 to increase luminance by.
|
||||||
|
alpha (float | None, optional): Alpha component for new color or None to copy alpha. Defaults to None.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Color: New color.
|
Color: New color.
|
||||||
"""
|
"""
|
||||||
return self.darken(-amount)
|
return self.darken(-amount, alpha)
|
||||||
|
|
||||||
@lru_cache(maxsize=1024)
|
@lru_cache(maxsize=1024)
|
||||||
def get_contrast_text(self, alpha=0.95) -> Color:
|
def get_contrast_text(self, alpha=0.95) -> Color:
|
||||||
@@ -508,7 +523,7 @@ def rgb_to_lab(rgb: Color) -> Lab:
|
|||||||
return Lab(116 * y - 16, 500 * (x - y), 200 * (y - z))
|
return Lab(116 * y - 16, 500 * (x - y), 200 * (y - z))
|
||||||
|
|
||||||
|
|
||||||
def lab_to_rgb(lab: Lab) -> Color:
|
def lab_to_rgb(lab: Lab, alpha: float = 1.0) -> Color:
|
||||||
"""Convert a CIE-L*ab color to RGB.
|
"""Convert a CIE-L*ab color to RGB.
|
||||||
|
|
||||||
Uses the standard RGB color space with a D65/2⁰ standard illuminant.
|
Uses the standard RGB color space with a D65/2⁰ standard illuminant.
|
||||||
@@ -533,7 +548,7 @@ def lab_to_rgb(lab: Lab) -> Color:
|
|||||||
g = 1.055 * pow(g, 1 / 2.4) - 0.055 if g > 0.0031308 else 12.92 * g
|
g = 1.055 * pow(g, 1 / 2.4) - 0.055 if g > 0.0031308 else 12.92 * g
|
||||||
b = 1.055 * pow(b, 1 / 2.4) - 0.055 if b > 0.0031308 else 12.92 * b
|
b = 1.055 * pow(b, 1 / 2.4) - 0.055 if b > 0.0031308 else 12.92 * b
|
||||||
|
|
||||||
return Color(int(r * 255), int(g * 255), int(b * 255))
|
return Color(int(r * 255), int(g * 255), int(b * 255), alpha)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -102,6 +102,13 @@ class IntegerProperty(GenericProperty[int, int]):
|
|||||||
raise StyleValueError(f"Expected a number here, got f{value}")
|
raise StyleValueError(f"Expected a number here, got f{value}")
|
||||||
|
|
||||||
|
|
||||||
|
class BooleanProperty(GenericProperty[bool, bool]):
|
||||||
|
"""A property that requires a True or False value."""
|
||||||
|
|
||||||
|
def validate_value(self, value: object) -> bool:
|
||||||
|
return bool(value)
|
||||||
|
|
||||||
|
|
||||||
class ScalarProperty:
|
class ScalarProperty:
|
||||||
"""Descriptor for getting and setting scalar properties. Scalars are numeric values with a unit, e.g. "50vh"."""
|
"""Descriptor for getting and setting scalar properties. Scalars are numeric values with a unit, e.g. "50vh"."""
|
||||||
|
|
||||||
|
|||||||
@@ -580,7 +580,9 @@ class StylesBuilder:
|
|||||||
alpha: float | None = None
|
alpha: float | None = None
|
||||||
|
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if token.name == "scalar":
|
if name == "color" and token.name == "token" and token.value == "auto":
|
||||||
|
self.styles._rules["auto_color"] = True
|
||||||
|
elif token.name == "scalar":
|
||||||
alpha_scalar = Scalar.parse(token.value)
|
alpha_scalar = Scalar.parse(token.value)
|
||||||
if alpha_scalar.unit != Unit.PERCENT:
|
if alpha_scalar.unit != Unit.PERCENT:
|
||||||
self.error(name, token, "alpha must be given as a percentage.")
|
self.error(name, token, "alpha must be given as a percentage.")
|
||||||
@@ -598,9 +600,10 @@ class StylesBuilder:
|
|||||||
else:
|
else:
|
||||||
self.error(name, token, color_property_help_text(name, context="css"))
|
self.error(name, token, color_property_help_text(name, context="css"))
|
||||||
|
|
||||||
if color is not None:
|
if color is not None or alpha is not None:
|
||||||
if alpha is not None:
|
if alpha is not None:
|
||||||
color = color.with_alpha(alpha)
|
|
||||||
|
color = (color or Color(255, 255, 255)).with_alpha(alpha)
|
||||||
self.styles._rules[name] = color
|
self.styles._rules[name] = color
|
||||||
|
|
||||||
process_tint = process_color
|
process_tint = process_color
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from ..color import Color
|
|||||||
from ..geometry import Offset, Spacing
|
from ..geometry import Offset, Spacing
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
AlignProperty,
|
AlignProperty,
|
||||||
|
BooleanProperty,
|
||||||
BorderProperty,
|
BorderProperty,
|
||||||
BoxProperty,
|
BoxProperty,
|
||||||
ColorProperty,
|
ColorProperty,
|
||||||
@@ -83,6 +84,7 @@ class RulesMap(TypedDict, total=False):
|
|||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
layout: "Layout"
|
layout: "Layout"
|
||||||
|
|
||||||
|
auto_color: bool
|
||||||
color: Color
|
color: Color
|
||||||
background: Color
|
background: Color
|
||||||
text_style: Style
|
text_style: Style
|
||||||
@@ -183,6 +185,7 @@ class StylesBase(ABC):
|
|||||||
"min_height",
|
"min_height",
|
||||||
"max_width",
|
"max_width",
|
||||||
"max_height",
|
"max_height",
|
||||||
|
"auto_color",
|
||||||
"color",
|
"color",
|
||||||
"background",
|
"background",
|
||||||
"opacity",
|
"opacity",
|
||||||
@@ -202,6 +205,7 @@ class StylesBase(ABC):
|
|||||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||||
layout = LayoutProperty()
|
layout = LayoutProperty()
|
||||||
|
|
||||||
|
auto_color = BooleanProperty(default=False)
|
||||||
color = ColorProperty(Color(255, 255, 255))
|
color = ColorProperty(Color(255, 255, 255))
|
||||||
background = ColorProperty(Color(0, 0, 0, 0), background=True)
|
background = ColorProperty(Color(0, 0, 0, 0), background=True)
|
||||||
text_style = StyleFlagsProperty()
|
text_style = StyleFlagsProperty()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ NUMBER_OF_SHADES = 3
|
|||||||
# Where no content exists
|
# Where no content exists
|
||||||
DEFAULT_DARK_BACKGROUND = "#000000"
|
DEFAULT_DARK_BACKGROUND = "#000000"
|
||||||
# What text usually goes on top off
|
# What text usually goes on top off
|
||||||
DEFAULT_DARK_SURFACE = "#292929"
|
DEFAULT_DARK_SURFACE = "#121212"
|
||||||
|
|
||||||
DEFAULT_LIGHT_SURFACE = "#f5f5f5"
|
DEFAULT_LIGHT_SURFACE = "#f5f5f5"
|
||||||
DEFAULT_LIGHT_BACKGROUND = "#efefef"
|
DEFAULT_LIGHT_BACKGROUND = "#efefef"
|
||||||
@@ -38,6 +38,7 @@ class ColorSystem:
|
|||||||
"secondary-background",
|
"secondary-background",
|
||||||
"surface",
|
"surface",
|
||||||
"panel",
|
"panel",
|
||||||
|
"boost",
|
||||||
"warning",
|
"warning",
|
||||||
"error",
|
"error",
|
||||||
"success",
|
"success",
|
||||||
@@ -55,6 +56,7 @@ class ColorSystem:
|
|||||||
background: str | None = None,
|
background: str | None = None,
|
||||||
surface: str | None = None,
|
surface: str | None = None,
|
||||||
panel: str | None = None,
|
panel: str | None = None,
|
||||||
|
boost: str | None = None,
|
||||||
dark: bool = False,
|
dark: bool = False,
|
||||||
luminosity_spread: float = 0.15,
|
luminosity_spread: float = 0.15,
|
||||||
text_alpha: float = 0.95,
|
text_alpha: float = 0.95,
|
||||||
@@ -73,6 +75,7 @@ class ColorSystem:
|
|||||||
self.background = parse(background)
|
self.background = parse(background)
|
||||||
self.surface = parse(surface)
|
self.surface = parse(surface)
|
||||||
self.panel = parse(panel)
|
self.panel = parse(panel)
|
||||||
|
self.boost = parse(boost)
|
||||||
self._dark = dark
|
self._dark = dark
|
||||||
self._luminosity_spread = luminosity_spread
|
self._luminosity_spread = luminosity_spread
|
||||||
self._text_alpha = text_alpha
|
self._text_alpha = text_alpha
|
||||||
@@ -126,6 +129,8 @@ class ColorSystem:
|
|||||||
else:
|
else:
|
||||||
panel = self.panel
|
panel = self.panel
|
||||||
|
|
||||||
|
boost = self.boost or background.get_contrast_text(1.0).with_alpha(0.07)
|
||||||
|
|
||||||
colors: dict[str, str] = {}
|
colors: dict[str, str] = {}
|
||||||
|
|
||||||
def luminosity_range(spread) -> Iterable[tuple[str, float]]:
|
def luminosity_range(spread) -> Iterable[tuple[str, float]]:
|
||||||
@@ -153,6 +158,7 @@ class ColorSystem:
|
|||||||
("secondary-background", secondary),
|
("secondary-background", secondary),
|
||||||
("background", background),
|
("background", background),
|
||||||
("panel", panel),
|
("panel", panel),
|
||||||
|
("boost", boost),
|
||||||
("surface", surface),
|
("surface", surface),
|
||||||
("warning", warning),
|
("warning", warning),
|
||||||
("error", error),
|
("error", error),
|
||||||
@@ -178,14 +184,10 @@ class ColorSystem:
|
|||||||
else:
|
else:
|
||||||
shade_color = color.lighten(luminosity_delta)
|
shade_color = color.lighten(luminosity_delta)
|
||||||
colors[f"{name}{shade_name}"] = shade_color.hex
|
colors[f"{name}{shade_name}"] = shade_color.hex
|
||||||
for fade in range(3):
|
|
||||||
text_color = shade_color.get_contrast_text(text_alpha)
|
colors["text"] = "auto 95%"
|
||||||
if fade > 0:
|
colors["text-muted"] = "auto 80%"
|
||||||
text_color = text_color.blend(shade_color, fade * 0.1 + 0.15)
|
colors["text-disabled"] = "auto 60%"
|
||||||
on_name = f"text-{name}{shade_name}-fade-{fade}"
|
|
||||||
else:
|
|
||||||
on_name = f"text-{name}{shade_name}"
|
|
||||||
colors[on_name] = text_color.hex
|
|
||||||
|
|
||||||
return colors
|
return colors
|
||||||
|
|
||||||
@@ -206,16 +208,12 @@ def show_design(light: ColorSystem, dark: ColorSystem) -> Table:
|
|||||||
def make_shades(system: ColorSystem):
|
def make_shades(system: ColorSystem):
|
||||||
colors = system.generate()
|
colors = system.generate()
|
||||||
for name in system.shades:
|
for name in system.shades:
|
||||||
background = colors[name]
|
background = Color.parse(colors[name]).with_alpha(1.0)
|
||||||
foreground = colors[f"text-{name}"]
|
foreground = background + background.get_contrast_text(0.9)
|
||||||
text = Text(f"{background} ", style=f"{foreground} on {background}")
|
|
||||||
for fade in range(3):
|
|
||||||
foreground = colors[
|
|
||||||
f"text-{name}-fade-{fade}" if fade else f"text-{name}"
|
|
||||||
]
|
|
||||||
text.append(f"{name} ", style=f"{foreground} on {background}")
|
|
||||||
|
|
||||||
yield Padding(text, 1, style=f"{foreground} on {background}")
|
text = Text(name)
|
||||||
|
|
||||||
|
yield Padding(text, 1, style=f"{foreground.hex6} on {background.hex6}")
|
||||||
|
|
||||||
table = Table(box=None, expand=True)
|
table = Table(box=None, expand=True)
|
||||||
table.add_column("Light", justify="center")
|
table.add_column("Light", justify="center")
|
||||||
|
|||||||
@@ -497,6 +497,8 @@ class DOMNode(MessagePump):
|
|||||||
if styles.has_rule("color"):
|
if styles.has_rule("color"):
|
||||||
color = styles.color
|
color = styles.color
|
||||||
style += styles.text_style
|
style += styles.text_style
|
||||||
|
if styles.has_rule("auto_color") and styles.auto_color:
|
||||||
|
color = background.get_contrast_text(color.a)
|
||||||
style += Style.from_color(
|
style += Style.from_color(
|
||||||
(background + color).rich_color, background.rich_color
|
(background + color).rich_color, background.rich_color
|
||||||
)
|
)
|
||||||
@@ -534,7 +536,11 @@ class DOMNode(MessagePump):
|
|||||||
background += styles.background
|
background += styles.background
|
||||||
if styles.has_rule("color"):
|
if styles.has_rule("color"):
|
||||||
base_color = color
|
base_color = color
|
||||||
|
if styles.auto_color:
|
||||||
|
color = background.get_contrast_text(color.a)
|
||||||
|
else:
|
||||||
color = styles.color
|
color = styles.color
|
||||||
|
|
||||||
return (base_background, base_color, background, color)
|
return (base_background, base_color, background, color)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ class Screen(Widget):
|
|||||||
Screen {
|
Screen {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
background: $surface;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Button(Widget, can_focus=True):
|
|||||||
width: auto;
|
width: auto;
|
||||||
height: 3;
|
height: 3;
|
||||||
background: $panel;
|
background: $panel;
|
||||||
color: $text-panel;
|
color: auto;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: tall $panel-lighten-2;
|
border-top: tall $panel-lighten-2;
|
||||||
border-bottom: tall $panel-darken-3;
|
border-bottom: tall $panel-darken-3;
|
||||||
@@ -56,7 +56,7 @@ class Button(Widget, can_focus=True):
|
|||||||
Button:hover {
|
Button:hover {
|
||||||
border-top: tall $panel-lighten-1;
|
border-top: tall $panel-lighten-1;
|
||||||
background: $panel-darken-2;
|
background: $panel-darken-2;
|
||||||
color: $text-panel-darken-2;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
Button.-active {
|
Button.-active {
|
||||||
@@ -69,7 +69,7 @@ class Button(Widget, can_focus=True):
|
|||||||
/* Primary variant */
|
/* Primary variant */
|
||||||
Button.-primary {
|
Button.-primary {
|
||||||
background: $primary;
|
background: $primary;
|
||||||
color: $text-primary;
|
color: $text;
|
||||||
border-top: tall $primary-lighten-3;
|
border-top: tall $primary-lighten-3;
|
||||||
border-bottom: tall $primary-darken-3;
|
border-bottom: tall $primary-darken-3;
|
||||||
|
|
||||||
@@ -77,8 +77,8 @@ class Button(Widget, can_focus=True):
|
|||||||
|
|
||||||
Button.-primary:hover {
|
Button.-primary:hover {
|
||||||
background: $primary-darken-2;
|
background: $primary-darken-2;
|
||||||
color: $text-primary-darken-2;
|
color: $text;
|
||||||
|
border-top: tall $primary-lighten-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
Button.-primary.-active {
|
Button.-primary.-active {
|
||||||
@@ -91,14 +91,14 @@ class Button(Widget, can_focus=True):
|
|||||||
/* Success variant */
|
/* Success variant */
|
||||||
Button.-success {
|
Button.-success {
|
||||||
background: $success;
|
background: $success;
|
||||||
color: $text-success;
|
color: $text;
|
||||||
border-top: tall $success-lighten-2;
|
border-top: tall $success-lighten-2;
|
||||||
border-bottom: tall $success-darken-3;
|
border-bottom: tall $success-darken-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
Button.-success:hover {
|
Button.-success:hover {
|
||||||
background: $success-darken-2;
|
background: $success-darken-2;
|
||||||
color: $text-success-darken-2;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
Button.-success.-active {
|
Button.-success.-active {
|
||||||
@@ -111,14 +111,14 @@ class Button(Widget, can_focus=True):
|
|||||||
/* Warning variant */
|
/* Warning variant */
|
||||||
Button.-warning {
|
Button.-warning {
|
||||||
background: $warning;
|
background: $warning;
|
||||||
color: $text-warning;
|
color: $text;
|
||||||
border-top: tall $warning-lighten-2;
|
border-top: tall $warning-lighten-2;
|
||||||
border-bottom: tall $warning-darken-3;
|
border-bottom: tall $warning-darken-3;
|
||||||
}
|
}
|
||||||
|
|
||||||
Button.-warning:hover {
|
Button.-warning:hover {
|
||||||
background: $warning-darken-2;
|
background: $warning-darken-2;
|
||||||
color: $text-warning-darken-1;
|
color: $text;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ class Button(Widget, can_focus=True):
|
|||||||
/* Error variant */
|
/* Error variant */
|
||||||
Button.-error {
|
Button.-error {
|
||||||
background: $error;
|
background: $error;
|
||||||
color: $text-error;
|
color: $text;
|
||||||
border-top: tall $error-lighten-2;
|
border-top: tall $error-lighten-2;
|
||||||
border-bottom: tall $error-darken-3;
|
border-bottom: tall $error-darken-3;
|
||||||
|
|
||||||
@@ -140,7 +140,7 @@ class Button(Widget, can_focus=True):
|
|||||||
|
|
||||||
Button.-error:hover {
|
Button.-error:hover {
|
||||||
background: $error-darken-1;
|
background: $error-darken-1;
|
||||||
color: $text-error-darken-2;
|
color: $text;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,17 +109,17 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
|||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
DataTable {
|
DataTable {
|
||||||
background: $surface;
|
background: $surface;
|
||||||
color: $text-surface;
|
color: $text;
|
||||||
}
|
}
|
||||||
DataTable > .datatable--header {
|
DataTable > .datatable--header {
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
background: $primary;
|
background: $primary;
|
||||||
color: $text-primary;
|
color: $text;
|
||||||
}
|
}
|
||||||
DataTable > .datatable--fixed {
|
DataTable > .datatable--fixed {
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
background: $primary;
|
background: $primary;
|
||||||
color: $text-primary;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable > .datatable--odd-row {
|
DataTable > .datatable--odd-row {
|
||||||
@@ -132,7 +132,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
|||||||
|
|
||||||
DataTable > .datatable--cursor {
|
DataTable > .datatable--cursor {
|
||||||
background: $secondary;
|
background: $secondary;
|
||||||
color: $text-secondary;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
.-dark-mode DataTable > .datatable--even-row {
|
.-dark-mode DataTable > .datatable--even-row {
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ class DirectoryTree(TreeControl[DirEntry]):
|
|||||||
label.stylize("bold")
|
label.stylize("bold")
|
||||||
icon = "📂" if expanded else "📁"
|
icon = "📂" if expanded else "📁"
|
||||||
else:
|
else:
|
||||||
|
|
||||||
icon = "📄"
|
icon = "📄"
|
||||||
label.highlight_regex(r"\..*$", "italic")
|
label.highlight_regex(r"\..*$", "italic")
|
||||||
|
|
||||||
|
|||||||
@@ -16,25 +16,22 @@ class Footer(Widget):
|
|||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
Footer {
|
Footer {
|
||||||
background: $accent;
|
background: $accent;
|
||||||
color: $text-accent;
|
color: $text;
|
||||||
dock: bottom;
|
dock: bottom;
|
||||||
height: 1;
|
height: 1;
|
||||||
}
|
}
|
||||||
Footer > .footer--highlight {
|
Footer > .footer--highlight {
|
||||||
background: $accent-darken-1;
|
background: $accent-darken-1;
|
||||||
color: $text-accent-darken-1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Footer > .footer--highlight-key {
|
Footer > .footer--highlight-key {
|
||||||
background: $secondary;
|
background: $secondary;
|
||||||
color: $text-secondary;
|
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
Footer > .footer--key {
|
Footer > .footer--key {
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
background: $accent-darken-2;
|
background: $accent-darken-2;
|
||||||
color: $text-accent-darken-2;
|
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class HeaderClock(Widget):
|
|||||||
width: 10;
|
width: 10;
|
||||||
padding: 0 1;
|
padding: 0 1;
|
||||||
background: $secondary-background-lighten-1;
|
background: $secondary-background-lighten-1;
|
||||||
color: $text-secondary-background;
|
color: $text;
|
||||||
text-opacity: 85%;
|
text-opacity: 85%;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
@@ -76,7 +76,7 @@ class Header(Widget):
|
|||||||
dock: top;
|
dock: top;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: $secondary-background;
|
background: $secondary-background;
|
||||||
color: $text-secondary-background;
|
color: $text;
|
||||||
height: 1;
|
height: 1;
|
||||||
}
|
}
|
||||||
Header.tall {
|
Header.tall {
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ class TreeNode(Generic[NodeDataType]):
|
|||||||
class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||||
DEFAULT_CSS = """
|
DEFAULT_CSS = """
|
||||||
TreeControl {
|
TreeControl {
|
||||||
background: $panel;
|
background: $surface;
|
||||||
color: $text-panel;
|
color: $text;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@@ -177,7 +177,6 @@ class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
|||||||
TreeControl > .tree--guides-highlight {
|
TreeControl > .tree--guides-highlight {
|
||||||
color: $success;
|
color: $success;
|
||||||
text-style: uu;
|
text-style: uu;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TreeControl > .tree--guides-cursor {
|
TreeControl > .tree--guides-cursor {
|
||||||
@@ -186,12 +185,12 @@ class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
|||||||
}
|
}
|
||||||
|
|
||||||
TreeControl > .tree--labels {
|
TreeControl > .tree--labels {
|
||||||
color: $text-panel;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
TreeControl > .tree--cursor {
|
TreeControl > .tree--cursor {
|
||||||
background: $secondary;
|
background: $secondary;
|
||||||
color: $text-secondary;
|
color: $text;
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user