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
|
||||
import os
|
||||
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
from typing import Any, Callable, ClassVar, Iterable, Type, TypeVar
|
||||
import warnings
|
||||
|
||||
from rich.control import Control
|
||||
from typing import Any, Callable, Iterable, Type, TypeVar
|
||||
|
||||
import rich.repr
|
||||
from rich.screen import Screen
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.control import Control
|
||||
from rich.measure import Measurement
|
||||
|
||||
from rich.screen import Screen
|
||||
from rich.traceback import Traceback
|
||||
|
||||
|
||||
from . import events
|
||||
from . import actions
|
||||
from .dom import DOMNode
|
||||
from ._animator import Animator
|
||||
from .binding import Bindings, NoBinding
|
||||
from .geometry import Offset, Region, Size
|
||||
from . import events
|
||||
from . import log
|
||||
from . import messages
|
||||
from ._animator import Animator
|
||||
from ._callback import invoke
|
||||
from ._context import active_app
|
||||
from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError
|
||||
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 .file_monitor import FileMonitor
|
||||
|
||||
from .geometry import Offset, Region, Size
|
||||
from .layouts.dock import DockLayout, Dock
|
||||
from ._linux_driver import LinuxDriver
|
||||
from ._types import MessageTarget
|
||||
from . import messages
|
||||
from .message_pump import MessagePump
|
||||
from ._profile import timer
|
||||
from .reactive import Reactive
|
||||
from .view import View
|
||||
from .views import DockView
|
||||
from .widget import Widget, Reactive
|
||||
|
||||
from .widget import Widget
|
||||
|
||||
# asyncio will warn against resources not being cleared
|
||||
warnings.simplefilter("always", ResourceWarning)
|
||||
|
||||
|
||||
LayoutDefinition = "dict[str, Any]"
|
||||
|
||||
ViewType = TypeVar("ViewType", bound=View)
|
||||
@@ -96,7 +90,6 @@ class App(DOMNode):
|
||||
self._screen = screen
|
||||
self.driver_class = driver_class or LinuxDriver
|
||||
self._title = title
|
||||
self._layout = DockLayout()
|
||||
self._view_stack: list[View] = []
|
||||
|
||||
self.focused: Widget | None = None
|
||||
@@ -638,17 +631,11 @@ class App(DOMNode):
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
from logging import FileHandler
|
||||
|
||||
from rich.panel import Panel
|
||||
|
||||
from .widgets import Header
|
||||
from .widgets import Footer
|
||||
|
||||
from .widgets import Placeholder
|
||||
from .scrollbar import ScrollBar
|
||||
|
||||
from rich.markdown import Markdown
|
||||
|
||||
# from .widgets.scroll_view import ScrollView
|
||||
|
||||
@@ -672,7 +659,6 @@ if __name__ == "__main__":
|
||||
self.show_bar = not self.show_bar
|
||||
|
||||
async def on_mount(self, event: events.Mount) -> None:
|
||||
|
||||
view = await self.push_view(DockView())
|
||||
|
||||
header = Header()
|
||||
|
||||
@@ -15,6 +15,9 @@ import rich.repr
|
||||
from rich.color import Color
|
||||
from rich.style import Style
|
||||
|
||||
from ._error_tools import friendly_list
|
||||
from .constants import NULL_SPACING
|
||||
from .errors import StyleTypeError, StyleValueError
|
||||
from .scalar import (
|
||||
get_symbols,
|
||||
UNIT_SYMBOL,
|
||||
@@ -23,15 +26,14 @@ from .scalar import (
|
||||
ScalarOffset,
|
||||
ScalarParseError,
|
||||
)
|
||||
from ..geometry import Spacing, SpacingDimensions
|
||||
from .constants import NULL_SPACING
|
||||
from .errors import StyleTypeError, StyleValueError
|
||||
from .transition import Transition
|
||||
from ._error_tools import friendly_list
|
||||
from ..geometry import Spacing, SpacingDimensions
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..layout import Layout
|
||||
from .styles import Styles
|
||||
from .styles import DockGroup
|
||||
from ..layouts.factory import LayoutName
|
||||
|
||||
|
||||
class ScalarProperty:
|
||||
@@ -80,7 +82,6 @@ class ScalarProperty:
|
||||
|
||||
|
||||
class BoxProperty:
|
||||
|
||||
DEFAULT = ("", Style())
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
@@ -212,7 +213,6 @@ class BorderProperty:
|
||||
|
||||
|
||||
class StyleProperty:
|
||||
|
||||
DEFAULT_STYLE = Style()
|
||||
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
@@ -257,7 +257,7 @@ class SpacingProperty:
|
||||
return getattr(obj, self._internal_name) or NULL_SPACING
|
||||
|
||||
def __set__(self, obj: Styles, spacing: SpacingDimensions) -> Spacing:
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
spacing = Spacing.unpack(spacing)
|
||||
setattr(obj, self._internal_name, spacing)
|
||||
return spacing
|
||||
@@ -272,7 +272,7 @@ class DocksProperty:
|
||||
def __set__(
|
||||
self, obj: Styles, docks: Iterable[DockGroup] | None
|
||||
) -> Iterable[DockGroup] | None:
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
if docks is None:
|
||||
obj._rule_docks = None
|
||||
else:
|
||||
@@ -285,11 +285,44 @@ class DockProperty:
|
||||
return obj._rule_dock or ""
|
||||
|
||||
def __set__(self, obj: Styles, spacing: str | None) -> str | None:
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
obj._rule_dock = 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:
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._internal_name = f"_rule_{name}"
|
||||
@@ -302,7 +335,7 @@ class OffsetProperty:
|
||||
def __set__(
|
||||
self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset
|
||||
) -> tuple[int | str, int | str] | ScalarOffset:
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
if isinstance(offset, ScalarOffset):
|
||||
setattr(obj, self._internal_name, offset)
|
||||
return offset
|
||||
@@ -370,7 +403,7 @@ class NameProperty:
|
||||
return getattr(obj, self._internal_name) or ""
|
||||
|
||||
def __set__(self, obj: Styles, name: str | None) -> str | None:
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
if not isinstance(name, str):
|
||||
raise StyleTypeError(f"{self._name} must be a str")
|
||||
setattr(obj, self._internal_name, name)
|
||||
@@ -390,7 +423,7 @@ class NameListProperty:
|
||||
def __set__(
|
||||
self, obj: Styles, names: str | tuple[str] | None = None
|
||||
) -> str | tuple[str] | None:
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
names_value: tuple[str, ...] | None = None
|
||||
if isinstance(names, str):
|
||||
names_value = tuple(name.strip().lower() for name in names.split(" "))
|
||||
@@ -424,7 +457,6 @@ class ColorProperty:
|
||||
|
||||
|
||||
class StyleFlagsProperty:
|
||||
|
||||
_VALID_PROPERTIES = {
|
||||
"not",
|
||||
"bold",
|
||||
|
||||
@@ -6,18 +6,19 @@ import rich.repr
|
||||
from rich.color import Color
|
||||
from rich.style import Style
|
||||
|
||||
from ._error_tools import friendly_list
|
||||
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
|
||||
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 .scalar import Scalar, ScalarOffset, Unit, ScalarError
|
||||
from .styles import DockGroup, Styles
|
||||
from .types import Edge, Display, Visibility
|
||||
from .tokenize import Token
|
||||
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:
|
||||
@@ -59,7 +60,7 @@ class StylesBuilder:
|
||||
self.styles.important.add(rule_name)
|
||||
try:
|
||||
process_method(declaration.name, tokens, important)
|
||||
except DeclarationError as error:
|
||||
except DeclarationError:
|
||||
raise
|
||||
except Exception as error:
|
||||
self.error(declaration.name, declaration.token, str(error))
|
||||
@@ -288,7 +289,16 @@ class StylesBuilder:
|
||||
if len(tokens) != 1:
|
||||
self.error(name, tokens[0], "unexpected tokens in declaration")
|
||||
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:
|
||||
style_definition = " ".join(token.value for token in tokens)
|
||||
|
||||
@@ -5,14 +5,15 @@ import rich.repr
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
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 .tokenize import Token
|
||||
from .types import Specificity3
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..dom import DOMNode
|
||||
|
||||
|
||||
class SelectorType(Enum):
|
||||
UNIVERSAL = 1
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
import sys
|
||||
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
from rich import print
|
||||
from rich.color import Color
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
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 (
|
||||
BorderProperty,
|
||||
BoxProperty,
|
||||
@@ -42,17 +24,24 @@ from ._style_properties import (
|
||||
StyleProperty,
|
||||
StyleFlagsProperty,
|
||||
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
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import Literal
|
||||
else:
|
||||
from typing_extensions import Literal
|
||||
from .types import Specificity3, Specificity4
|
||||
from .. import log
|
||||
from .._animator import Animation, EasingFunction
|
||||
from ..geometry import Spacing
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..layout import Layout
|
||||
from ..dom import DOMNode
|
||||
|
||||
|
||||
@@ -70,7 +59,7 @@ class Styles:
|
||||
|
||||
_rule_display: Display | 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_background: Color | None = None
|
||||
@@ -110,7 +99,7 @@ class Styles:
|
||||
|
||||
display = StringProperty(VALID_DISPLAY, "block")
|
||||
visibility = StringProperty(VALID_VISIBILITY, "visible")
|
||||
layout = StringProperty(VALID_LAYOUT, "dock")
|
||||
layout = LayoutProperty()
|
||||
|
||||
text = StyleProperty()
|
||||
text_color = ColorProperty()
|
||||
@@ -279,6 +268,7 @@ class Styles:
|
||||
)
|
||||
else:
|
||||
setattr(styles, f"_rule_{key}", value)
|
||||
|
||||
if self.node is not None:
|
||||
self.node.on_style_change()
|
||||
|
||||
@@ -429,7 +419,7 @@ if __name__ == "__main__":
|
||||
styles.dock = "bar"
|
||||
styles.layers = "foo bar"
|
||||
|
||||
from rich import inspect, print
|
||||
from rich import print
|
||||
|
||||
print(styles.text_style)
|
||||
print(styles.text)
|
||||
|
||||
@@ -108,18 +108,37 @@ class Stylesheet:
|
||||
yield selector_set.specificity
|
||||
|
||||
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 = defaultdict(list)
|
||||
|
||||
_check_rule = self._check_rule
|
||||
|
||||
# TODO: The line below breaks inline styles and animations
|
||||
node.styles.reset()
|
||||
|
||||
# Get the default node CSS rules
|
||||
# Collect default node CSS rules
|
||||
for key, default_specificity, value in node._default_rules:
|
||||
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 specificity in _check_rule(rule, node):
|
||||
for key, rule_specificity, value in rule.styles.extract_rules(
|
||||
@@ -127,6 +146,7 @@ class Stylesheet:
|
||||
):
|
||||
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)
|
||||
node_rules = [
|
||||
(name, max(specificity_rules, key=get_first_item)[1])
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
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
|
||||
from rich.highlighter import ReprHighlighter
|
||||
from rich.pretty import Pretty
|
||||
from rich.style import Style
|
||||
from rich.tree import Tree
|
||||
|
||||
from ._node_list import NodeList
|
||||
from .css._error_tools import friendly_list
|
||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||
from .css.errors import StyleValueError
|
||||
from .css.styles import Styles
|
||||
from .message_pump import MessagePump
|
||||
from ._node_list import NodeList
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .css.query import DOMQuery
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
class NoParent(Exception):
|
||||
|
||||
@@ -5,14 +5,10 @@ from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
||||
|
||||
|
||||
from .. import log
|
||||
from ..dom import DOMNode
|
||||
from .._layout_resolve import layout_resolve
|
||||
from ..css.types import Edge
|
||||
from ..geometry import Offset, Region, Size
|
||||
from ..layout import Layout, WidgetPlacement
|
||||
from ..layout_map import LayoutMap
|
||||
from ..css.types import Edge
|
||||
from ..widget import Widget
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
@@ -20,12 +16,9 @@ if sys.version_info >= (3, 8):
|
||||
else:
|
||||
from typing_extensions import Literal
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..widget import Widget
|
||||
from ..view import View
|
||||
|
||||
|
||||
DockEdge = Literal["top", "right", "bottom", "left"]
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
|
||||
from ..layout import Layout
|
||||
from .dock import DockLayout
|
||||
from .grid import GridLayout
|
||||
from .vertical import VerticalLayout
|
||||
from ..layouts.dock import DockLayout
|
||||
from ..layouts.grid import GridLayout
|
||||
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):
|
||||
pass
|
||||
|
||||
|
||||
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
|
||||
|
||||
|
||||
def get_layout(name: str) -> Layout:
|
||||
def get_layout(name: LayoutName) -> Layout:
|
||||
"""Get a named layout object.
|
||||
|
||||
Args:
|
||||
@@ -25,7 +30,8 @@ def get_layout(name: str) -> Layout:
|
||||
Returns:
|
||||
Layout: A layout object.
|
||||
"""
|
||||
|
||||
layout_class = LAYOUT_MAP.get(name)
|
||||
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()
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from operator import itemgetter
|
||||
from logging import getLogger
|
||||
from itertools import cycle, product
|
||||
import sys
|
||||
from logging import getLogger
|
||||
from operator import itemgetter
|
||||
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
||||
|
||||
from .._layout_resolve import layout_resolve
|
||||
from ..geometry import Size, Offset, Region
|
||||
from ..layout import Layout, WidgetPlacement
|
||||
from ..widget import Widget
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..widget import Widget
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import Iterable, TYPE_CHECKING
|
||||
|
||||
from ..geometry import Offset, Region, Size, Spacing, SpacingDimensions
|
||||
from ..layout import Layout, WidgetPlacement
|
||||
from ..widget import Widget
|
||||
from .._loop import loop_last
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
import rich.repr
|
||||
from rich.color import Color
|
||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
||||
from rich.segment import Segment, Segments
|
||||
from rich.console import ConsoleOptions, RenderResult, RenderableType
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style, StyleType
|
||||
|
||||
from textual.reactive import Reactive
|
||||
from . import events
|
||||
from .geometry import Offset
|
||||
from ._types import MessageTarget
|
||||
from .geometry import Offset
|
||||
from .message import Message
|
||||
from .widget import Reactive, Widget
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
|
||||
@@ -6,43 +6,21 @@ import rich.repr
|
||||
from rich.console import RenderableType
|
||||
from rich.style import Style
|
||||
|
||||
from . import errors
|
||||
from . import events
|
||||
from . import messages
|
||||
from . import errors, events, messages
|
||||
from .geometry import Size, Offset, Region
|
||||
from .layout import Layout, NoWidget, WidgetPlacement
|
||||
from .layouts.factory import get_layout
|
||||
from .reactive import Reactive, watch
|
||||
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
|
||||
class View(Widget):
|
||||
|
||||
STYLES = """
|
||||
layout: dock;
|
||||
docks: main=top;
|
||||
|
||||
"""
|
||||
|
||||
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.widgets: set[Widget] = set()
|
||||
self._mouse_style: Style = Style()
|
||||
@@ -62,17 +40,34 @@ class View(Widget):
|
||||
cls.layout_factory = layout
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
layout = LayoutProperty()
|
||||
|
||||
background: Reactive[str] = Reactive("")
|
||||
scroll_x: Reactive[int] = Reactive(0)
|
||||
scroll_y: Reactive[int] = Reactive(0)
|
||||
virtual_size = Reactive(Size(0, 0))
|
||||
|
||||
async def watch_background(self, value: str) -> None:
|
||||
self._layout.background = value
|
||||
self.layout.background = value
|
||||
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
|
||||
def scroll(self) -> Offset:
|
||||
return Offset(self.scroll_x, self.scroll_y)
|
||||
@@ -98,10 +93,10 @@ class View(Widget):
|
||||
return self.app.is_mounted(widget)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
return self._layout
|
||||
return self.layout
|
||||
|
||||
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]:
|
||||
cached_size, cached_scroll, arrangement = self._cached_arrangement
|
||||
@@ -122,7 +117,7 @@ class View(Widget):
|
||||
widget = message.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:
|
||||
self.app.display(display_update)
|
||||
|
||||
@@ -139,7 +134,7 @@ class View(Widget):
|
||||
async def refresh_layout(self) -> None:
|
||||
self._cached_arrangement = (Size(), Offset(), [])
|
||||
try:
|
||||
await self._layout.mount_all(self)
|
||||
await self.layout.mount_all(self)
|
||||
if not self.is_root_view:
|
||||
await self.app.view.refresh_layout()
|
||||
return
|
||||
@@ -147,8 +142,8 @@ class View(Widget):
|
||||
if not self.size:
|
||||
return
|
||||
|
||||
hidden, shown, resized = self._layout.reflow(self, Size(*self.console.size))
|
||||
assert self._layout.map is not None
|
||||
hidden, shown, resized = self.layout.reflow(self, Size(*self.console.size))
|
||||
assert self.layout.map is not None
|
||||
|
||||
for widget in hidden:
|
||||
widget.post_message_no_wait(events.Hide(self))
|
||||
@@ -158,7 +153,7 @@ class View(Widget):
|
||||
send_resize = shown
|
||||
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)
|
||||
if widget in send_resize:
|
||||
widget.post_message_no_wait(
|
||||
@@ -175,13 +170,13 @@ class View(Widget):
|
||||
event.stop()
|
||||
|
||||
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:
|
||||
return self._layout.get_style_at(x, y)
|
||||
return self.layout.get_style_at(x, y)
|
||||
|
||||
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 watch_background(value: str) -> None:
|
||||
@@ -190,8 +185,8 @@ class View(Widget):
|
||||
watch(self.app, "background", watch_background)
|
||||
|
||||
async def on_idle(self, event: events.Idle) -> None:
|
||||
if self._layout.check_update():
|
||||
self._layout.reset_update()
|
||||
if self.layout.check_update():
|
||||
self.layout.reset_update()
|
||||
await self.refresh_layout()
|
||||
|
||||
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
|
||||
@@ -29,8 +29,8 @@ class DockView(View):
|
||||
) -> None:
|
||||
|
||||
dock = Dock(edge, widgets, z)
|
||||
assert isinstance(self._layout, DockLayout)
|
||||
self._layout.docks.append(dock)
|
||||
assert isinstance(self.layout, DockLayout)
|
||||
self.layout.docks.append(dock)
|
||||
for widget in widgets:
|
||||
if id is not None:
|
||||
widget._id = id
|
||||
@@ -60,8 +60,8 @@ class DockView(View):
|
||||
grid = GridLayout(gap=gap, gutter=gutter, align=align)
|
||||
view = View(layout=grid, id=id, name=name)
|
||||
dock = Dock(edge, (view,), z)
|
||||
assert isinstance(self._layout, DockLayout)
|
||||
self._layout.docks.append(dock)
|
||||
assert isinstance(self.layout, DockLayout)
|
||||
self.layout.docks.append(dock)
|
||||
if size is not do_not_set:
|
||||
view.layout_size = cast(Optional[int], size)
|
||||
if name is None:
|
||||
|
||||
@@ -32,7 +32,7 @@ class WindowView(View, layout=VerticalLayout):
|
||||
super().__init__(name=name, layout=layout)
|
||||
|
||||
async def update(self, widget: Widget | RenderableType) -> None:
|
||||
layout = self._layout
|
||||
layout = self.layout
|
||||
assert isinstance(layout, VerticalLayout)
|
||||
layout.clear()
|
||||
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
||||
@@ -46,7 +46,7 @@ class WindowView(View, layout=VerticalLayout):
|
||||
await self.emit(WindowChange(self))
|
||||
|
||||
async def handle_layout(self, message: messages.Layout) -> None:
|
||||
self._layout.require_update()
|
||||
self.layout.require_update()
|
||||
message.stop()
|
||||
self.refresh()
|
||||
|
||||
@@ -54,11 +54,11 @@ class WindowView(View, layout=VerticalLayout):
|
||||
await self.emit(WindowChange(self))
|
||||
|
||||
async def watch_scroll_x(self, value: int) -> None:
|
||||
self._layout.require_update()
|
||||
self.layout.require_update()
|
||||
self.refresh()
|
||||
|
||||
async def watch_scroll_y(self, value: int) -> None:
|
||||
self._layout.require_update()
|
||||
self.layout.require_update()
|
||||
self.refresh()
|
||||
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
|
||||
@@ -10,7 +10,8 @@ import rich.repr
|
||||
from logging import getLogger
|
||||
|
||||
from .. import events
|
||||
from ..widget import Reactive, Widget
|
||||
from ..reactive import Reactive
|
||||
from ..widget import Widget
|
||||
|
||||
log = getLogger("rich")
|
||||
|
||||
|
||||
@@ -93,13 +93,13 @@ class ScrollView(View):
|
||||
await self.window.update(renderable)
|
||||
|
||||
async def on_mount(self, event: events.Mount) -> None:
|
||||
assert isinstance(self._layout, GridLayout)
|
||||
self._layout.place(
|
||||
assert isinstance(self.layout, GridLayout)
|
||||
self.layout.place(
|
||||
content=self.window,
|
||||
vscroll=self.vscroll,
|
||||
hscroll=self.hscroll,
|
||||
)
|
||||
await self._layout.mount_all(self)
|
||||
await self.layout.mount_all(self)
|
||||
|
||||
def home(self) -> None:
|
||||
self.x = self.y = 0
|
||||
@@ -215,10 +215,10 @@ class ScrollView(View):
|
||||
self.vscroll.virtual_size = virtual_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)
|
||||
hscroll_change = self._layout.show_row("hscroll", virtual_width > width)
|
||||
vscroll_change = self.layout.show_column("vscroll", virtual_height > height)
|
||||
hscroll_change = self.layout.show_row("hscroll", virtual_width > width)
|
||||
if hscroll_change or vscroll_change:
|
||||
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.stylesheet import Stylesheet, StylesheetParseError
|
||||
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:
|
||||
|
||||
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