mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
improved color harmony
This commit is contained in:
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
* {
|
* {
|
||||||
transition: color 300ms linear, background 300ms linear;
|
transition: color 300ms linear, background 300ms linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
*:hover {
|
*:hover {
|
||||||
@@ -29,8 +29,8 @@ DataTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
color: $text-primary;
|
color: $text-panel;
|
||||||
background: $primary-background;
|
background: $panel;
|
||||||
dock: side;
|
dock: side;
|
||||||
width: 30;
|
width: 30;
|
||||||
offset-x: -100%;
|
offset-x: -100%;
|
||||||
@@ -43,33 +43,35 @@ DataTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .title {
|
#sidebar .title {
|
||||||
height: 3;
|
height: 1;
|
||||||
background: $primary-background-darken-2;
|
background: $primary-background-darken-1;
|
||||||
color: $text-primary-darken-2 ;
|
color: $text-primary-background-darken-1;
|
||||||
border-right: outer $primary-darken-3;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .user {
|
#sidebar .user {
|
||||||
height: 8;
|
height: 8;
|
||||||
background: $primary-background-darken-1;
|
background: $panel-darken-1;
|
||||||
color: $text-primary-darken-1;
|
color: $text-panel-darken-1;
|
||||||
border-right: outer $primary-background-darken-3;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .content {
|
#sidebar .content {
|
||||||
background: $primary-background;
|
background: $surface;
|
||||||
color: $text-primary-background;
|
color: $text-surface;
|
||||||
border-right: outer $primary-background-darken-3;
|
border-right: wide $background;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header {
|
#header {
|
||||||
color: $text-secondary-background-darken-1;
|
color: $text-secondary-background;
|
||||||
background: $secondary-background-darken-1;
|
background: $secondary-background;
|
||||||
height: 3;
|
height: 1;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
@@ -90,7 +92,7 @@ Tweet {
|
|||||||
layout: vertical;
|
layout: vertical;
|
||||||
/* border: outer $primary; */
|
/* border: outer $primary; */
|
||||||
padding: 1;
|
padding: 1;
|
||||||
border: wide $panel-darken-2;
|
border: wide $panel;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
/* scrollbar-gutter: stable; */
|
/* scrollbar-gutter: stable; */
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
@@ -175,16 +177,16 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
|
|
||||||
OptionItem {
|
OptionItem {
|
||||||
height: 3;
|
height: 3;
|
||||||
background: $primary-background;
|
background: $panel;
|
||||||
border-right: outer $primary-background-darken-2;
|
border-right: wide $background;
|
||||||
border-left: blank;
|
border-left: blank;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionItem:hover {
|
OptionItem:hover {
|
||||||
height: 3;
|
height: 3;
|
||||||
color: $secondary;
|
color: $text-primary;
|
||||||
background: $primary-background-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; */
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
@@ -196,8 +198,8 @@ Error {
|
|||||||
height:3;
|
height:3;
|
||||||
background: $error;
|
background: $error;
|
||||||
color: $text-error;
|
color: $text-error;
|
||||||
border-top: wide $error-darken-1;
|
border-top: tall $error-darken-1;
|
||||||
border-bottom: wide $error-darken-1;
|
border-bottom: tall $error-darken-1;
|
||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
@@ -209,8 +211,8 @@ Warning {
|
|||||||
height:3;
|
height:3;
|
||||||
background: $warning;
|
background: $warning;
|
||||||
color: $text-warning-fade-1;
|
color: $text-warning-fade-1;
|
||||||
border-top: wide $warning-darken-1;
|
border-top: tall $warning-darken-1;
|
||||||
border-bottom: wide $warning-darken-1;
|
border-bottom: tall $warning-darken-1;
|
||||||
|
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
@@ -218,7 +220,7 @@ Warning {
|
|||||||
|
|
||||||
Success {
|
Success {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
width:90%;
|
|
||||||
height:auto;
|
height:auto;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: $success;
|
background: $success;
|
||||||
@@ -227,7 +229,7 @@ Success {
|
|||||||
border-top: hkey $success-darken-1;
|
border-top: hkey $success-darken-1;
|
||||||
border-bottom: hkey $success-darken-1;
|
border-bottom: hkey $success-darken-1;
|
||||||
|
|
||||||
text-style: bold underline;
|
text-style: bold ;
|
||||||
|
|
||||||
align-horizontal: center;
|
align-horizontal: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,21 +179,21 @@ app = BasicApp()
|
|||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run()
|
app.run()
|
||||||
|
|
||||||
from textual.geometry import Region
|
# from textual.geometry import Region
|
||||||
from textual.color import Color
|
# from textual.color import Color
|
||||||
|
|
||||||
print(Region.intersection.cache_info())
|
# print(Region.intersection.cache_info())
|
||||||
print(Region.overlaps.cache_info())
|
# print(Region.overlaps.cache_info())
|
||||||
print(Region.union.cache_info())
|
# print(Region.union.cache_info())
|
||||||
print(Region.split_vertical.cache_info())
|
# print(Region.split_vertical.cache_info())
|
||||||
print(Region.__contains__.cache_info())
|
# print(Region.__contains__.cache_info())
|
||||||
from textual.css.scalar import Scalar
|
# from textual.css.scalar import Scalar
|
||||||
|
|
||||||
print(Scalar.resolve_dimension.cache_info())
|
# print(Scalar.resolve_dimension.cache_info())
|
||||||
|
|
||||||
from rich.style import Style
|
# from rich.style import Style
|
||||||
from rich.cells import cached_cell_len
|
# from rich.cells import cached_cell_len
|
||||||
|
|
||||||
print(Style._add.cache_info())
|
# print(Style._add.cache_info())
|
||||||
|
|
||||||
print(cached_cell_len.cache_info())
|
# print(cached_cell_len.cache_info())
|
||||||
|
|||||||
@@ -80,16 +80,26 @@ _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED = sys.version_info >= (3, 10, 0)
|
|||||||
LayoutDefinition = "dict[str, Any]"
|
LayoutDefinition = "dict[str, Any]"
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_COLORS = ColorSystem(
|
DEFAULT_COLORS = {
|
||||||
primary="#2A4E6E",
|
"dark": ColorSystem(
|
||||||
secondary="#ffa62b",
|
primary="#004578",
|
||||||
warning="#ffa62b",
|
secondary="#ffa62b",
|
||||||
error="#ba3c5b",
|
warning="#ffa62b",
|
||||||
success="#4EBF71",
|
error="#ba3c5b",
|
||||||
accent="#1A75B4",
|
success="#4EBF71",
|
||||||
system="#5a4599",
|
accent="#0178D4",
|
||||||
dark_surface="#292929",
|
dark=True,
|
||||||
)
|
),
|
||||||
|
"light": ColorSystem(
|
||||||
|
primary="#004578",
|
||||||
|
secondary="#ffa62b",
|
||||||
|
warning="#ffa62b",
|
||||||
|
error="#ba3c5b",
|
||||||
|
success="#4EBF71",
|
||||||
|
accent="#0178D4",
|
||||||
|
dark=False,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ComposeResult = Iterable[Widget]
|
ComposeResult = Iterable[Widget]
|
||||||
@@ -338,7 +348,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
dict[str, str]: A mapping of variable name to value.
|
dict[str, str]: A mapping of variable name to value.
|
||||||
"""
|
"""
|
||||||
variables = self.design.generate(self.dark)
|
variables = self.design["dark" if self.dark else "light"].generate()
|
||||||
return variables
|
return variables
|
||||||
|
|
||||||
def watch_dark(self, dark: bool) -> None:
|
def watch_dark(self, dark: bool) -> None:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from rich.console import ConsoleOptions, Console
|
from rich.console import ConsoleOptions, Console, RenderResult
|
||||||
from rich.traceback import Traceback
|
from rich.traceback import Traceback
|
||||||
|
|
||||||
from ._help_renderables import HelpText
|
from ._help_renderables import HelpText
|
||||||
@@ -15,10 +15,6 @@ class DeclarationError(Exception):
|
|||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
class UnresolvedVariableError(NameError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class StyleTypeError(TypeError):
|
class StyleTypeError(TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -35,7 +31,9 @@ class StyleValueError(ValueError):
|
|||||||
super().__init__(*args)
|
super().__init__(*args)
|
||||||
self.help_text = help_text
|
self.help_text = help_text
|
||||||
|
|
||||||
def __rich_console__(self, console: Console, options: ConsoleOptions):
|
def __rich_console__(
|
||||||
|
self, console: Console, options: ConsoleOptions
|
||||||
|
) -> RenderResult:
|
||||||
yield Traceback.from_exception(type(self), self, self.__traceback__)
|
yield Traceback.from_exception(type(self), self, self.__traceback__)
|
||||||
if self.help_text is not None:
|
if self.help_text is not None:
|
||||||
yield ""
|
yield ""
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
from typing import Iterator, Iterable
|
from typing import Iterator, Iterable, NoReturn, Sequence
|
||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
from textual.css.errors import UnresolvedVariableError
|
from textual.css.tokenizer import TokenError
|
||||||
from textual.css.types import Specificity3
|
from textual.css.types import Specificity3
|
||||||
from ._styles_builder import StylesBuilder, DeclarationError
|
from ._styles_builder import StylesBuilder, DeclarationError
|
||||||
from .model import (
|
from .model import (
|
||||||
@@ -18,6 +18,7 @@ from .model import (
|
|||||||
SelectorType,
|
SelectorType,
|
||||||
)
|
)
|
||||||
from .styles import Styles
|
from .styles import Styles
|
||||||
|
from ..suggestions import get_suggestion
|
||||||
from .tokenize import tokenize, tokenize_declarations, Token, tokenize_values
|
from .tokenize import tokenize, tokenize_declarations, Token, tokenize_values
|
||||||
from .tokenizer import EOFError, ReferencedBy
|
from .tokenizer import EOFError, ReferencedBy
|
||||||
|
|
||||||
@@ -209,12 +210,20 @@ def parse_declarations(css: str, path: str) -> Styles:
|
|||||||
return styles_builder.styles
|
return styles_builder.styles
|
||||||
|
|
||||||
|
|
||||||
def _unresolved(
|
def _unresolved(variable_name: str, variables: Sequence[str], token: Token) -> NoReturn:
|
||||||
variable_name: str, location: tuple[int, int]
|
|
||||||
) -> UnresolvedVariableError:
|
message = f"reference to undefined variable '${variable_name}'"
|
||||||
line_idx, col_idx = location
|
|
||||||
return UnresolvedVariableError(
|
suggested_variable = get_suggestion(variable_name, variables)
|
||||||
f"reference to undefined variable '${variable_name}' at line {line_idx + 1}, column {col_idx + 1}."
|
if suggested_variable:
|
||||||
|
message += f"; did you mean '{suggested_variable}'?"
|
||||||
|
|
||||||
|
raise TokenError(
|
||||||
|
token.path,
|
||||||
|
token.code,
|
||||||
|
token.start,
|
||||||
|
message,
|
||||||
|
end=token.end,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -284,9 +293,7 @@ def substitute_references(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise _unresolved(
|
_unresolved(ref_name, variables.keys(), token)
|
||||||
variable_name=ref_name, location=token.location
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
variables.setdefault(variable_name, []).append(token)
|
variables.setdefault(variable_name, []).append(token)
|
||||||
yield token
|
yield token
|
||||||
@@ -306,7 +313,7 @@ def substitute_references(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise _unresolved(variable_name=variable_name, location=token.location)
|
_unresolved(variable_name, variables.keys(), token)
|
||||||
else:
|
else:
|
||||||
yield token
|
yield token
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from .model import RuleSet
|
|||||||
from .parse import parse
|
from .parse import parse
|
||||||
from .styles import RulesMap, Styles
|
from .styles import RulesMap, Styles
|
||||||
from .tokenize import tokenize_values, Token
|
from .tokenize import tokenize_values, Token
|
||||||
from .tokenizer import TokenizeError
|
from .tokenizer import TokenError
|
||||||
from .types import Specificity3, Specificity4
|
from .types import Specificity3, Specificity4
|
||||||
from ..dom import DOMNode
|
from ..dom import DOMNode
|
||||||
from .. import messages
|
from .. import messages
|
||||||
@@ -198,7 +198,7 @@ class Stylesheet:
|
|||||||
is_default_rules=is_default_rules,
|
is_default_rules=is_default_rules,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except TokenizeError:
|
except TokenError:
|
||||||
raise
|
raise
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise StylesheetError(f"failed to parse css; {error}")
|
raise StylesheetError(f"failed to parse css; {error}")
|
||||||
|
|||||||
@@ -15,28 +15,43 @@ from rich.text import Text
|
|||||||
from .._loop import loop_last
|
from .._loop import loop_last
|
||||||
|
|
||||||
|
|
||||||
class TokenizeError(Exception):
|
class TokenError(Exception):
|
||||||
"""Error raised when the CSS cannot be tokenized (syntax error)."""
|
"""Error raised when the CSS cannot be tokenized (syntax error)."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, path: str, code: str, line_no: int, col_no: int, message: str
|
self,
|
||||||
|
path: str,
|
||||||
|
code: str,
|
||||||
|
start: tuple[int, int],
|
||||||
|
message: str,
|
||||||
|
end: tuple[int, int] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
path (str): Path to source or "<object>" if source is parsed from a literal.
|
path (str): Path to source or "<object>" if source is parsed from a literal.
|
||||||
code (str): The code being parsed.
|
code (str): The code being parsed.
|
||||||
line_no (int): Line number of the error.
|
start (tuple[int, int]): Line number of the error.
|
||||||
col_no (int): Column number of the error.
|
|
||||||
message (str): A message associated with the error.
|
message (str): A message associated with the error.
|
||||||
|
end (tuple[int, int]): End location of .
|
||||||
"""
|
"""
|
||||||
|
"""_summary_
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str): Path to source or "<object>" if source is parsed from a literal.
|
||||||
|
code (str): The code being parsed.
|
||||||
|
start (tuple[int, int]): Location of the error.
|
||||||
|
message (str): A message associated with the error.
|
||||||
|
end (tuple[int, int] | None, optional): End location of the error, or None for
|
||||||
|
the same as start. Defaults to None.
|
||||||
|
"""
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
self.code = code
|
self.code = code
|
||||||
self.line_no = line_no
|
self.start = start
|
||||||
self.col_no = col_no
|
self.end = end or start
|
||||||
super().__init__(message)
|
super().__init__(message)
|
||||||
|
|
||||||
@classmethod
|
def _get_snippet(self) -> Panel:
|
||||||
def _get_snippet(cls, code: str, line_no: int) -> Panel:
|
|
||||||
"""Get a short snippet of code around a given line number.
|
"""Get a short snippet of code around a given line number.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -46,9 +61,10 @@ class TokenizeError(Exception):
|
|||||||
Returns:
|
Returns:
|
||||||
Panel: A renderable.
|
Panel: A renderable.
|
||||||
"""
|
"""
|
||||||
|
line_no = self.start[0]
|
||||||
# TODO: Highlight column number
|
# TODO: Highlight column number
|
||||||
syntax = Syntax(
|
syntax = Syntax(
|
||||||
code,
|
self.code,
|
||||||
lexer="scss",
|
lexer="scss",
|
||||||
theme="ansi_light",
|
theme="ansi_light",
|
||||||
line_numbers=True,
|
line_numbers=True,
|
||||||
@@ -56,6 +72,7 @@ class TokenizeError(Exception):
|
|||||||
line_range=(max(0, line_no - 2), line_no + 2),
|
line_range=(max(0, line_no - 2), line_no + 2),
|
||||||
highlight_lines={line_no},
|
highlight_lines={line_no},
|
||||||
)
|
)
|
||||||
|
syntax.stylize_range("reverse bold", self.start, self.end)
|
||||||
return Panel(syntax, border_style="red")
|
return Panel(syntax, border_style="red")
|
||||||
|
|
||||||
def __rich__(self) -> RenderableType:
|
def __rich__(self) -> RenderableType:
|
||||||
@@ -63,14 +80,12 @@ class TokenizeError(Exception):
|
|||||||
errors: list[RenderableType] = []
|
errors: list[RenderableType] = []
|
||||||
|
|
||||||
message = str(self)
|
message = str(self)
|
||||||
errors.append(Text(" Tokenizer error in stylesheet:", style="bold red"))
|
errors.append(Text(" Error in stylesheet:", style="bold red"))
|
||||||
|
|
||||||
errors.append(
|
line_no, col_no = self.start
|
||||||
highlighter(
|
|
||||||
f" {self.path or '<unknown>'}:{self.line_no + 1}:{self.col_no + 1}"
|
errors.append(highlighter(f" {self.path or '<unknown>'}:{line_no}:{col_no}"))
|
||||||
)
|
errors.append(self._get_snippet())
|
||||||
)
|
|
||||||
errors.append(self._get_snippet(self.code, self.line_no + 1))
|
|
||||||
final_message = ""
|
final_message = ""
|
||||||
for is_last, message_part in loop_last(message.split(";")):
|
for is_last, message_part in loop_last(message.split(";")):
|
||||||
end = "" if is_last else "\n"
|
end = "" if is_last else "\n"
|
||||||
@@ -80,7 +95,7 @@ class TokenizeError(Exception):
|
|||||||
return Group(*errors)
|
return Group(*errors)
|
||||||
|
|
||||||
|
|
||||||
class EOFError(TokenizeError):
|
class EOFError(TokenError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -120,6 +135,18 @@ class Token(NamedTuple):
|
|||||||
location: tuple[int, int]
|
location: tuple[int, int]
|
||||||
referenced_by: ReferencedBy | None = None
|
referenced_by: ReferencedBy | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start(self) -> tuple[int, int]:
|
||||||
|
"""Start line and column (1 indexed)."""
|
||||||
|
line, offset = self.location
|
||||||
|
return (line + 1, offset)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def end(self) -> tuple[int, int]:
|
||||||
|
"""End line and column (1 indexed)."""
|
||||||
|
line, offset = self.location
|
||||||
|
return (line + 1, offset + len(self.value))
|
||||||
|
|
||||||
def with_reference(self, by: ReferencedBy | None) -> "Token":
|
def with_reference(self, by: ReferencedBy | None) -> "Token":
|
||||||
"""Return a copy of the Token, with reference information attached.
|
"""Return a copy of the Token, with reference information attached.
|
||||||
This is used for variable substitution, where a variable reference
|
This is used for variable substitution, where a variable reference
|
||||||
@@ -161,19 +188,28 @@ class Tokenizer:
|
|||||||
col_no = self.col_no
|
col_no = self.col_no
|
||||||
if line_no >= len(self.lines):
|
if line_no >= len(self.lines):
|
||||||
if expect._expect_eof:
|
if expect._expect_eof:
|
||||||
return Token("eof", "", self.path, self.code, (line_no, col_no), None)
|
return Token(
|
||||||
|
"eof",
|
||||||
|
"",
|
||||||
|
self.path,
|
||||||
|
self.code,
|
||||||
|
(line_no + 1, col_no + 1),
|
||||||
|
None,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
raise EOFError(
|
raise EOFError(
|
||||||
self.path, self.code, line_no, col_no, "Unexpected end of file"
|
self.path,
|
||||||
|
self.code,
|
||||||
|
(line_no + 1, col_no + 1),
|
||||||
|
"Unexpected end of file",
|
||||||
)
|
)
|
||||||
line = self.lines[line_no]
|
line = self.lines[line_no]
|
||||||
match = expect.match(line, col_no)
|
match = expect.match(line, col_no)
|
||||||
if match is None:
|
if match is None:
|
||||||
raise TokenizeError(
|
raise TokenError(
|
||||||
self.path,
|
self.path,
|
||||||
self.code,
|
self.code,
|
||||||
line_no,
|
(line_no, col_no),
|
||||||
col_no,
|
|
||||||
"expected " + ", ".join(name.upper() for name in expect.names),
|
"expected " + ", ".join(name.upper() for name in expect.names),
|
||||||
)
|
)
|
||||||
iter_groups = iter(match.groups())
|
iter_groups = iter(match.groups())
|
||||||
|
|||||||
@@ -9,39 +9,18 @@ from rich.text import Text
|
|||||||
|
|
||||||
from .color import Color, WHITE
|
from .color import Color, WHITE
|
||||||
|
|
||||||
|
|
||||||
NUMBER_OF_SHADES = 3
|
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 = "#121212"
|
DEFAULT_DARK_SURFACE = "#292929"
|
||||||
|
|
||||||
DEFAULT_LIGHT_SURFACE = "#f5f5f5"
|
DEFAULT_LIGHT_SURFACE = "#f5f5f5"
|
||||||
DEFAULT_LIGHT_BACKGROUND = "#efefef"
|
DEFAULT_LIGHT_BACKGROUND = "#efefef"
|
||||||
|
|
||||||
|
|
||||||
class ColorProperty:
|
|
||||||
"""Descriptor to parse colors."""
|
|
||||||
|
|
||||||
def __set_name__(self, owner: ColorSystem, name: str) -> None:
|
|
||||||
self._name = f"_{name}"
|
|
||||||
|
|
||||||
def __get__(
|
|
||||||
self, obj: ColorSystem, objtype: type[ColorSystem] | None = None
|
|
||||||
) -> Color | None:
|
|
||||||
color = getattr(obj, self._name)
|
|
||||||
if color is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return Color.parse(color)
|
|
||||||
|
|
||||||
def __set__(self, obj: ColorSystem, value: Color | str | None) -> None:
|
|
||||||
if isinstance(value, Color):
|
|
||||||
setattr(obj, self._name, value.css)
|
|
||||||
else:
|
|
||||||
setattr(obj, self._name, value)
|
|
||||||
|
|
||||||
|
|
||||||
class ColorSystem:
|
class ColorSystem:
|
||||||
"""Defines a standard set of colors and variations for building a UI.
|
"""Defines a standard set of colors and variations for building a UI.
|
||||||
|
|
||||||
@@ -63,7 +42,6 @@ class ColorSystem:
|
|||||||
"error",
|
"error",
|
||||||
"success",
|
"success",
|
||||||
"accent",
|
"accent",
|
||||||
"system",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -74,42 +52,30 @@ class ColorSystem:
|
|||||||
error: str | None = None,
|
error: str | None = None,
|
||||||
success: str | None = None,
|
success: str | None = None,
|
||||||
accent: str | None = None,
|
accent: str | None = None,
|
||||||
system: str | None = None,
|
|
||||||
background: str | None = None,
|
background: str | None = None,
|
||||||
surface: str | None = None,
|
surface: str | None = None,
|
||||||
dark_background: str | None = None,
|
|
||||||
dark_surface: str | None = None,
|
|
||||||
panel: str | None = None,
|
panel: str | None = None,
|
||||||
|
dark: bool = False,
|
||||||
|
luminosity_spread: float = 0.15,
|
||||||
|
text_alpha: float = 0.95,
|
||||||
):
|
):
|
||||||
self._primary = primary
|
def parse(color: str | None) -> Color | None:
|
||||||
self._secondary = secondary
|
if color is None:
|
||||||
self._warning = warning
|
return None
|
||||||
self._error = error
|
return Color.parse(color)
|
||||||
self._success = success
|
|
||||||
self._accent = accent
|
|
||||||
self._system = system
|
|
||||||
self._background = background
|
|
||||||
self._surface = surface
|
|
||||||
self._dark_background = dark_background
|
|
||||||
self._dark_surface = dark_surface
|
|
||||||
self._panel = panel
|
|
||||||
|
|
||||||
@property
|
self.primary = Color.parse(primary)
|
||||||
def primary(self) -> Color:
|
self.secondary = parse(secondary)
|
||||||
"""Get the primary color."""
|
self.warning = parse(warning)
|
||||||
return Color.parse(self._primary)
|
self.error = parse(error)
|
||||||
|
self.success = parse(success)
|
||||||
secondary = ColorProperty()
|
self.accent = parse(accent)
|
||||||
warning = ColorProperty()
|
self.background = parse(background)
|
||||||
error = ColorProperty()
|
self.surface = parse(surface)
|
||||||
success = ColorProperty()
|
self.panel = parse(panel)
|
||||||
accent = ColorProperty()
|
self._dark = dark
|
||||||
system = ColorProperty()
|
self._luminosity_spread = luminosity_spread
|
||||||
surface = ColorProperty()
|
self._text_alpha = text_alpha
|
||||||
background = ColorProperty()
|
|
||||||
dark_surface = ColorProperty()
|
|
||||||
dark_background = ColorProperty()
|
|
||||||
panel = ColorProperty()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def shades(self) -> Iterable[str]:
|
def shades(self) -> Iterable[str]:
|
||||||
@@ -123,12 +89,7 @@ class ColorSystem:
|
|||||||
else:
|
else:
|
||||||
yield color
|
yield color
|
||||||
|
|
||||||
def generate(
|
def generate(self) -> dict[str, str]:
|
||||||
self,
|
|
||||||
dark: bool = False,
|
|
||||||
luminosity_spread: float = 0.15,
|
|
||||||
text_alpha: float = 0.9,
|
|
||||||
) -> dict[str, str]:
|
|
||||||
"""Generate a mapping of color name on to a CSS color.
|
"""Generate a mapping of color name on to a CSS color.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -148,22 +109,20 @@ class ColorSystem:
|
|||||||
error = self.error or secondary
|
error = self.error or secondary
|
||||||
success = self.success or secondary
|
success = self.success or secondary
|
||||||
accent = self.accent or primary
|
accent = self.accent or primary
|
||||||
system = self.system or accent
|
|
||||||
|
|
||||||
light_background = self.background or Color.parse(DEFAULT_LIGHT_BACKGROUND)
|
dark = self._dark
|
||||||
dark_background = self.dark_background or Color.parse(DEFAULT_DARK_BACKGROUND)
|
luminosity_spread = self._luminosity_spread
|
||||||
|
text_alpha = self._text_alpha
|
||||||
|
|
||||||
light_surface = self.surface or Color.parse(DEFAULT_LIGHT_SURFACE)
|
if dark:
|
||||||
dark_surface = self.dark_surface or Color.parse(DEFAULT_DARK_SURFACE)
|
background = self.background or Color.parse(DEFAULT_DARK_BACKGROUND)
|
||||||
|
surface = self.surface or Color.parse(DEFAULT_DARK_SURFACE)
|
||||||
|
else:
|
||||||
|
background = self.background or Color.parse(DEFAULT_LIGHT_BACKGROUND)
|
||||||
|
surface = self.surface or Color.parse(DEFAULT_LIGHT_SURFACE)
|
||||||
|
|
||||||
background = dark_background if dark else light_background
|
|
||||||
surface = dark_surface if dark else light_surface
|
|
||||||
|
|
||||||
text = background.get_contrast_text(1.0)
|
|
||||||
if self.panel is None:
|
if self.panel is None:
|
||||||
panel = background.blend(
|
panel = surface.blend(primary, luminosity_spread)
|
||||||
text, luminosity_spread if dark else luminosity_spread
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
panel = self.panel
|
panel = self.panel
|
||||||
|
|
||||||
@@ -199,7 +158,6 @@ class ColorSystem:
|
|||||||
("error", error),
|
("error", error),
|
||||||
("success", success),
|
("success", success),
|
||||||
("accent", accent),
|
("accent", accent),
|
||||||
("system", system),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Colors names that have a dark variant
|
# Colors names that have a dark variant
|
||||||
@@ -207,7 +165,7 @@ class ColorSystem:
|
|||||||
|
|
||||||
for name, color in COLORS:
|
for name, color in COLORS:
|
||||||
is_dark_shade = dark and name in DARK_SHADES
|
is_dark_shade = dark and name in DARK_SHADES
|
||||||
spread = luminosity_spread / 1.5 if is_dark_shade else luminosity_spread
|
spread = luminosity_spread
|
||||||
if name == "panel":
|
if name == "panel":
|
||||||
spread /= 2
|
spread /= 2
|
||||||
for shade_name, luminosity_delta in luminosity_range(spread):
|
for shade_name, luminosity_delta in luminosity_range(spread):
|
||||||
@@ -231,27 +189,39 @@ class ColorSystem:
|
|||||||
|
|
||||||
return colors
|
return colors
|
||||||
|
|
||||||
def __rich__(self) -> Table:
|
|
||||||
@group()
|
|
||||||
def make_shades(dark: bool):
|
|
||||||
colors = self.generate(dark)
|
|
||||||
for name in self.shades:
|
|
||||||
background = colors[name]
|
|
||||||
foreground = colors[f"text-{name}"]
|
|
||||||
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}")
|
def show_design(light: ColorSystem, dark: ColorSystem) -> Table:
|
||||||
|
"""Generate a renderable to show color systems.
|
||||||
|
|
||||||
table = Table(box=None, expand=True)
|
Args:
|
||||||
table.add_column("Light", justify="center")
|
light (ColorSystem): Light ColorSystem.
|
||||||
table.add_column("Dark", justify="center")
|
dark (ColorSystem): Dark ColorSystem
|
||||||
table.add_row(make_shades(False), make_shades(True))
|
|
||||||
return table
|
Returns:
|
||||||
|
Table: Table showing all colors.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@group()
|
||||||
|
def make_shades(system: ColorSystem):
|
||||||
|
colors = system.generate()
|
||||||
|
for name in system.shades:
|
||||||
|
background = colors[name]
|
||||||
|
foreground = colors[f"text-{name}"]
|
||||||
|
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}")
|
||||||
|
|
||||||
|
table = Table(box=None, expand=True)
|
||||||
|
table.add_column("Light", justify="center")
|
||||||
|
table.add_column("Dark", justify="center")
|
||||||
|
table.add_row(make_shades(light), make_shades(dark))
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
@@ -259,4 +229,4 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
from rich import print
|
from rich import print
|
||||||
|
|
||||||
print(DEFAULT_COLORS)
|
print(show_design(DEFAULT_COLORS["light"], DEFAULT_COLORS["dark"]))
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
CSS = """
|
CSS = """
|
||||||
Widget{
|
Widget{
|
||||||
scrollbar-background: $panel-darken-2;
|
scrollbar-background: $panel-darken-1;
|
||||||
scrollbar-background-hover: $panel-darken-3;
|
scrollbar-background-hover: $panel-darken-2;
|
||||||
scrollbar-color: $system;
|
scrollbar-color: $primary-lighten-1;
|
||||||
scrollbar-color-active: $warning-darken-1;
|
scrollbar-color-active: $warning-darken-1;
|
||||||
scrollbar-size-vertical: 2;
|
scrollbar-size-vertical: 2;
|
||||||
scrollbar-size-horizontal: 1;
|
scrollbar-size-horizontal: 1;
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ class Button(Widget, can_focus=True):
|
|||||||
Button {
|
Button {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 3;
|
height: 3;
|
||||||
background: $primary;
|
background: $panel;
|
||||||
color: $text-primary;
|
color: $text-panel;
|
||||||
border: none;
|
border: none;
|
||||||
border-top: tall $primary-lighten-2;
|
border-top: tall $panel-lighten-2;
|
||||||
border-bottom: tall $primary-darken-3;
|
border-bottom: tall $panel-darken-3;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
@@ -47,15 +47,15 @@ class Button(Widget, can_focus=True):
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button:hover {
|
Button:hover {
|
||||||
border-top: tall $primary-lighten-1;
|
border-top: tall $panel-lighten-1;
|
||||||
background: $primary-darken-2;
|
background: $panel-darken-2;
|
||||||
color: $text-primary-darken-2;
|
color: $text-panel-darken-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
Button.-active {
|
Button.-active {
|
||||||
background: $primary;
|
background: $panel;
|
||||||
border-bottom: tall $primary-lighten-2;
|
border-bottom: tall $panel-lighten-2;
|
||||||
border-top: tall $primary-darken-2;
|
border-top: tall $panel-darken-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -113,13 +113,13 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
|||||||
}
|
}
|
||||||
DataTable > .datatable--header {
|
DataTable > .datatable--header {
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
background: $primary-darken-1;
|
background: $primary;
|
||||||
color: $text-primary-darken-1;
|
color: $text-primary;
|
||||||
}
|
}
|
||||||
DataTable > .datatable--fixed {
|
DataTable > .datatable--fixed {
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
background: $primary-darken-2;
|
background: $primary;
|
||||||
color: $text-primary-darken-2;
|
color: $text-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
DataTable > .datatable--odd-row {
|
DataTable > .datatable--odd-row {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
from textual.color import Color
|
from textual.color import Color
|
||||||
from textual.css._help_renderables import HelpText
|
from textual.css._help_renderables import HelpText
|
||||||
from textual.css.stylesheet import Stylesheet, StylesheetParseError, CssSource
|
from textual.css.stylesheet import Stylesheet, StylesheetParseError, CssSource
|
||||||
from textual.css.tokenizer import TokenizeError
|
from textual.css.tokenizer import TokenError
|
||||||
from textual.dom import DOMNode
|
from textual.dom import DOMNode
|
||||||
from textual.geometry import Spacing
|
from textual.geometry import Spacing
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
@@ -145,7 +145,7 @@ def test_stylesheet_apply_user_css_over_widget_css():
|
|||||||
["ansi_dark_cyan", pytest.raises(StylesheetParseError), None],
|
["ansi_dark_cyan", pytest.raises(StylesheetParseError), None],
|
||||||
["red 4", pytest.raises(StylesheetParseError), None], # space in it
|
["red 4", pytest.raises(StylesheetParseError), None], # space in it
|
||||||
["1", pytest.raises(StylesheetParseError), None], # invalid value
|
["1", pytest.raises(StylesheetParseError), None], # invalid value
|
||||||
["()", pytest.raises(TokenizeError), None], # invalid tokens
|
["()", pytest.raises(TokenError), None], # invalid tokens
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_color_property_parsing(css_value, expectation, expected_color):
|
def test_color_property_parsing(css_value, expectation, expected_color):
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from textual.css.tokenize import tokenize
|
from textual.css.tokenize import tokenize
|
||||||
from textual.css.tokenizer import Token, TokenizeError
|
from textual.css.tokenizer import Token, TokenError
|
||||||
|
|
||||||
VALID_VARIABLE_NAMES = [
|
VALID_VARIABLE_NAMES = [
|
||||||
"warning-text",
|
"warning-text",
|
||||||
@@ -331,7 +331,7 @@ def test_variable_declaration_no_semicolon():
|
|||||||
|
|
||||||
def test_variable_declaration_invalid_value():
|
def test_variable_declaration_invalid_value():
|
||||||
css = "$x:(@$12x)"
|
css = "$x:(@$12x)"
|
||||||
with pytest.raises(TokenizeError):
|
with pytest.raises(TokenError):
|
||||||
list(tokenize(css, ""))
|
list(tokenize(css, ""))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user