mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' into dont-render-margin
This commit is contained in:
@@ -1,50 +1,44 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import os
|
|
||||||
|
|
||||||
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from typing import Any, Callable, ClassVar, Iterable, Type, TypeVar
|
|
||||||
import warnings
|
import warnings
|
||||||
|
from typing import Any, Callable, Iterable, Type, TypeVar
|
||||||
from rich.control import Control
|
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.screen import Screen
|
|
||||||
from rich.console import Console, RenderableType
|
from rich.console import Console, RenderableType
|
||||||
|
from rich.control import Control
|
||||||
from rich.measure import Measurement
|
from rich.measure import Measurement
|
||||||
|
from rich.screen import Screen
|
||||||
from rich.traceback import Traceback
|
from rich.traceback import Traceback
|
||||||
|
|
||||||
|
|
||||||
from . import events
|
|
||||||
from . import actions
|
from . import actions
|
||||||
from .dom import DOMNode
|
from . import events
|
||||||
from ._animator import Animator
|
|
||||||
from .binding import Bindings, NoBinding
|
|
||||||
from .geometry import Offset, Region, Size
|
|
||||||
from . import log
|
from . import log
|
||||||
|
from . import messages
|
||||||
|
from ._animator import Animator
|
||||||
from ._callback import invoke
|
from ._callback import invoke
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError
|
|
||||||
from ._event_broker import extract_handler_actions, NoHandler
|
from ._event_broker import extract_handler_actions, NoHandler
|
||||||
|
from ._linux_driver import LinuxDriver
|
||||||
|
from ._profile import timer
|
||||||
|
from .binding import Bindings, NoBinding
|
||||||
|
from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError
|
||||||
|
from .dom import DOMNode
|
||||||
from .driver import Driver
|
from .driver import Driver
|
||||||
from .file_monitor import FileMonitor
|
from .file_monitor import FileMonitor
|
||||||
|
from .geometry import Offset, Region, Size
|
||||||
from .layouts.dock import DockLayout, Dock
|
from .layouts.dock import DockLayout, Dock
|
||||||
from ._linux_driver import LinuxDriver
|
|
||||||
from ._types import MessageTarget
|
|
||||||
from . import messages
|
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
from ._profile import timer
|
from .reactive import Reactive
|
||||||
from .view import View
|
from .view import View
|
||||||
from .views import DockView
|
from .views import DockView
|
||||||
from .widget import Widget, Reactive
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
# asyncio will warn against resources not being cleared
|
# asyncio will warn against resources not being cleared
|
||||||
warnings.simplefilter("always", ResourceWarning)
|
warnings.simplefilter("always", ResourceWarning)
|
||||||
|
|
||||||
|
|
||||||
LayoutDefinition = "dict[str, Any]"
|
LayoutDefinition = "dict[str, Any]"
|
||||||
|
|
||||||
ViewType = TypeVar("ViewType", bound=View)
|
ViewType = TypeVar("ViewType", bound=View)
|
||||||
@@ -96,7 +90,6 @@ class App(DOMNode):
|
|||||||
self._screen = screen
|
self._screen = screen
|
||||||
self.driver_class = driver_class or LinuxDriver
|
self.driver_class = driver_class or LinuxDriver
|
||||||
self._title = title
|
self._title = title
|
||||||
self._layout = DockLayout()
|
|
||||||
self._view_stack: list[View] = []
|
self._view_stack: list[View] = []
|
||||||
|
|
||||||
self.focused: Widget | None = None
|
self.focused: Widget | None = None
|
||||||
@@ -638,17 +631,11 @@ class App(DOMNode):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import asyncio
|
import asyncio
|
||||||
from logging import FileHandler
|
|
||||||
|
|
||||||
from rich.panel import Panel
|
|
||||||
|
|
||||||
from .widgets import Header
|
from .widgets import Header
|
||||||
from .widgets import Footer
|
from .widgets import Footer
|
||||||
|
|
||||||
from .widgets import Placeholder
|
from .widgets import Placeholder
|
||||||
from .scrollbar import ScrollBar
|
|
||||||
|
|
||||||
from rich.markdown import Markdown
|
|
||||||
|
|
||||||
# from .widgets.scroll_view import ScrollView
|
# from .widgets.scroll_view import ScrollView
|
||||||
|
|
||||||
@@ -672,7 +659,6 @@ if __name__ == "__main__":
|
|||||||
self.show_bar = not self.show_bar
|
self.show_bar = not self.show_bar
|
||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
|
|
||||||
view = await self.push_view(DockView())
|
view = await self.push_view(DockView())
|
||||||
|
|
||||||
header = Header()
|
header = Header()
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import rich.repr
|
|||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from ._error_tools import friendly_list
|
||||||
|
from .constants import NULL_SPACING
|
||||||
|
from .errors import StyleTypeError, StyleValueError
|
||||||
from .scalar import (
|
from .scalar import (
|
||||||
get_symbols,
|
get_symbols,
|
||||||
UNIT_SYMBOL,
|
UNIT_SYMBOL,
|
||||||
@@ -23,15 +26,14 @@ from .scalar import (
|
|||||||
ScalarOffset,
|
ScalarOffset,
|
||||||
ScalarParseError,
|
ScalarParseError,
|
||||||
)
|
)
|
||||||
from ..geometry import Spacing, SpacingDimensions
|
|
||||||
from .constants import NULL_SPACING
|
|
||||||
from .errors import StyleTypeError, StyleValueError
|
|
||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
from ._error_tools import friendly_list
|
from ..geometry import Spacing, SpacingDimensions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from ..layout import Layout
|
||||||
from .styles import Styles
|
from .styles import Styles
|
||||||
from .styles import DockGroup
|
from .styles import DockGroup
|
||||||
|
from ..layouts.factory import LayoutName
|
||||||
|
|
||||||
|
|
||||||
class ScalarProperty:
|
class ScalarProperty:
|
||||||
@@ -80,7 +82,6 @@ class ScalarProperty:
|
|||||||
|
|
||||||
|
|
||||||
class BoxProperty:
|
class BoxProperty:
|
||||||
|
|
||||||
DEFAULT = ("", Style())
|
DEFAULT = ("", Style())
|
||||||
|
|
||||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||||
@@ -212,7 +213,6 @@ class BorderProperty:
|
|||||||
|
|
||||||
|
|
||||||
class StyleProperty:
|
class StyleProperty:
|
||||||
|
|
||||||
DEFAULT_STYLE = Style()
|
DEFAULT_STYLE = Style()
|
||||||
|
|
||||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||||
@@ -257,7 +257,7 @@ class SpacingProperty:
|
|||||||
return getattr(obj, self._internal_name) or NULL_SPACING
|
return getattr(obj, self._internal_name) or NULL_SPACING
|
||||||
|
|
||||||
def __set__(self, obj: Styles, spacing: SpacingDimensions) -> Spacing:
|
def __set__(self, obj: Styles, spacing: SpacingDimensions) -> Spacing:
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
spacing = Spacing.unpack(spacing)
|
spacing = Spacing.unpack(spacing)
|
||||||
setattr(obj, self._internal_name, spacing)
|
setattr(obj, self._internal_name, spacing)
|
||||||
return spacing
|
return spacing
|
||||||
@@ -272,7 +272,7 @@ class DocksProperty:
|
|||||||
def __set__(
|
def __set__(
|
||||||
self, obj: Styles, docks: Iterable[DockGroup] | None
|
self, obj: Styles, docks: Iterable[DockGroup] | None
|
||||||
) -> Iterable[DockGroup] | None:
|
) -> Iterable[DockGroup] | None:
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
if docks is None:
|
if docks is None:
|
||||||
obj._rule_docks = None
|
obj._rule_docks = None
|
||||||
else:
|
else:
|
||||||
@@ -285,11 +285,44 @@ class DockProperty:
|
|||||||
return obj._rule_dock or ""
|
return obj._rule_dock or ""
|
||||||
|
|
||||||
def __set__(self, obj: Styles, spacing: str | None) -> str | None:
|
def __set__(self, obj: Styles, spacing: str | None) -> str | None:
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
obj._rule_dock = spacing
|
obj._rule_dock = spacing
|
||||||
return spacing
|
return spacing
|
||||||
|
|
||||||
|
|
||||||
|
class LayoutProperty:
|
||||||
|
"""Descriptor for getting and setting layout."""
|
||||||
|
|
||||||
|
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||||
|
self._internal_name = f"_rule_{name}"
|
||||||
|
|
||||||
|
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Layout:
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
obj (Styles): The Styles object
|
||||||
|
objtype (type[Styles]): The Styles class
|
||||||
|
Returns:
|
||||||
|
The ``Layout`` object.
|
||||||
|
"""
|
||||||
|
return getattr(obj, self._internal_name)
|
||||||
|
|
||||||
|
def __set__(self, obj: Styles, layout: LayoutName | Layout):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
obj (Styles): The Styles object.
|
||||||
|
layout (LayoutName | Layout): The layout to use. You can supply a ``LayoutName``
|
||||||
|
(a string literal such as ``"dock"``) or a ``Layout`` object.
|
||||||
|
"""
|
||||||
|
from ..layouts.factory import get_layout, Layout # Prevents circular import
|
||||||
|
|
||||||
|
obj.refresh(layout=True)
|
||||||
|
if isinstance(layout, Layout):
|
||||||
|
new_layout = layout
|
||||||
|
else:
|
||||||
|
new_layout = get_layout(layout)
|
||||||
|
setattr(obj, self._internal_name, new_layout)
|
||||||
|
|
||||||
|
|
||||||
class OffsetProperty:
|
class OffsetProperty:
|
||||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||||
self._internal_name = f"_rule_{name}"
|
self._internal_name = f"_rule_{name}"
|
||||||
@@ -302,7 +335,7 @@ class OffsetProperty:
|
|||||||
def __set__(
|
def __set__(
|
||||||
self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset
|
self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset
|
||||||
) -> tuple[int | str, int | str] | ScalarOffset:
|
) -> tuple[int | str, int | str] | ScalarOffset:
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
if isinstance(offset, ScalarOffset):
|
if isinstance(offset, ScalarOffset):
|
||||||
setattr(obj, self._internal_name, offset)
|
setattr(obj, self._internal_name, offset)
|
||||||
return offset
|
return offset
|
||||||
@@ -370,7 +403,7 @@ class NameProperty:
|
|||||||
return getattr(obj, self._internal_name) or ""
|
return getattr(obj, self._internal_name) or ""
|
||||||
|
|
||||||
def __set__(self, obj: Styles, name: str | None) -> str | None:
|
def __set__(self, obj: Styles, name: str | None) -> str | None:
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
if not isinstance(name, str):
|
if not isinstance(name, str):
|
||||||
raise StyleTypeError(f"{self._name} must be a str")
|
raise StyleTypeError(f"{self._name} must be a str")
|
||||||
setattr(obj, self._internal_name, name)
|
setattr(obj, self._internal_name, name)
|
||||||
@@ -390,7 +423,7 @@ class NameListProperty:
|
|||||||
def __set__(
|
def __set__(
|
||||||
self, obj: Styles, names: str | tuple[str] | None = None
|
self, obj: Styles, names: str | tuple[str] | None = None
|
||||||
) -> str | tuple[str] | None:
|
) -> str | tuple[str] | None:
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
names_value: tuple[str, ...] | None = None
|
names_value: tuple[str, ...] | None = None
|
||||||
if isinstance(names, str):
|
if isinstance(names, str):
|
||||||
names_value = tuple(name.strip().lower() for name in names.split(" "))
|
names_value = tuple(name.strip().lower() for name in names.split(" "))
|
||||||
@@ -424,7 +457,6 @@ class ColorProperty:
|
|||||||
|
|
||||||
|
|
||||||
class StyleFlagsProperty:
|
class StyleFlagsProperty:
|
||||||
|
|
||||||
_VALID_PROPERTIES = {
|
_VALID_PROPERTIES = {
|
||||||
"not",
|
"not",
|
||||||
"bold",
|
"bold",
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ import rich.repr
|
|||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from ._error_tools import friendly_list
|
||||||
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
|
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
|
||||||
from .errors import DeclarationError
|
from .errors import DeclarationError
|
||||||
from ._error_tools import friendly_list
|
|
||||||
from .._duration import _duration_as_seconds
|
|
||||||
from .._easing import EASING
|
|
||||||
from ..geometry import Spacing, SpacingDimensions
|
|
||||||
from .model import Declaration
|
from .model import Declaration
|
||||||
from .scalar import Scalar, ScalarOffset, Unit, ScalarError
|
from .scalar import Scalar, ScalarOffset, Unit, ScalarError
|
||||||
from .styles import DockGroup, Styles
|
from .styles import DockGroup, Styles
|
||||||
from .types import Edge, Display, Visibility
|
|
||||||
from .tokenize import Token
|
from .tokenize import Token
|
||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
|
from .types import Edge, Display, Visibility
|
||||||
|
from .._duration import _duration_as_seconds
|
||||||
|
from .._easing import EASING
|
||||||
|
from ..geometry import Spacing, SpacingDimensions
|
||||||
|
from ..layouts.factory import get_layout, LayoutName, MissingLayout, LAYOUT_MAP
|
||||||
|
|
||||||
|
|
||||||
class StylesBuilder:
|
class StylesBuilder:
|
||||||
@@ -59,7 +60,7 @@ class StylesBuilder:
|
|||||||
self.styles.important.add(rule_name)
|
self.styles.important.add(rule_name)
|
||||||
try:
|
try:
|
||||||
process_method(declaration.name, tokens, important)
|
process_method(declaration.name, tokens, important)
|
||||||
except DeclarationError as error:
|
except DeclarationError:
|
||||||
raise
|
raise
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.error(declaration.name, declaration.token, str(error))
|
self.error(declaration.name, declaration.token, str(error))
|
||||||
@@ -288,7 +289,16 @@ class StylesBuilder:
|
|||||||
if len(tokens) != 1:
|
if len(tokens) != 1:
|
||||||
self.error(name, tokens[0], "unexpected tokens in declaration")
|
self.error(name, tokens[0], "unexpected tokens in declaration")
|
||||||
else:
|
else:
|
||||||
self.styles._rule_layout = tokens[0].value
|
value = tokens[0].value
|
||||||
|
layout_name = cast(LayoutName, value)
|
||||||
|
try:
|
||||||
|
self.styles._rule_layout = get_layout(layout_name)
|
||||||
|
except MissingLayout:
|
||||||
|
self.error(
|
||||||
|
name,
|
||||||
|
tokens[0],
|
||||||
|
f"invalid value for layout (received {value!r}, expected {friendly_list(LAYOUT_MAP.keys())})",
|
||||||
|
)
|
||||||
|
|
||||||
def process_text(self, name: str, tokens: list[Token], important: bool) -> None:
|
def process_text(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||||
style_definition = " ".join(token.value for token in tokens)
|
style_definition = " ".join(token.value for token in tokens)
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import rich.repr
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Iterable
|
from typing import Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
from .. import log
|
|
||||||
from ..dom import DOMNode
|
|
||||||
from .styles import Styles
|
from .styles import Styles
|
||||||
from .tokenize import Token
|
from .tokenize import Token
|
||||||
from .types import Specificity3
|
from .types import Specificity3
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..dom import DOMNode
|
||||||
|
|
||||||
|
|
||||||
class SelectorType(Enum):
|
class SelectorType(Enum):
|
||||||
UNIVERSAL = 1
|
UNIVERSAL = 1
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import sys
|
|
||||||
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
from rich import print
|
|
||||||
from rich.color import Color
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
from rich.color import Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from .. import log
|
|
||||||
from .._animator import SimpleAnimation, Animation, EasingFunction
|
|
||||||
from .._types import MessageTarget
|
|
||||||
from .errors import StyleValueError
|
|
||||||
from .. import events
|
|
||||||
from ._error_tools import friendly_list
|
|
||||||
from .types import Specificity3, Specificity4
|
|
||||||
from .constants import (
|
|
||||||
VALID_DISPLAY,
|
|
||||||
VALID_VISIBILITY,
|
|
||||||
VALID_LAYOUT,
|
|
||||||
NULL_SPACING,
|
|
||||||
)
|
|
||||||
from .scalar_animation import ScalarAnimation
|
|
||||||
from ..geometry import NULL_OFFSET, Offset, Spacing
|
|
||||||
from .scalar import Scalar, ScalarOffset, Unit
|
|
||||||
from .transition import Transition
|
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
BorderProperty,
|
BorderProperty,
|
||||||
BoxProperty,
|
BoxProperty,
|
||||||
@@ -42,17 +24,24 @@ from ._style_properties import (
|
|||||||
StyleProperty,
|
StyleProperty,
|
||||||
StyleFlagsProperty,
|
StyleFlagsProperty,
|
||||||
TransitionsProperty,
|
TransitionsProperty,
|
||||||
|
LayoutProperty,
|
||||||
)
|
)
|
||||||
|
from .constants import (
|
||||||
|
VALID_DISPLAY,
|
||||||
|
VALID_VISIBILITY,
|
||||||
|
)
|
||||||
|
from .scalar import Scalar, ScalarOffset, Unit
|
||||||
|
from .scalar_animation import ScalarAnimation
|
||||||
|
from .transition import Transition
|
||||||
from .types import Display, Edge, Visibility
|
from .types import Display, Edge, Visibility
|
||||||
|
from .types import Specificity3, Specificity4
|
||||||
|
from .. import log
|
||||||
if sys.version_info >= (3, 8):
|
from .._animator import Animation, EasingFunction
|
||||||
from typing import Literal
|
from ..geometry import Spacing
|
||||||
else:
|
|
||||||
from typing_extensions import Literal
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from ..layout import Layout
|
||||||
from ..dom import DOMNode
|
from ..dom import DOMNode
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +59,7 @@ class Styles:
|
|||||||
|
|
||||||
_rule_display: Display | None = None
|
_rule_display: Display | None = None
|
||||||
_rule_visibility: Visibility | None = None
|
_rule_visibility: Visibility | None = None
|
||||||
_rule_layout: str | None = None
|
_rule_layout: "Layout" | None = None
|
||||||
|
|
||||||
_rule_text_color: Color | None = None
|
_rule_text_color: Color | None = None
|
||||||
_rule_text_background: Color | None = None
|
_rule_text_background: Color | None = None
|
||||||
@@ -110,7 +99,7 @@ class Styles:
|
|||||||
|
|
||||||
display = StringProperty(VALID_DISPLAY, "block")
|
display = StringProperty(VALID_DISPLAY, "block")
|
||||||
visibility = StringProperty(VALID_VISIBILITY, "visible")
|
visibility = StringProperty(VALID_VISIBILITY, "visible")
|
||||||
layout = StringProperty(VALID_LAYOUT, "dock")
|
layout = LayoutProperty()
|
||||||
|
|
||||||
text = StyleProperty()
|
text = StyleProperty()
|
||||||
text_color = ColorProperty()
|
text_color = ColorProperty()
|
||||||
@@ -279,6 +268,7 @@ class Styles:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
setattr(styles, f"_rule_{key}", value)
|
setattr(styles, f"_rule_{key}", value)
|
||||||
|
|
||||||
if self.node is not None:
|
if self.node is not None:
|
||||||
self.node.on_style_change()
|
self.node.on_style_change()
|
||||||
|
|
||||||
@@ -429,7 +419,7 @@ if __name__ == "__main__":
|
|||||||
styles.dock = "bar"
|
styles.dock = "bar"
|
||||||
styles.layers = "foo bar"
|
styles.layers = "foo bar"
|
||||||
|
|
||||||
from rich import inspect, print
|
from rich import print
|
||||||
|
|
||||||
print(styles.text_style)
|
print(styles.text_style)
|
||||||
print(styles.text)
|
print(styles.text)
|
||||||
|
|||||||
@@ -108,18 +108,37 @@ class Stylesheet:
|
|||||||
yield selector_set.specificity
|
yield selector_set.specificity
|
||||||
|
|
||||||
def apply(self, node: DOMNode) -> None:
|
def apply(self, node: DOMNode) -> None:
|
||||||
|
"""Apply the stylesheet to a DOM node.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node (DOMNode): The ``DOMNode`` to apply the stylesheet to.
|
||||||
|
Applies the styles defined in this ``Stylesheet`` to the node.
|
||||||
|
If the same rule is defined multiple times for the node (e.g. multiple
|
||||||
|
classes modifying the same CSS property), then only the most specific
|
||||||
|
rule will be applied.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Dictionary of rule attribute names e.g. "text_background" to list of tuples.
|
||||||
|
# The tuples contain the rule specificity, and the value for that rule.
|
||||||
|
# We can use this to determine, for a given rule, whether we should apply it
|
||||||
|
# or not by examining the specificity. If we have two rules for the
|
||||||
|
# same attribute, then we can choose the most specific rule and use that.
|
||||||
rule_attributes: dict[str, list[tuple[Specificity4, object]]]
|
rule_attributes: dict[str, list[tuple[Specificity4, object]]]
|
||||||
rule_attributes = defaultdict(list)
|
rule_attributes = defaultdict(list)
|
||||||
|
|
||||||
_check_rule = self._check_rule
|
_check_rule = self._check_rule
|
||||||
|
|
||||||
|
# TODO: The line below breaks inline styles and animations
|
||||||
node.styles.reset()
|
node.styles.reset()
|
||||||
|
|
||||||
# Get the default node CSS rules
|
# Collect default node CSS rules
|
||||||
for key, default_specificity, value in node._default_rules:
|
for key, default_specificity, value in node._default_rules:
|
||||||
rule_attributes[key].append((default_specificity, value))
|
rule_attributes[key].append((default_specificity, value))
|
||||||
|
|
||||||
# Apply styles on top of the default node CSS rules
|
# Collect the rules defined in the stylesheet
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
for specificity in _check_rule(rule, node):
|
for specificity in _check_rule(rule, node):
|
||||||
for key, rule_specificity, value in rule.styles.extract_rules(
|
for key, rule_specificity, value in rule.styles.extract_rules(
|
||||||
@@ -127,6 +146,7 @@ class Stylesheet:
|
|||||||
):
|
):
|
||||||
rule_attributes[key].append((rule_specificity, value))
|
rule_attributes[key].append((rule_specificity, value))
|
||||||
|
|
||||||
|
# For each rule declared for this node, keep only the most specific one
|
||||||
get_first_item = itemgetter(0)
|
get_first_item = itemgetter(0)
|
||||||
node_rules = [
|
node_rules = [
|
||||||
(name, max(specificity_rules, key=get_first_item)[1])
|
(name, max(specificity_rules, key=get_first_item)[1])
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, cast, Iterable, Iterator, TYPE_CHECKING
|
from typing import Iterable, Iterator, TYPE_CHECKING
|
||||||
|
|
||||||
from rich.highlighter import ReprHighlighter
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
from rich.highlighter import ReprHighlighter
|
||||||
from rich.pretty import Pretty
|
from rich.pretty import Pretty
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
|
from ._node_list import NodeList
|
||||||
from .css._error_tools import friendly_list
|
from .css._error_tools import friendly_list
|
||||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||||
from .css.errors import StyleValueError
|
from .css.errors import StyleValueError
|
||||||
from .css.styles import Styles
|
from .css.styles import Styles
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
from ._node_list import NodeList
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .css.query import DOMQuery
|
from .css.query import DOMQuery
|
||||||
from .widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class NoParent(Exception):
|
class NoParent(Exception):
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ from collections import defaultdict
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
||||||
|
|
||||||
|
|
||||||
from .. import log
|
|
||||||
from ..dom import DOMNode
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
|
from ..css.types import Edge
|
||||||
from ..geometry import Offset, Region, Size
|
from ..geometry import Offset, Region, Size
|
||||||
from ..layout import Layout, WidgetPlacement
|
from ..layout import Layout, WidgetPlacement
|
||||||
from ..layout_map import LayoutMap
|
|
||||||
from ..css.types import Edge
|
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
@@ -20,12 +16,9 @@ if sys.version_info >= (3, 8):
|
|||||||
else:
|
else:
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..widget import Widget
|
|
||||||
from ..view import View
|
from ..view import View
|
||||||
|
|
||||||
|
|
||||||
DockEdge = Literal["top", "right", "bottom", "left"]
|
DockEdge = Literal["top", "right", "bottom", "left"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
from __future__ import annotations
|
import sys
|
||||||
|
|
||||||
from ..layout import Layout
|
from ..layout import Layout
|
||||||
from .dock import DockLayout
|
from ..layouts.dock import DockLayout
|
||||||
from .grid import GridLayout
|
from ..layouts.grid import GridLayout
|
||||||
from .vertical import VerticalLayout
|
from ..layouts.vertical import VerticalLayout
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
from typing import Literal
|
||||||
|
else:
|
||||||
|
from typing_extensions import Literal
|
||||||
|
|
||||||
|
LayoutName = Literal["dock", "grid", "vertical"]
|
||||||
|
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
|
||||||
|
|
||||||
|
|
||||||
class MissingLayout(Exception):
|
class MissingLayout(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
|
def get_layout(name: LayoutName) -> Layout:
|
||||||
|
|
||||||
|
|
||||||
def get_layout(name: str) -> Layout:
|
|
||||||
"""Get a named layout object.
|
"""Get a named layout object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -25,7 +30,8 @@ def get_layout(name: str) -> Layout:
|
|||||||
Returns:
|
Returns:
|
||||||
Layout: A layout object.
|
Layout: A layout object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
layout_class = LAYOUT_MAP.get(name)
|
layout_class = LAYOUT_MAP.get(name)
|
||||||
if layout_class is None:
|
if layout_class is None:
|
||||||
raise MissingLayout("no layout called {name!r}")
|
raise MissingLayout(f"no layout called {name!r}, valid layouts")
|
||||||
return layout_class()
|
return layout_class()
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from operator import itemgetter
|
|
||||||
from logging import getLogger
|
|
||||||
from itertools import cycle, product
|
from itertools import cycle, product
|
||||||
import sys
|
from logging import getLogger
|
||||||
|
from operator import itemgetter
|
||||||
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
from ..geometry import Size, Offset, Region
|
from ..geometry import Size, Offset, Region
|
||||||
from ..layout import Layout, WidgetPlacement
|
from ..layout import Layout, WidgetPlacement
|
||||||
from ..widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from typing import Iterable, TYPE_CHECKING
|
|||||||
|
|
||||||
from ..geometry import Offset, Region, Size, Spacing, SpacingDimensions
|
from ..geometry import Offset, Region, Size, Spacing, SpacingDimensions
|
||||||
from ..layout import Layout, WidgetPlacement
|
from ..layout import Layout, WidgetPlacement
|
||||||
from ..widget import Widget
|
|
||||||
from .._loop import loop_last
|
from .._loop import loop_last
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
from rich.console import ConsoleOptions, RenderResult, RenderableType
|
||||||
from rich.segment import Segment, Segments
|
from rich.segment import Segment
|
||||||
from rich.style import Style, StyleType
|
from rich.style import Style, StyleType
|
||||||
|
|
||||||
|
from textual.reactive import Reactive
|
||||||
from . import events
|
from . import events
|
||||||
from .geometry import Offset
|
|
||||||
from ._types import MessageTarget
|
from ._types import MessageTarget
|
||||||
|
from .geometry import Offset
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .widget import Reactive, Widget
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
|
|||||||
@@ -6,43 +6,21 @@ import rich.repr
|
|||||||
from rich.console import RenderableType
|
from rich.console import RenderableType
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from . import errors
|
from . import errors, events, messages
|
||||||
from . import events
|
|
||||||
from . import messages
|
|
||||||
from .geometry import Size, Offset, Region
|
from .geometry import Size, Offset, Region
|
||||||
from .layout import Layout, NoWidget, WidgetPlacement
|
from .layout import Layout, NoWidget, WidgetPlacement
|
||||||
from .layouts.factory import get_layout
|
|
||||||
from .reactive import Reactive, watch
|
from .reactive import Reactive, watch
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
class LayoutProperty:
|
|
||||||
def __get__(self, obj: View, objtype: type[View] | None = None) -> str:
|
|
||||||
return obj._layout.name
|
|
||||||
|
|
||||||
def __set__(self, obj: View, layout: str | Layout) -> str:
|
|
||||||
if isinstance(layout, str):
|
|
||||||
new_layout = get_layout(layout)
|
|
||||||
else:
|
|
||||||
new_layout = layout
|
|
||||||
self._layout = new_layout
|
|
||||||
return self._layout.name
|
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class View(Widget):
|
class View(Widget):
|
||||||
|
|
||||||
STYLES = """
|
STYLES = """
|
||||||
|
layout: dock;
|
||||||
docks: main=top;
|
docks: main=top;
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||||
|
|
||||||
from .layouts.dock import DockLayout
|
|
||||||
|
|
||||||
self._layout: Layout = DockLayout()
|
|
||||||
|
|
||||||
self.mouse_over: Widget | None = None
|
self.mouse_over: Widget | None = None
|
||||||
self.widgets: set[Widget] = set()
|
self.widgets: set[Widget] = set()
|
||||||
self._mouse_style: Style = Style()
|
self._mouse_style: Style = Style()
|
||||||
@@ -62,17 +40,34 @@ class View(Widget):
|
|||||||
cls.layout_factory = layout
|
cls.layout_factory = layout
|
||||||
super().__init_subclass__(**kwargs)
|
super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
layout = LayoutProperty()
|
|
||||||
|
|
||||||
background: Reactive[str] = Reactive("")
|
background: Reactive[str] = Reactive("")
|
||||||
scroll_x: Reactive[int] = Reactive(0)
|
scroll_x: Reactive[int] = Reactive(0)
|
||||||
scroll_y: Reactive[int] = Reactive(0)
|
scroll_y: Reactive[int] = Reactive(0)
|
||||||
virtual_size = Reactive(Size(0, 0))
|
virtual_size = Reactive(Size(0, 0))
|
||||||
|
|
||||||
async def watch_background(self, value: str) -> None:
|
async def watch_background(self, value: str) -> None:
|
||||||
self._layout.background = value
|
self.layout.background = value
|
||||||
self.app.refresh()
|
self.app.refresh()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layout(self) -> Layout:
|
||||||
|
"""Convenience property for accessing ``view.styles.layout``.
|
||||||
|
|
||||||
|
Returns: The Layout associated with this view
|
||||||
|
"""
|
||||||
|
return self.styles.layout
|
||||||
|
|
||||||
|
@layout.setter
|
||||||
|
def layout(self, new_value: Layout) -> None:
|
||||||
|
"""Convenience property setter for setting ``view.styles.layout``.
|
||||||
|
Args:
|
||||||
|
new_value:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
self.styles.layout = new_value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scroll(self) -> Offset:
|
def scroll(self) -> Offset:
|
||||||
return Offset(self.scroll_x, self.scroll_y)
|
return Offset(self.scroll_x, self.scroll_y)
|
||||||
@@ -98,10 +93,10 @@ class View(Widget):
|
|||||||
return self.app.is_mounted(widget)
|
return self.app.is_mounted(widget)
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return self._layout
|
return self.layout
|
||||||
|
|
||||||
def get_offset(self, widget: Widget) -> Offset:
|
def get_offset(self, widget: Widget) -> Offset:
|
||||||
return self._layout.get_offset(widget)
|
return self.layout.get_offset(widget)
|
||||||
|
|
||||||
def get_arrangement(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
def get_arrangement(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
||||||
cached_size, cached_scroll, arrangement = self._cached_arrangement
|
cached_size, cached_scroll, arrangement = self._cached_arrangement
|
||||||
@@ -122,7 +117,7 @@ class View(Widget):
|
|||||||
widget = message.widget
|
widget = message.widget
|
||||||
assert isinstance(widget, Widget)
|
assert isinstance(widget, Widget)
|
||||||
|
|
||||||
display_update = self._layout.update_widget(self.console, widget)
|
display_update = self.layout.update_widget(self.console, widget)
|
||||||
if display_update is not None:
|
if display_update is not None:
|
||||||
self.app.display(display_update)
|
self.app.display(display_update)
|
||||||
|
|
||||||
@@ -139,7 +134,7 @@ class View(Widget):
|
|||||||
async def refresh_layout(self) -> None:
|
async def refresh_layout(self) -> None:
|
||||||
self._cached_arrangement = (Size(), Offset(), [])
|
self._cached_arrangement = (Size(), Offset(), [])
|
||||||
try:
|
try:
|
||||||
await self._layout.mount_all(self)
|
await self.layout.mount_all(self)
|
||||||
if not self.is_root_view:
|
if not self.is_root_view:
|
||||||
await self.app.view.refresh_layout()
|
await self.app.view.refresh_layout()
|
||||||
return
|
return
|
||||||
@@ -147,8 +142,8 @@ class View(Widget):
|
|||||||
if not self.size:
|
if not self.size:
|
||||||
return
|
return
|
||||||
|
|
||||||
hidden, shown, resized = self._layout.reflow(self, Size(*self.console.size))
|
hidden, shown, resized = self.layout.reflow(self, Size(*self.console.size))
|
||||||
assert self._layout.map is not None
|
assert self.layout.map is not None
|
||||||
|
|
||||||
for widget in hidden:
|
for widget in hidden:
|
||||||
widget.post_message_no_wait(events.Hide(self))
|
widget.post_message_no_wait(events.Hide(self))
|
||||||
@@ -158,7 +153,7 @@ class View(Widget):
|
|||||||
send_resize = shown
|
send_resize = shown
|
||||||
send_resize.update(resized)
|
send_resize.update(resized)
|
||||||
|
|
||||||
for widget, region, unclipped_region in self._layout:
|
for widget, region, unclipped_region in self.layout:
|
||||||
widget._update_size(unclipped_region.size)
|
widget._update_size(unclipped_region.size)
|
||||||
if widget in send_resize:
|
if widget in send_resize:
|
||||||
widget.post_message_no_wait(
|
widget.post_message_no_wait(
|
||||||
@@ -175,13 +170,13 @@ class View(Widget):
|
|||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||||
return self._layout.get_widget_at(x, y)
|
return self.layout.get_widget_at(x, y)
|
||||||
|
|
||||||
def get_style_at(self, x: int, y: int) -> Style:
|
def get_style_at(self, x: int, y: int) -> Style:
|
||||||
return self._layout.get_style_at(x, y)
|
return self.layout.get_style_at(x, y)
|
||||||
|
|
||||||
def get_widget_region(self, widget: Widget) -> Region:
|
def get_widget_region(self, widget: Widget) -> Region:
|
||||||
return self._layout.get_widget_region(widget)
|
return self.layout.get_widget_region(widget)
|
||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
async def watch_background(value: str) -> None:
|
async def watch_background(value: str) -> None:
|
||||||
@@ -190,8 +185,8 @@ class View(Widget):
|
|||||||
watch(self.app, "background", watch_background)
|
watch(self.app, "background", watch_background)
|
||||||
|
|
||||||
async def on_idle(self, event: events.Idle) -> None:
|
async def on_idle(self, event: events.Idle) -> None:
|
||||||
if self._layout.check_update():
|
if self.layout.check_update():
|
||||||
self._layout.reset_update()
|
self.layout.reset_update()
|
||||||
await self.refresh_layout()
|
await self.refresh_layout()
|
||||||
|
|
||||||
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ class DockView(View):
|
|||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
dock = Dock(edge, widgets, z)
|
dock = Dock(edge, widgets, z)
|
||||||
assert isinstance(self._layout, DockLayout)
|
assert isinstance(self.layout, DockLayout)
|
||||||
self._layout.docks.append(dock)
|
self.layout.docks.append(dock)
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
if id is not None:
|
if id is not None:
|
||||||
widget._id = id
|
widget._id = id
|
||||||
@@ -60,8 +60,8 @@ class DockView(View):
|
|||||||
grid = GridLayout(gap=gap, gutter=gutter, align=align)
|
grid = GridLayout(gap=gap, gutter=gutter, align=align)
|
||||||
view = View(layout=grid, id=id, name=name)
|
view = View(layout=grid, id=id, name=name)
|
||||||
dock = Dock(edge, (view,), z)
|
dock = Dock(edge, (view,), z)
|
||||||
assert isinstance(self._layout, DockLayout)
|
assert isinstance(self.layout, DockLayout)
|
||||||
self._layout.docks.append(dock)
|
self.layout.docks.append(dock)
|
||||||
if size is not do_not_set:
|
if size is not do_not_set:
|
||||||
view.layout_size = cast(Optional[int], size)
|
view.layout_size = cast(Optional[int], size)
|
||||||
if name is None:
|
if name is None:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class WindowView(View, layout=VerticalLayout):
|
|||||||
super().__init__(name=name, layout=layout)
|
super().__init__(name=name, layout=layout)
|
||||||
|
|
||||||
async def update(self, widget: Widget | RenderableType) -> None:
|
async def update(self, widget: Widget | RenderableType) -> None:
|
||||||
layout = self._layout
|
layout = self.layout
|
||||||
assert isinstance(layout, VerticalLayout)
|
assert isinstance(layout, VerticalLayout)
|
||||||
layout.clear()
|
layout.clear()
|
||||||
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
||||||
@@ -46,7 +46,7 @@ class WindowView(View, layout=VerticalLayout):
|
|||||||
await self.emit(WindowChange(self))
|
await self.emit(WindowChange(self))
|
||||||
|
|
||||||
async def handle_layout(self, message: messages.Layout) -> None:
|
async def handle_layout(self, message: messages.Layout) -> None:
|
||||||
self._layout.require_update()
|
self.layout.require_update()
|
||||||
message.stop()
|
message.stop()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ class WindowView(View, layout=VerticalLayout):
|
|||||||
await self.emit(WindowChange(self))
|
await self.emit(WindowChange(self))
|
||||||
|
|
||||||
async def watch_scroll_x(self, value: int) -> None:
|
async def watch_scroll_x(self, value: int) -> None:
|
||||||
self._layout.require_update()
|
self.layout.require_update()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
async def watch_scroll_y(self, value: int) -> None:
|
async def watch_scroll_y(self, value: int) -> None:
|
||||||
self._layout.require_update()
|
self.layout.require_update()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
async def on_resize(self, event: events.Resize) -> None:
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import rich.repr
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from .. import events
|
from .. import events
|
||||||
from ..widget import Reactive, Widget
|
from ..reactive import Reactive
|
||||||
|
from ..widget import Widget
|
||||||
|
|
||||||
log = getLogger("rich")
|
log = getLogger("rich")
|
||||||
|
|
||||||
|
|||||||
@@ -93,13 +93,13 @@ class ScrollView(View):
|
|||||||
await self.window.update(renderable)
|
await self.window.update(renderable)
|
||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
assert isinstance(self._layout, GridLayout)
|
assert isinstance(self.layout, GridLayout)
|
||||||
self._layout.place(
|
self.layout.place(
|
||||||
content=self.window,
|
content=self.window,
|
||||||
vscroll=self.vscroll,
|
vscroll=self.vscroll,
|
||||||
hscroll=self.hscroll,
|
hscroll=self.hscroll,
|
||||||
)
|
)
|
||||||
await self._layout.mount_all(self)
|
await self.layout.mount_all(self)
|
||||||
|
|
||||||
def home(self) -> None:
|
def home(self) -> None:
|
||||||
self.x = self.y = 0
|
self.x = self.y = 0
|
||||||
@@ -215,10 +215,10 @@ class ScrollView(View):
|
|||||||
self.vscroll.virtual_size = virtual_height
|
self.vscroll.virtual_size = virtual_height
|
||||||
self.vscroll.window_size = height
|
self.vscroll.window_size = height
|
||||||
|
|
||||||
assert isinstance(self._layout, GridLayout)
|
assert isinstance(self.layout, GridLayout)
|
||||||
|
|
||||||
vscroll_change = self._layout.show_column("vscroll", virtual_height > height)
|
vscroll_change = self.layout.show_column("vscroll", virtual_height > height)
|
||||||
hscroll_change = self._layout.show_row("hscroll", virtual_width > width)
|
hscroll_change = self.layout.show_row("hscroll", virtual_width > width)
|
||||||
if hscroll_change or vscroll_change:
|
if hscroll_change or vscroll_change:
|
||||||
self.refresh(layout=True)
|
self.refresh(layout=True)
|
||||||
|
|
||||||
|
|||||||
14
tests/layouts/test_factory.py
Normal file
14
tests/layouts/test_factory.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from textual.layouts.dock import DockLayout
|
||||||
|
from textual.layouts.factory import get_layout, MissingLayout
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_layout_valid_layout():
|
||||||
|
layout = get_layout("dock")
|
||||||
|
assert type(layout) is DockLayout
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_layout_invalid_layout():
|
||||||
|
with pytest.raises(MissingLayout):
|
||||||
|
get_layout("invalid")
|
||||||
@@ -4,6 +4,27 @@ from rich.color import Color, ColorType
|
|||||||
from textual.css.scalar import Scalar, Unit
|
from textual.css.scalar import Scalar, Unit
|
||||||
from textual.css.stylesheet import Stylesheet, StylesheetParseError
|
from textual.css.stylesheet import Stylesheet, StylesheetParseError
|
||||||
from textual.css.transition import Transition
|
from textual.css.transition import Transition
|
||||||
|
from textual.layouts.dock import DockLayout
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseLayout:
|
||||||
|
def test_valid_layout_name(self):
|
||||||
|
css = "#some-widget { layout: dock; }"
|
||||||
|
|
||||||
|
stylesheet = Stylesheet()
|
||||||
|
stylesheet.parse(css)
|
||||||
|
|
||||||
|
styles = stylesheet.rules[0].styles
|
||||||
|
assert isinstance(styles.layout, DockLayout)
|
||||||
|
|
||||||
|
def test_invalid_layout_name(self):
|
||||||
|
css = "#some-widget { layout: invalidlayout; }"
|
||||||
|
|
||||||
|
stylesheet = Stylesheet()
|
||||||
|
with pytest.raises(StylesheetParseError) as ex:
|
||||||
|
stylesheet.parse(css)
|
||||||
|
|
||||||
|
assert ex.value.errors is not None
|
||||||
|
|
||||||
|
|
||||||
class TestParseText:
|
class TestParseText:
|
||||||
|
|||||||
18
tests/test_view.py
Normal file
18
tests/test_view.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from textual.layouts.dock import DockLayout
|
||||||
|
from textual.layouts.grid import GridLayout
|
||||||
|
from textual.layouts.vertical import VerticalLayout
|
||||||
|
from textual.view import View
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("layout_name, layout_type", [
|
||||||
|
["dock", DockLayout],
|
||||||
|
["grid", GridLayout],
|
||||||
|
["vertical", VerticalLayout],
|
||||||
|
])
|
||||||
|
def test_view_layout_get_and_set(layout_name, layout_type):
|
||||||
|
view = View()
|
||||||
|
view.layout = layout_name
|
||||||
|
assert type(view.layout) is layout_type
|
||||||
|
assert view.styles.layout is view.layout
|
||||||
Reference in New Issue
Block a user