Merge branch 'css' into dont-render-margin

This commit is contained in:
Darren Burns
2022-01-25 16:42:34 +00:00
committed by GitHub
20 changed files with 252 additions and 170 deletions

View File

@@ -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()

View File

@@ -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",

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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])

View File

@@ -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):

View File

@@ -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"]

View File

@@ -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()

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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")

View File

@@ -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)

View 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")

View File

@@ -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
View 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