mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'css' of github.com:willmcgugan/textual into style-property-docs
This commit is contained in:
49
examples/dev_sandbox.css
Normal file
49
examples/dev_sandbox.css
Normal file
@@ -0,0 +1,49 @@
|
||||
/* CSS file for dev_sandbox.py */
|
||||
|
||||
App > View {
|
||||
docks: side=left/1;
|
||||
text: on #20639b;
|
||||
}
|
||||
|
||||
Widget:hover {
|
||||
outline: heavy;
|
||||
text: bold !important;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
text: #09312e on #3caea3;
|
||||
dock: side;
|
||||
width: 30;
|
||||
offset-x: -100%;
|
||||
transition: offset 500ms in_out_cubic;
|
||||
border-right: outer #09312e;
|
||||
}
|
||||
|
||||
#sidebar.-active {
|
||||
offset-x: 0;
|
||||
}
|
||||
|
||||
#header {
|
||||
text: white on #173f5f;
|
||||
height: 3;
|
||||
border: hkey;
|
||||
}
|
||||
|
||||
#header.-visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#content {
|
||||
text: white on #20639b;
|
||||
border-bottom: hkey #0f2b41;
|
||||
offset-y: -3;
|
||||
}
|
||||
|
||||
#content.-content-visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#footer {
|
||||
text: #3a3009 on #f6d55c;
|
||||
height: 3;
|
||||
}
|
||||
32
examples/dev_sandbox.py
Normal file
32
examples/dev_sandbox.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from rich.console import RenderableType
|
||||
from rich.panel import Panel
|
||||
|
||||
from textual.app import App
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class PanelWidget(Widget):
|
||||
def render(self) -> RenderableType:
|
||||
return Panel("hello world!", title="Title")
|
||||
|
||||
|
||||
class BasicApp(App):
|
||||
"""Sandbox application used for testing/development by Textual developers"""
|
||||
|
||||
def on_load(self):
|
||||
"""Bind keys here."""
|
||||
self.bind("tab", "toggle_class('#sidebar', '-active')")
|
||||
self.bind("a", "toggle_class('#header', '-visible')")
|
||||
self.bind("c", "toggle_class('#content', '-content-visible')")
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
self.mount(
|
||||
header=Widget(),
|
||||
content=PanelWidget(),
|
||||
footer=Widget(),
|
||||
sidebar=Widget(),
|
||||
)
|
||||
|
||||
|
||||
BasicApp.run(css_file="test_app.css", watch_css=True, log="textual.log")
|
||||
@@ -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,16 +26,15 @@ 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 .._box import BoxType
|
||||
from ..layouts.factory import LayoutName
|
||||
|
||||
|
||||
class ScalarProperty:
|
||||
@@ -367,7 +369,7 @@ class SpacingProperty:
|
||||
ValueError: When the value is malformed, e.g. a ``tuple`` with a length that is
|
||||
not 1, 2, or 4.
|
||||
"""
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
spacing = Spacing.unpack(spacing)
|
||||
setattr(obj, self._internal_name, spacing)
|
||||
|
||||
@@ -398,7 +400,7 @@ class DocksProperty:
|
||||
obj (Styles): The ``Styles`` object.
|
||||
docks (Iterable[DockGroup]): Iterable of DockGroups
|
||||
"""
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
if docks is None:
|
||||
obj._rule_docks = None
|
||||
else:
|
||||
@@ -431,10 +433,43 @@ class DockProperty:
|
||||
obj (Styles): The ``Styles`` object
|
||||
spacing (str | None): The spacing to use.
|
||||
"""
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
obj._rule_dock = 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:
|
||||
"""Descriptor for getting and setting the offset property.
|
||||
Offset consists of two values, x and y, that a widget's position
|
||||
@@ -473,7 +508,7 @@ class OffsetProperty:
|
||||
ScalarParseError: If any of the string values supplied in the 2-tuple cannot
|
||||
be parsed into a Scalar. For example, if you specify an non-existent unit.
|
||||
"""
|
||||
obj.refresh(True)
|
||||
obj.refresh(layout=True)
|
||||
if isinstance(offset, ScalarOffset):
|
||||
setattr(obj, self._internal_name, offset)
|
||||
return offset
|
||||
@@ -600,7 +635,7 @@ class NameProperty:
|
||||
Raises:
|
||||
StyleTypeError: If the value is not a ``str``.
|
||||
"""
|
||||
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)
|
||||
@@ -619,7 +654,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(" "))
|
||||
|
||||
@@ -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))
|
||||
@@ -128,7 +129,7 @@ class StylesBuilder:
|
||||
append = space.append
|
||||
for token in tokens:
|
||||
(token_name, value, _, _, location) = token
|
||||
if token_name == "scalar":
|
||||
if token_name in ("number", "scalar"):
|
||||
try:
|
||||
append(int(value))
|
||||
except ValueError:
|
||||
@@ -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,11 +24,26 @@ 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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -64,7 +61,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
|
||||
@@ -104,7 +101,7 @@ class Styles:
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||
layout = StringEnumProperty(VALID_LAYOUT, "dock")
|
||||
layout = LayoutProperty()
|
||||
|
||||
text = StyleProperty()
|
||||
text_color = ColorProperty()
|
||||
@@ -273,6 +270,7 @@ class Styles:
|
||||
)
|
||||
else:
|
||||
setattr(styles, f"_rule_{key}", value)
|
||||
|
||||
if self.node is not None:
|
||||
self.node.on_style_change()
|
||||
|
||||
@@ -423,7 +421,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
|
||||
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):
|
||||
@@ -160,6 +158,22 @@ class DOMNode(MessagePump):
|
||||
f"expected {friendly_list(VALID_DISPLAY)})",
|
||||
)
|
||||
|
||||
@property
|
||||
def visible(self) -> bool:
|
||||
return self.styles.visibility != "hidden"
|
||||
|
||||
@visible.setter
|
||||
def visible(self, new_value: bool) -> None:
|
||||
if isinstance(new_value, bool):
|
||||
self.styles.visibility = "visible" if new_value else "hidden"
|
||||
elif new_value in VALID_VISIBILITY:
|
||||
self.styles.visibility = new_value
|
||||
else:
|
||||
raise StyleValueError(
|
||||
f"invalid value for visibility (received {new_value!r}, "
|
||||
f"expected {friendly_list(VALID_VISIBILITY)})"
|
||||
)
|
||||
|
||||
@property
|
||||
def z(self) -> tuple[int, ...]:
|
||||
"""Get the z index tuple for this node.
|
||||
|
||||
@@ -415,6 +415,25 @@ class Region(NamedTuple):
|
||||
)
|
||||
return new_region
|
||||
|
||||
def shrink(self, margin: Spacing) -> Region:
|
||||
"""Shrink a region by pushing each edge inwards.
|
||||
|
||||
Args:
|
||||
margin (Spacing): Defines how many cells to shrink the Region by at each edge.
|
||||
|
||||
Returns:
|
||||
Region: The new, smaller region.
|
||||
"""
|
||||
_clamp = clamp
|
||||
top, right, bottom, left = margin
|
||||
x, y, width, height = self
|
||||
return Region(
|
||||
x=_clamp(x + left, 0, width),
|
||||
y=_clamp(y + top, 0, height),
|
||||
width=_clamp(width - left - right, 0, width),
|
||||
height=_clamp(height - top - bottom, 0, height),
|
||||
)
|
||||
|
||||
def intersection(self, region: Region) -> Region:
|
||||
"""Get that covers both regions.
|
||||
|
||||
|
||||
@@ -55,6 +55,17 @@ class WidgetPlacement(NamedTuple):
|
||||
widget: Widget | None = None
|
||||
order: int = 0
|
||||
|
||||
def apply_margin(self) -> "WidgetPlacement":
|
||||
region, widget, order = self
|
||||
styles = widget.styles
|
||||
if styles.has_margin:
|
||||
return WidgetPlacement(
|
||||
region=region.shrink(styles.margin),
|
||||
widget=widget,
|
||||
order=order,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class LayoutUpdate:
|
||||
@@ -199,6 +210,15 @@ class Layout(ABC):
|
||||
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
|
||||
|
||||
def get_style_at(self, x: int, y: int) -> Style:
|
||||
"""Get the Style at the given cell or Style.null()
|
||||
|
||||
Args:
|
||||
x (int): X position within the Layout
|
||||
y (int): Y position within the Layout
|
||||
|
||||
Returns:
|
||||
Style: The Style at the cell (x, y) within the Layout
|
||||
"""
|
||||
try:
|
||||
widget, region = self.get_widget_at(x, y)
|
||||
except NoWidget:
|
||||
@@ -217,6 +237,18 @@ class Layout(ABC):
|
||||
return Style.null()
|
||||
|
||||
def get_widget_region(self, widget: Widget) -> Region:
|
||||
"""Get the Region of a Widget contained in this Layout.
|
||||
|
||||
Args:
|
||||
widget (Widget): The Widget in this layout you wish to know the Region of.
|
||||
|
||||
Raises:
|
||||
NoWidget: If the Widget is not contained in this Layout.
|
||||
|
||||
Returns:
|
||||
Region: The Region of the Widget.
|
||||
|
||||
"""
|
||||
try:
|
||||
region, *_ = self.map[widget]
|
||||
except KeyError:
|
||||
@@ -270,7 +302,7 @@ class Layout(ABC):
|
||||
|
||||
for widget, region, _order, clip in widget_regions:
|
||||
|
||||
if not widget.is_visual:
|
||||
if not (widget.is_visual and widget.visible):
|
||||
continue
|
||||
|
||||
lines = widget._get_lines()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,55 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import chain
|
||||
from typing import Callable, Iterable, ClassVar, TYPE_CHECKING
|
||||
from typing import Callable, Iterable
|
||||
|
||||
from rich.console import RenderableType
|
||||
import rich.repr
|
||||
from rich.console import RenderableType
|
||||
from rich.style import Style
|
||||
|
||||
from . import events
|
||||
from . import errors
|
||||
from . import log
|
||||
from . import messages
|
||||
from .layout import Layout, NoWidget, WidgetPlacement
|
||||
from .layouts.factory import get_layout
|
||||
from . import errors, events, messages
|
||||
from .geometry import Size, Offset, Region
|
||||
from .layout import Layout, NoWidget, WidgetPlacement
|
||||
from .reactive import Reactive, watch
|
||||
|
||||
from .widget import Widget, Widget
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App
|
||||
|
||||
|
||||
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
|
||||
from .widget import Widget
|
||||
|
||||
|
||||
@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()
|
||||
@@ -69,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)
|
||||
@@ -105,18 +93,23 @@ 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
|
||||
if cached_size == size and cached_scroll == scroll:
|
||||
return arrangement
|
||||
arrangement = list(self._layout.arrange(self, size, scroll))
|
||||
self._cached_arrangement = (size, scroll, arrangement)
|
||||
return arrangement
|
||||
|
||||
placements = [
|
||||
placement.apply_margin()
|
||||
for placement in self._layout.arrange(self, size, scroll)
|
||||
]
|
||||
|
||||
self._cached_arrangement = (size, scroll, placements)
|
||||
return placements
|
||||
|
||||
async def handle_update(self, message: messages.Update) -> None:
|
||||
if self.is_root_view:
|
||||
@@ -124,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)
|
||||
|
||||
@@ -141,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
|
||||
@@ -149,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))
|
||||
@@ -160,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(
|
||||
@@ -177,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:
|
||||
@@ -192,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:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import PercentStyle, getLogger
|
||||
from logging import getLogger
|
||||
from typing import (
|
||||
Any,
|
||||
Awaitable,
|
||||
@@ -11,35 +11,29 @@ from typing import (
|
||||
NamedTuple,
|
||||
cast,
|
||||
)
|
||||
import rich.repr
|
||||
from rich import box
|
||||
from rich.align import Align
|
||||
from rich.console import Console, RenderableType, ConsoleOptions
|
||||
from rich.measure import Measurement
|
||||
from rich.panel import Panel
|
||||
from rich.padding import Padding
|
||||
from rich.pretty import Pretty
|
||||
from rich.segment import Segment
|
||||
from rich.style import Style, StyleType
|
||||
from rich.styled import Styled
|
||||
from rich.text import Text, TextType
|
||||
|
||||
from . import events
|
||||
import rich.repr
|
||||
from rich.align import Align
|
||||
from rich.console import Console, RenderableType
|
||||
from rich.padding import Padding
|
||||
from rich.style import Style
|
||||
from rich.styled import Styled
|
||||
from rich.text import Text
|
||||
|
||||
from . import errors
|
||||
from . import events
|
||||
from ._animator import BoundAnimator
|
||||
from ._border import Border, BORDER_STYLES
|
||||
from ._border import Border
|
||||
from ._callback import invoke
|
||||
from .blank import Blank
|
||||
from .dom import DOMNode
|
||||
from ._context import active_app
|
||||
from .geometry import Size, Spacing, SpacingDimensions
|
||||
from ._types import Lines
|
||||
from .dom import DOMNode
|
||||
from .geometry import Size, Spacing
|
||||
from .message import Message
|
||||
from .messages import Layout, Update
|
||||
from .reactive import Reactive, watch
|
||||
from ._types import Lines
|
||||
from .reactive import watch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .app import App
|
||||
from .view import View
|
||||
|
||||
log = getLogger("rich")
|
||||
@@ -163,34 +157,26 @@ class Widget(DOMNode):
|
||||
styles = self.styles
|
||||
parent_text_style = self.parent.text_style
|
||||
|
||||
if styles.visibility == "hidden":
|
||||
renderable = Blank(parent_text_style)
|
||||
else:
|
||||
text_style = styles.text
|
||||
renderable_text_style = parent_text_style + text_style
|
||||
if renderable_text_style:
|
||||
renderable = Styled(renderable, renderable_text_style)
|
||||
text_style = styles.text
|
||||
renderable_text_style = parent_text_style + text_style
|
||||
if renderable_text_style:
|
||||
renderable = Styled(renderable, renderable_text_style)
|
||||
|
||||
if styles.has_padding:
|
||||
renderable = Padding(
|
||||
renderable, styles.padding, style=renderable_text_style
|
||||
)
|
||||
if styles.has_padding:
|
||||
renderable = Padding(
|
||||
renderable, styles.padding, style=renderable_text_style
|
||||
)
|
||||
|
||||
if styles.has_border:
|
||||
renderable = Border(
|
||||
renderable, styles.border, style=renderable_text_style
|
||||
)
|
||||
if styles.has_border:
|
||||
renderable = Border(renderable, styles.border, style=renderable_text_style)
|
||||
|
||||
if styles.has_margin:
|
||||
renderable = Padding(renderable, styles.margin, style=parent_text_style)
|
||||
|
||||
if styles.has_outline:
|
||||
renderable = Border(
|
||||
renderable,
|
||||
styles.outline,
|
||||
outline=True,
|
||||
style=renderable_text_style,
|
||||
)
|
||||
if styles.has_outline:
|
||||
renderable = Border(
|
||||
renderable,
|
||||
styles.outline,
|
||||
outline=True,
|
||||
style=renderable_text_style,
|
||||
)
|
||||
|
||||
return renderable
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from textual.geometry import clamp, Offset, Size, Region
|
||||
from textual.geometry import clamp, Offset, Size, Region, Spacing
|
||||
|
||||
|
||||
def test_dimensions_region():
|
||||
@@ -173,6 +173,12 @@ def test_clip():
|
||||
assert Region(10, 10, 20, 30).clip(20, 25) == Region(10, 10, 10, 15)
|
||||
|
||||
|
||||
def test_region_shrink():
|
||||
margin = Spacing(top=1, right=2, bottom=3, left=4)
|
||||
region = Region(x=10, y=10, width=50, height=50)
|
||||
assert region.shrink(margin) == Region(x=14, y=11, width=44, height=46)
|
||||
|
||||
|
||||
def test_region_intersection():
|
||||
assert Region(0, 0, 100, 50).intersection(Region(10, 10, 10, 10)) == Region(
|
||||
10, 10, 10, 10
|
||||
|
||||
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
|
||||
28
tests/test_widget.py
Normal file
28
tests/test_widget.py
Normal file
@@ -0,0 +1,28 @@
|
||||
import pytest
|
||||
|
||||
from textual.css.errors import StyleValueError
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"set_val, get_val, style_str", [
|
||||
[True, True, "visible"],
|
||||
[False, False, "hidden"],
|
||||
["hidden", False, "hidden"],
|
||||
["visible", True, "visible"],
|
||||
])
|
||||
def test_widget_set_visible_true(set_val, get_val, style_str):
|
||||
widget = Widget()
|
||||
widget.visible = set_val
|
||||
|
||||
assert widget.visible is get_val
|
||||
assert widget.styles.visibility == style_str
|
||||
|
||||
|
||||
def test_widget_set_visible_invalid_string():
|
||||
widget = Widget()
|
||||
|
||||
with pytest.raises(StyleValueError):
|
||||
widget.visible = "nope! no widget for me!"
|
||||
|
||||
assert widget.visible
|
||||
Reference in New Issue
Block a user