mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -7,13 +7,6 @@
|
||||
}
|
||||
|
||||
|
||||
* {
|
||||
scrollbar-background: $panel-darken-2;
|
||||
scrollbar-background-hover: $panel-darken-3;
|
||||
scrollbar-color: $system;
|
||||
scrollbar-color-active: $accent-darken-1;
|
||||
}
|
||||
|
||||
App > Screen {
|
||||
layout: dock;
|
||||
docks: side=left/1;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
|
||||
|
||||
* {
|
||||
* {
|
||||
transition: color 300ms linear, background 300ms linear;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
*:hover {
|
||||
@@ -15,7 +15,7 @@
|
||||
App > Screen {
|
||||
layout: dock;
|
||||
docks: side=left/1;
|
||||
background: $surface;
|
||||
background: $surfaceX;
|
||||
color: $text-surface;
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ DataTable {
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
color: $text-primary;
|
||||
background: $primary-background;
|
||||
color: $text-panel;
|
||||
background: $panel;
|
||||
dock: side;
|
||||
width: 30;
|
||||
offset-x: -100%;
|
||||
@@ -43,33 +43,35 @@ DataTable {
|
||||
}
|
||||
|
||||
#sidebar .title {
|
||||
height: 3;
|
||||
background: $primary-background-darken-2;
|
||||
color: $text-primary-darken-2 ;
|
||||
border-right: outer $primary-darken-3;
|
||||
height: 1;
|
||||
background: $primary-background-darken-1;
|
||||
color: $text-primary-background-darken-1;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .user {
|
||||
height: 8;
|
||||
background: $primary-background-darken-1;
|
||||
color: $text-primary-darken-1;
|
||||
border-right: outer $primary-background-darken-3;
|
||||
background: $panel-darken-1;
|
||||
color: $text-panel-darken-1;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#sidebar .content {
|
||||
background: $primary-background;
|
||||
color: $text-primary-background;
|
||||
border-right: outer $primary-background-darken-3;
|
||||
background: $surface;
|
||||
color: $text-surface;
|
||||
border-right: wide $background;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
#header {
|
||||
color: $text-secondary-background-darken-1;
|
||||
background: $secondary-background-darken-1;
|
||||
height: 3;
|
||||
color: $text-secondary-background;
|
||||
background: $secondary-background;
|
||||
height: 1;
|
||||
content-align: center middle;
|
||||
|
||||
|
||||
}
|
||||
|
||||
#content {
|
||||
@@ -90,7 +92,7 @@ Tweet {
|
||||
layout: vertical;
|
||||
/* border: outer $primary; */
|
||||
padding: 1;
|
||||
border: wide $panel-darken-2;
|
||||
border: wide $panel;
|
||||
overflow: auto;
|
||||
/* scrollbar-gutter: stable; */
|
||||
align-horizontal: center;
|
||||
@@ -175,16 +177,16 @@ Tweet.scroll-horizontal TweetBody {
|
||||
|
||||
OptionItem {
|
||||
height: 3;
|
||||
background: $primary-background;
|
||||
border-right: outer $primary-background-darken-2;
|
||||
background: $panel;
|
||||
border-right: wide $background;
|
||||
border-left: blank;
|
||||
content-align: center middle;
|
||||
}
|
||||
|
||||
OptionItem:hover {
|
||||
height: 3;
|
||||
color: $secondary;
|
||||
background: $primary-background-darken-1;
|
||||
color: $text-primary;
|
||||
background: $primary-darken-1;
|
||||
/* border-top: hkey $accent2-darken-3;
|
||||
border-bottom: hkey $accent2-darken-3; */
|
||||
text-style: bold;
|
||||
@@ -196,8 +198,8 @@ Error {
|
||||
height:3;
|
||||
background: $error;
|
||||
color: $text-error;
|
||||
border-top: wide $error-darken-1;
|
||||
border-bottom: wide $error-darken-1;
|
||||
border-top: tall $error-darken-2;
|
||||
border-bottom: tall $error-darken-2;
|
||||
|
||||
padding: 0;
|
||||
text-style: bold;
|
||||
@@ -209,8 +211,8 @@ Warning {
|
||||
height:3;
|
||||
background: $warning;
|
||||
color: $text-warning-fade-1;
|
||||
border-top: wide $warning-darken-1;
|
||||
border-bottom: wide $warning-darken-1;
|
||||
border-top: tall $warning-darken-2;
|
||||
border-bottom: tall $warning-darken-2;
|
||||
|
||||
text-style: bold;
|
||||
align-horizontal: center;
|
||||
@@ -218,16 +220,16 @@ Warning {
|
||||
|
||||
Success {
|
||||
width: 100%;
|
||||
width:90%;
|
||||
|
||||
height:auto;
|
||||
box-sizing: border-box;
|
||||
background: $success;
|
||||
color: $text-success;
|
||||
|
||||
border-top: hkey $success-darken-1;
|
||||
border-bottom: hkey $success-darken-1;
|
||||
border-top: hkey $success-darken-2;
|
||||
border-bottom: hkey $success-darken-2;
|
||||
|
||||
text-style: bold underline;
|
||||
text-style: bold ;
|
||||
|
||||
align-horizontal: center;
|
||||
}
|
||||
|
||||
@@ -179,21 +179,21 @@ app = BasicApp()
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
from textual.geometry import Region
|
||||
from textual.color import Color
|
||||
# from textual.geometry import Region
|
||||
# from textual.color import Color
|
||||
|
||||
print(Region.intersection.cache_info())
|
||||
print(Region.overlaps.cache_info())
|
||||
print(Region.union.cache_info())
|
||||
print(Region.split_vertical.cache_info())
|
||||
print(Region.__contains__.cache_info())
|
||||
from textual.css.scalar import Scalar
|
||||
# print(Region.intersection.cache_info())
|
||||
# print(Region.overlaps.cache_info())
|
||||
# print(Region.union.cache_info())
|
||||
# print(Region.split_vertical.cache_info())
|
||||
# print(Region.__contains__.cache_info())
|
||||
# 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.cells import cached_cell_len
|
||||
# from rich.style import Style
|
||||
# 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())
|
||||
|
||||
@@ -609,6 +609,7 @@ class Compositor:
|
||||
Returns:
|
||||
SegmentLines: A renderable
|
||||
"""
|
||||
|
||||
width, height = self.size
|
||||
screen_region = Region(0, 0, width, height)
|
||||
|
||||
@@ -706,7 +707,7 @@ class Compositor:
|
||||
region, clip = self.regions[widget]
|
||||
offset = region.offset
|
||||
intersection = clip.intersection
|
||||
for dirty_region in widget.get_dirty_regions():
|
||||
for dirty_region in widget._exchange_repaint_regions():
|
||||
update_region = intersection(dirty_region.translate(offset))
|
||||
if update_region:
|
||||
add_region(update_region)
|
||||
|
||||
@@ -80,16 +80,26 @@ _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED = sys.version_info >= (3, 10, 0)
|
||||
LayoutDefinition = "dict[str, Any]"
|
||||
|
||||
|
||||
DEFAULT_COLORS = ColorSystem(
|
||||
primary="#2A4E6E",
|
||||
secondary="#ffa62b",
|
||||
warning="#ffa62b",
|
||||
error="#ba3c5b",
|
||||
success="#4EBF71",
|
||||
accent="#1A75B4",
|
||||
system="#5a4599",
|
||||
dark_surface="#292929",
|
||||
)
|
||||
DEFAULT_COLORS = {
|
||||
"dark": ColorSystem(
|
||||
primary="#004578",
|
||||
secondary="#ffa62b",
|
||||
warning="#ffa62b",
|
||||
error="#ba3c5b",
|
||||
success="#4EBF71",
|
||||
accent="#0178D4",
|
||||
dark=True,
|
||||
),
|
||||
"light": ColorSystem(
|
||||
primary="#004578",
|
||||
secondary="#ffa62b",
|
||||
warning="#ffa62b",
|
||||
error="#ba3c5b",
|
||||
success="#4EBF71",
|
||||
accent="#0178D4",
|
||||
dark=False,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
ComposeResult = Iterable[Widget]
|
||||
@@ -338,7 +348,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
Returns:
|
||||
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
|
||||
|
||||
def watch_dark(self, dark: bool) -> None:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from rich.console import ConsoleOptions, Console
|
||||
from rich.console import ConsoleOptions, Console, RenderResult
|
||||
from rich.traceback import Traceback
|
||||
|
||||
from ._help_renderables import HelpText
|
||||
from .tokenize import Token
|
||||
from .tokenizer import TokenError
|
||||
|
||||
|
||||
class DeclarationError(Exception):
|
||||
@@ -15,11 +16,11 @@ class DeclarationError(Exception):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class UnresolvedVariableError(NameError):
|
||||
class StyleTypeError(TypeError):
|
||||
pass
|
||||
|
||||
|
||||
class StyleTypeError(TypeError):
|
||||
class UnresolvedVariableError(TokenError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -35,7 +36,9 @@ class StyleValueError(ValueError):
|
||||
super().__init__(*args)
|
||||
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__)
|
||||
if self.help_text is not None:
|
||||
yield ""
|
||||
|
||||
@@ -2,12 +2,12 @@ from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
from pathlib import PurePath
|
||||
from typing import Iterator, Iterable
|
||||
from typing import Iterator, Iterable, NoReturn, Sequence
|
||||
|
||||
from rich import print
|
||||
|
||||
from textual.css.errors import UnresolvedVariableError
|
||||
from textual.css.types import Specificity3
|
||||
from .errors import UnresolvedVariableError
|
||||
from .types import Specificity3
|
||||
from ._styles_builder import StylesBuilder, DeclarationError
|
||||
from .model import (
|
||||
Declaration,
|
||||
@@ -18,6 +18,7 @@ from .model import (
|
||||
SelectorType,
|
||||
)
|
||||
from .styles import Styles
|
||||
from ..suggestions import get_suggestion
|
||||
from .tokenize import tokenize, tokenize_declarations, Token, tokenize_values
|
||||
from .tokenizer import EOFError, ReferencedBy
|
||||
|
||||
@@ -209,12 +210,29 @@ def parse_declarations(css: str, path: str) -> Styles:
|
||||
return styles_builder.styles
|
||||
|
||||
|
||||
def _unresolved(
|
||||
variable_name: str, location: tuple[int, int]
|
||||
) -> UnresolvedVariableError:
|
||||
line_idx, col_idx = location
|
||||
return UnresolvedVariableError(
|
||||
f"reference to undefined variable '${variable_name}' at line {line_idx + 1}, column {col_idx + 1}."
|
||||
def _unresolved(variable_name: str, variables: Iterable[str], token: Token) -> NoReturn:
|
||||
"""Raise a TokenError regarding an unresolved variable.
|
||||
|
||||
Args:
|
||||
variable_name (str): A variable name.
|
||||
variables (Iterable[str]): Possible choices used to generate suggestion.
|
||||
token (Token): The Token.
|
||||
|
||||
Raises:
|
||||
UnresolvedVariableError: Always raises a TokenError.
|
||||
|
||||
"""
|
||||
message = f"reference to undefined variable '${variable_name}'"
|
||||
suggested_variable = get_suggestion(variable_name, list(variables))
|
||||
if suggested_variable:
|
||||
message += f"; did you mean '${suggested_variable}'?"
|
||||
|
||||
raise UnresolvedVariableError(
|
||||
token.path,
|
||||
token.code,
|
||||
token.start,
|
||||
message,
|
||||
end=token.end,
|
||||
)
|
||||
|
||||
|
||||
@@ -284,9 +302,7 @@ def substitute_references(
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise _unresolved(
|
||||
variable_name=ref_name, location=token.location
|
||||
)
|
||||
_unresolved(ref_name, variables.keys(), token)
|
||||
else:
|
||||
variables.setdefault(variable_name, []).append(token)
|
||||
yield token
|
||||
@@ -306,7 +322,7 @@ def substitute_references(
|
||||
)
|
||||
)
|
||||
else:
|
||||
raise _unresolved(variable_name=variable_name, location=token.location)
|
||||
_unresolved(variable_name, variables.keys(), token)
|
||||
else:
|
||||
yield token
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from .model import RuleSet
|
||||
from .parse import parse
|
||||
from .styles import RulesMap, Styles
|
||||
from .tokenize import tokenize_values, Token
|
||||
from .tokenizer import TokenizeError
|
||||
from .tokenizer import TokenError
|
||||
from .types import Specificity3, Specificity4
|
||||
from ..dom import DOMNode
|
||||
from .. import messages
|
||||
@@ -198,7 +198,7 @@ class Stylesheet:
|
||||
is_default_rules=is_default_rules,
|
||||
)
|
||||
)
|
||||
except TokenizeError:
|
||||
except TokenError:
|
||||
raise
|
||||
except Exception as error:
|
||||
raise StylesheetError(f"failed to parse css; {error}")
|
||||
|
||||
@@ -15,28 +15,33 @@ from rich.text import Text
|
||||
from .._loop import loop_last
|
||||
|
||||
|
||||
class TokenizeError(Exception):
|
||||
class TokenError(Exception):
|
||||
"""Error raised when the CSS cannot be tokenized (syntax error)."""
|
||||
|
||||
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:
|
||||
"""
|
||||
Args:
|
||||
path (str): Path to source or "<object>" if source is parsed from a literal.
|
||||
code (str): The code being parsed.
|
||||
line_no (int): Line number of the error.
|
||||
col_no (int): Column number of the error.
|
||||
start (tuple[int, int]): Line number of the error.
|
||||
message (str): A message associated with the error.
|
||||
end (tuple[int, int] | None): End location of token, or None if not known. Defaults to None.
|
||||
"""
|
||||
|
||||
self.path = path
|
||||
self.code = code
|
||||
self.line_no = line_no
|
||||
self.col_no = col_no
|
||||
self.start = start
|
||||
self.end = end or start
|
||||
super().__init__(message)
|
||||
|
||||
@classmethod
|
||||
def _get_snippet(cls, code: str, line_no: int) -> Panel:
|
||||
def _get_snippet(self) -> Panel:
|
||||
"""Get a short snippet of code around a given line number.
|
||||
|
||||
Args:
|
||||
@@ -46,9 +51,10 @@ class TokenizeError(Exception):
|
||||
Returns:
|
||||
Panel: A renderable.
|
||||
"""
|
||||
line_no = self.start[0]
|
||||
# TODO: Highlight column number
|
||||
syntax = Syntax(
|
||||
code,
|
||||
self.code,
|
||||
lexer="scss",
|
||||
theme="ansi_light",
|
||||
line_numbers=True,
|
||||
@@ -56,6 +62,7 @@ class TokenizeError(Exception):
|
||||
line_range=(max(0, line_no - 2), line_no + 2),
|
||||
highlight_lines={line_no},
|
||||
)
|
||||
syntax.stylize_range("reverse bold", self.start, self.end)
|
||||
return Panel(syntax, border_style="red")
|
||||
|
||||
def __rich__(self) -> RenderableType:
|
||||
@@ -63,24 +70,29 @@ class TokenizeError(Exception):
|
||||
errors: list[RenderableType] = []
|
||||
|
||||
message = str(self)
|
||||
errors.append(Text(" Tokenizer error in stylesheet:", style="bold red"))
|
||||
errors.append(Text(" Error in stylesheet:", style="bold red"))
|
||||
|
||||
line_no, col_no = self.start
|
||||
|
||||
errors.append(highlighter(f" {self.path or '<unknown>'}:{line_no}:{col_no}"))
|
||||
errors.append(self._get_snippet())
|
||||
|
||||
final_message = "\n".join(
|
||||
f"• {message_part.strip()}" for message_part in message.split(";")
|
||||
)
|
||||
errors.append(
|
||||
highlighter(
|
||||
f" {self.path or '<unknown>'}:{self.line_no + 1}:{self.col_no + 1}"
|
||||
Padding(
|
||||
highlighter(
|
||||
Text(final_message, "red"),
|
||||
),
|
||||
pad=(0, 1),
|
||||
)
|
||||
)
|
||||
errors.append(self._get_snippet(self.code, self.line_no + 1))
|
||||
final_message = ""
|
||||
for is_last, message_part in loop_last(message.split(";")):
|
||||
end = "" if is_last else "\n"
|
||||
final_message += f"• {message_part.strip()};{end}"
|
||||
errors.append(Padding(highlighter(Text(final_message, "red")), pad=(0, 1)))
|
||||
|
||||
return Group(*errors)
|
||||
|
||||
|
||||
class EOFError(TokenizeError):
|
||||
class EOFError(TokenError):
|
||||
pass
|
||||
|
||||
|
||||
@@ -120,6 +132,18 @@ class Token(NamedTuple):
|
||||
location: tuple[int, int]
|
||||
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":
|
||||
"""Return a copy of the Token, with reference information attached.
|
||||
This is used for variable substitution, where a variable reference
|
||||
@@ -161,19 +185,28 @@ class Tokenizer:
|
||||
col_no = self.col_no
|
||||
if line_no >= len(self.lines):
|
||||
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:
|
||||
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]
|
||||
match = expect.match(line, col_no)
|
||||
if match is None:
|
||||
raise TokenizeError(
|
||||
raise TokenError(
|
||||
self.path,
|
||||
self.code,
|
||||
line_no,
|
||||
col_no,
|
||||
(line_no, col_no),
|
||||
"expected " + ", ".join(name.upper() for name in expect.names),
|
||||
)
|
||||
iter_groups = iter(match.groups())
|
||||
|
||||
@@ -9,39 +9,18 @@ from rich.text import Text
|
||||
|
||||
from .color import Color, WHITE
|
||||
|
||||
|
||||
NUMBER_OF_SHADES = 3
|
||||
|
||||
# Where no content exists
|
||||
DEFAULT_DARK_BACKGROUND = "#000000"
|
||||
# What text usually goes on top off
|
||||
DEFAULT_DARK_SURFACE = "#121212"
|
||||
DEFAULT_DARK_SURFACE = "#292929"
|
||||
|
||||
DEFAULT_LIGHT_SURFACE = "#f5f5f5"
|
||||
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:
|
||||
"""Defines a standard set of colors and variations for building a UI.
|
||||
|
||||
@@ -63,7 +42,6 @@ class ColorSystem:
|
||||
"error",
|
||||
"success",
|
||||
"accent",
|
||||
"system",
|
||||
]
|
||||
|
||||
def __init__(
|
||||
@@ -74,42 +52,30 @@ class ColorSystem:
|
||||
error: str | None = None,
|
||||
success: str | None = None,
|
||||
accent: str | None = None,
|
||||
system: str | None = None,
|
||||
background: str | None = None,
|
||||
surface: str | None = None,
|
||||
dark_background: str | None = None,
|
||||
dark_surface: str | None = None,
|
||||
panel: str | None = None,
|
||||
dark: bool = False,
|
||||
luminosity_spread: float = 0.15,
|
||||
text_alpha: float = 0.95,
|
||||
):
|
||||
self._primary = primary
|
||||
self._secondary = secondary
|
||||
self._warning = warning
|
||||
self._error = error
|
||||
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
|
||||
def parse(color: str | None) -> Color | None:
|
||||
if color is None:
|
||||
return None
|
||||
return Color.parse(color)
|
||||
|
||||
@property
|
||||
def primary(self) -> Color:
|
||||
"""Get the primary color."""
|
||||
return Color.parse(self._primary)
|
||||
|
||||
secondary = ColorProperty()
|
||||
warning = ColorProperty()
|
||||
error = ColorProperty()
|
||||
success = ColorProperty()
|
||||
accent = ColorProperty()
|
||||
system = ColorProperty()
|
||||
surface = ColorProperty()
|
||||
background = ColorProperty()
|
||||
dark_surface = ColorProperty()
|
||||
dark_background = ColorProperty()
|
||||
panel = ColorProperty()
|
||||
self.primary = Color.parse(primary)
|
||||
self.secondary = parse(secondary)
|
||||
self.warning = parse(warning)
|
||||
self.error = parse(error)
|
||||
self.success = parse(success)
|
||||
self.accent = parse(accent)
|
||||
self.background = parse(background)
|
||||
self.surface = parse(surface)
|
||||
self.panel = parse(panel)
|
||||
self._dark = dark
|
||||
self._luminosity_spread = luminosity_spread
|
||||
self._text_alpha = text_alpha
|
||||
|
||||
@property
|
||||
def shades(self) -> Iterable[str]:
|
||||
@@ -123,12 +89,7 @@ class ColorSystem:
|
||||
else:
|
||||
yield color
|
||||
|
||||
def generate(
|
||||
self,
|
||||
dark: bool = False,
|
||||
luminosity_spread: float = 0.15,
|
||||
text_alpha: float = 0.9,
|
||||
) -> dict[str, str]:
|
||||
def generate(self) -> dict[str, str]:
|
||||
"""Generate a mapping of color name on to a CSS color.
|
||||
|
||||
Args:
|
||||
@@ -148,22 +109,20 @@ class ColorSystem:
|
||||
error = self.error or secondary
|
||||
success = self.success or secondary
|
||||
accent = self.accent or primary
|
||||
system = self.system or accent
|
||||
|
||||
light_background = self.background or Color.parse(DEFAULT_LIGHT_BACKGROUND)
|
||||
dark_background = self.dark_background or Color.parse(DEFAULT_DARK_BACKGROUND)
|
||||
dark = self._dark
|
||||
luminosity_spread = self._luminosity_spread
|
||||
text_alpha = self._text_alpha
|
||||
|
||||
light_surface = self.surface or Color.parse(DEFAULT_LIGHT_SURFACE)
|
||||
dark_surface = self.dark_surface or Color.parse(DEFAULT_DARK_SURFACE)
|
||||
if dark:
|
||||
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:
|
||||
panel = background.blend(
|
||||
text, luminosity_spread if dark else luminosity_spread
|
||||
)
|
||||
panel = surface.blend(primary, luminosity_spread)
|
||||
else:
|
||||
panel = self.panel
|
||||
|
||||
@@ -199,7 +158,6 @@ class ColorSystem:
|
||||
("error", error),
|
||||
("success", success),
|
||||
("accent", accent),
|
||||
("system", system),
|
||||
]
|
||||
|
||||
# Colors names that have a dark variant
|
||||
@@ -207,7 +165,7 @@ class ColorSystem:
|
||||
|
||||
for name, color in COLORS:
|
||||
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":
|
||||
spread /= 2
|
||||
for shade_name, luminosity_delta in luminosity_range(spread):
|
||||
@@ -231,27 +189,39 @@ class ColorSystem:
|
||||
|
||||
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)
|
||||
table.add_column("Light", justify="center")
|
||||
table.add_column("Dark", justify="center")
|
||||
table.add_row(make_shades(False), make_shades(True))
|
||||
return table
|
||||
Args:
|
||||
light (ColorSystem): Light ColorSystem.
|
||||
dark (ColorSystem): Dark ColorSystem
|
||||
|
||||
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__":
|
||||
@@ -259,4 +229,4 @@ if __name__ == "__main__":
|
||||
|
||||
from rich import print
|
||||
|
||||
print(DEFAULT_COLORS)
|
||||
print(show_design(DEFAULT_COLORS["light"], DEFAULT_COLORS["dark"]))
|
||||
|
||||
@@ -40,16 +40,6 @@ class ScrollView(Widget):
|
||||
"""Not transparent, i.e. renders something."""
|
||||
return False
|
||||
|
||||
def get_dirty_regions(self) -> Collection[Region]:
|
||||
"""Get regions which require a repaint.
|
||||
|
||||
Returns:
|
||||
Collection[Region]: Regions to repaint.
|
||||
"""
|
||||
regions = self._dirty_regions.copy()
|
||||
self._dirty_regions.clear()
|
||||
return regions
|
||||
|
||||
def on_mount(self):
|
||||
self._refresh_scrollbars()
|
||||
|
||||
|
||||
@@ -71,9 +71,9 @@ class Widget(DOMNode):
|
||||
|
||||
CSS = """
|
||||
Widget{
|
||||
scrollbar-background: $panel-darken-2;
|
||||
scrollbar-background-hover: $panel-darken-3;
|
||||
scrollbar-color: $system;
|
||||
scrollbar-background: $panel-darken-1;
|
||||
scrollbar-background-hover: $panel-darken-2;
|
||||
scrollbar-color: $primary-lighten-1;
|
||||
scrollbar-color-active: $warning-darken-1;
|
||||
scrollbar-size-vertical: 2;
|
||||
scrollbar-size-horizontal: 1;
|
||||
@@ -106,7 +106,10 @@ class Widget(DOMNode):
|
||||
self._horizontal_scrollbar: ScrollBar | None = None
|
||||
|
||||
self._render_cache = RenderCache(Size(0, 0), [])
|
||||
# Regions which need to be updated (in Widget)
|
||||
self._dirty_regions: set[Region] = set()
|
||||
# Regions which need to be transferred from cache to screen
|
||||
self._repaint_regions: set[Region] = set()
|
||||
|
||||
# Cache the auto content dimensions
|
||||
# TODO: add mechanism to explicitly clear this
|
||||
@@ -549,24 +552,27 @@ class Widget(DOMNode):
|
||||
*regions (Region): Regions which require a repaint.
|
||||
|
||||
"""
|
||||
|
||||
if regions:
|
||||
content_offset = self.content_offset
|
||||
widget_regions = [region.translate(content_offset) for region in regions]
|
||||
self._dirty_regions.update(widget_regions)
|
||||
self._repaint_regions.update(widget_regions)
|
||||
self._styles_cache.set_dirty(*widget_regions)
|
||||
else:
|
||||
self._dirty_regions.clear()
|
||||
self._repaint_regions.clear()
|
||||
self._styles_cache.clear()
|
||||
self._dirty_regions.add(self.outer_size.region)
|
||||
self._repaint_regions.add(self.outer_size.region)
|
||||
|
||||
def get_dirty_regions(self) -> Collection[Region]:
|
||||
"""Get regions which require a repaint.
|
||||
def _exchange_repaint_regions(self) -> Collection[Region]:
|
||||
"""Get a copy of the regions which need a repaint, and clear internal cache.
|
||||
|
||||
Returns:
|
||||
Collection[Region]: Regions to repaint.
|
||||
"""
|
||||
regions = self._dirty_regions.copy()
|
||||
regions = self._repaint_regions.copy()
|
||||
self._repaint_regions.clear()
|
||||
return regions
|
||||
|
||||
def scroll_to(
|
||||
@@ -956,10 +962,10 @@ class Widget(DOMNode):
|
||||
"""Render the widget in to lines.
|
||||
|
||||
Args:
|
||||
crop (Region): Region within visible area to.
|
||||
crop (Region): Region within visible area to render.
|
||||
|
||||
Returns:
|
||||
Lines: A list of list of segments
|
||||
Lines: A list of list of segments.
|
||||
"""
|
||||
lines = self._styles_cache.render_widget(self, crop)
|
||||
return lines
|
||||
|
||||
@@ -33,11 +33,11 @@ class Button(Widget, can_focus=True):
|
||||
Button {
|
||||
width: auto;
|
||||
height: 3;
|
||||
background: $primary;
|
||||
color: $text-primary;
|
||||
background: $panel;
|
||||
color: $text-panel;
|
||||
border: none;
|
||||
border-top: tall $primary-lighten-2;
|
||||
border-bottom: tall $primary-darken-3;
|
||||
border-top: tall $panel-lighten-2;
|
||||
border-bottom: tall $panel-darken-3;
|
||||
content-align: center middle;
|
||||
text-style: bold;
|
||||
}
|
||||
@@ -47,15 +47,15 @@ class Button(Widget, can_focus=True):
|
||||
}
|
||||
|
||||
Button:hover {
|
||||
border-top: tall $primary-lighten-1;
|
||||
background: $primary-darken-2;
|
||||
color: $text-primary-darken-2;
|
||||
border-top: tall $panel-lighten-1;
|
||||
background: $panel-darken-2;
|
||||
color: $text-panel-darken-2;
|
||||
}
|
||||
|
||||
Button.-active {
|
||||
background: $primary;
|
||||
border-bottom: tall $primary-lighten-2;
|
||||
border-top: tall $primary-darken-2;
|
||||
background: $panel;
|
||||
border-bottom: tall $panel-lighten-2;
|
||||
border-top: tall $panel-darken-2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -113,13 +113,13 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
}
|
||||
DataTable > .datatable--header {
|
||||
text-style: bold;
|
||||
background: $primary-darken-1;
|
||||
color: $text-primary-darken-1;
|
||||
background: $primary;
|
||||
color: $text-primary;
|
||||
}
|
||||
DataTable > .datatable--fixed {
|
||||
text-style: bold;
|
||||
background: $primary-darken-2;
|
||||
color: $text-primary-darken-2;
|
||||
background: $primary;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
DataTable > .datatable--odd-row {
|
||||
|
||||
@@ -907,17 +907,20 @@ class TestParseText:
|
||||
class TestParseColor:
|
||||
"""More in-depth tests around parsing of CSS colors"""
|
||||
|
||||
@pytest.mark.parametrize("value,result", [
|
||||
("rgb(1,255,50)", Color(1, 255, 50)),
|
||||
("rgb( 1, 255,50 )", Color(1, 255, 50)),
|
||||
("rgba( 1, 255,50,0.3 )", Color(1, 255, 50, 0.3)),
|
||||
("rgba( 1, 255,50, 1.3 )", Color(1, 255, 50, 1.0)),
|
||||
("hsl( 180, 50%, 50% )", Color(64, 191, 191)),
|
||||
("hsl(180,50%,50%)", Color(64, 191, 191)),
|
||||
("hsla(180,50%,50%,0.25)", Color(64, 191, 191, 0.25)),
|
||||
("hsla( 180, 50% ,50%,0.25 )", Color(64, 191, 191, 0.25)),
|
||||
("hsla( 180, 50% , 50% , 1.5 )", Color(64, 191, 191)),
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"value,result",
|
||||
[
|
||||
("rgb(1,255,50)", Color(1, 255, 50)),
|
||||
("rgb( 1, 255,50 )", Color(1, 255, 50)),
|
||||
("rgba( 1, 255,50,0.3 )", Color(1, 255, 50, 0.3)),
|
||||
("rgba( 1, 255,50, 1.3 )", Color(1, 255, 50, 1.0)),
|
||||
("hsl( 180, 50%, 50% )", Color(64, 191, 191)),
|
||||
("hsl(180,50%,50%)", Color(64, 191, 191)),
|
||||
("hsla(180,50%,50%,0.25)", Color(64, 191, 191, 0.25)),
|
||||
("hsla( 180, 50% ,50%,0.25 )", Color(64, 191, 191, 0.25)),
|
||||
("hsla( 180, 50% , 50% , 1.5 )", Color(64, 191, 191)),
|
||||
],
|
||||
)
|
||||
def test_rgb_and_hsl(self, value, result):
|
||||
css = f""".box {{
|
||||
color: {value};
|
||||
@@ -1021,6 +1024,7 @@ class TestParseOverflow:
|
||||
assert styles.overflow_x == "hidden"
|
||||
assert styles.overflow_y == "auto"
|
||||
|
||||
|
||||
class TestParseTransition:
|
||||
@pytest.mark.parametrize(
|
||||
"duration, parsed_duration",
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
from textual.color import Color
|
||||
from textual.css._help_renderables import HelpText
|
||||
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.geometry import Spacing
|
||||
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],
|
||||
["red 4", pytest.raises(StylesheetParseError), None], # space in it
|
||||
["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):
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
|
||||
from textual.css.tokenize import tokenize
|
||||
from textual.css.tokenizer import Token, TokenizeError
|
||||
from textual.css.tokenizer import Token, TokenError
|
||||
|
||||
VALID_VARIABLE_NAMES = [
|
||||
"warning-text",
|
||||
@@ -331,7 +331,7 @@ def test_variable_declaration_no_semicolon():
|
||||
|
||||
def test_variable_declaration_invalid_value():
|
||||
css = "$x:(@$12x)"
|
||||
with pytest.raises(TokenizeError):
|
||||
with pytest.raises(TokenError):
|
||||
list(tokenize(css, ""))
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user