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
|
from __future__ import annotations
|
||||||
import os
|
|
||||||
|
|
||||||
|
import os
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from typing import Any, Callable, ClassVar, Iterable, Type, TypeVar
|
|
||||||
import warnings
|
import warnings
|
||||||
|
from typing import Any, Callable, Iterable, Type, TypeVar
|
||||||
from rich.control import Control
|
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.screen import Screen
|
|
||||||
from rich.console import Console, RenderableType
|
from rich.console import Console, RenderableType
|
||||||
|
from rich.control import Control
|
||||||
from rich.measure import Measurement
|
from rich.measure import Measurement
|
||||||
|
from rich.screen import Screen
|
||||||
from rich.traceback import Traceback
|
from rich.traceback import Traceback
|
||||||
|
|
||||||
|
|
||||||
from . import events
|
|
||||||
from . import actions
|
from . import actions
|
||||||
from .dom import DOMNode
|
from . import events
|
||||||
from ._animator import Animator
|
|
||||||
from .binding import Bindings, NoBinding
|
|
||||||
from .geometry import Offset, Region, Size
|
|
||||||
from . import log
|
from . import log
|
||||||
|
from . import messages
|
||||||
|
from ._animator import Animator
|
||||||
from ._callback import invoke
|
from ._callback import invoke
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError
|
|
||||||
from ._event_broker import extract_handler_actions, NoHandler
|
from ._event_broker import extract_handler_actions, NoHandler
|
||||||
|
from ._linux_driver import LinuxDriver
|
||||||
|
from ._profile import timer
|
||||||
|
from .binding import Bindings, NoBinding
|
||||||
|
from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError
|
||||||
|
from .dom import DOMNode
|
||||||
from .driver import Driver
|
from .driver import Driver
|
||||||
from .file_monitor import FileMonitor
|
from .file_monitor import FileMonitor
|
||||||
|
from .geometry import Offset, Region, Size
|
||||||
from .layouts.dock import DockLayout, Dock
|
from .layouts.dock import DockLayout, Dock
|
||||||
from ._linux_driver import LinuxDriver
|
|
||||||
from ._types import MessageTarget
|
|
||||||
from . import messages
|
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
from ._profile import timer
|
from .reactive import Reactive
|
||||||
from .view import View
|
from .view import View
|
||||||
from .views import DockView
|
from .views import DockView
|
||||||
from .widget import Widget, Reactive
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
# asyncio will warn against resources not being cleared
|
# asyncio will warn against resources not being cleared
|
||||||
warnings.simplefilter("always", ResourceWarning)
|
warnings.simplefilter("always", ResourceWarning)
|
||||||
|
|
||||||
|
|
||||||
LayoutDefinition = "dict[str, Any]"
|
LayoutDefinition = "dict[str, Any]"
|
||||||
|
|
||||||
ViewType = TypeVar("ViewType", bound=View)
|
ViewType = TypeVar("ViewType", bound=View)
|
||||||
@@ -96,7 +90,6 @@ class App(DOMNode):
|
|||||||
self._screen = screen
|
self._screen = screen
|
||||||
self.driver_class = driver_class or LinuxDriver
|
self.driver_class = driver_class or LinuxDriver
|
||||||
self._title = title
|
self._title = title
|
||||||
self._layout = DockLayout()
|
|
||||||
self._view_stack: list[View] = []
|
self._view_stack: list[View] = []
|
||||||
|
|
||||||
self.focused: Widget | None = None
|
self.focused: Widget | None = None
|
||||||
@@ -638,17 +631,11 @@ class App(DOMNode):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import asyncio
|
import asyncio
|
||||||
from logging import FileHandler
|
|
||||||
|
|
||||||
from rich.panel import Panel
|
|
||||||
|
|
||||||
from .widgets import Header
|
from .widgets import Header
|
||||||
from .widgets import Footer
|
from .widgets import Footer
|
||||||
|
|
||||||
from .widgets import Placeholder
|
from .widgets import Placeholder
|
||||||
from .scrollbar import ScrollBar
|
|
||||||
|
|
||||||
from rich.markdown import Markdown
|
|
||||||
|
|
||||||
# from .widgets.scroll_view import ScrollView
|
# from .widgets.scroll_view import ScrollView
|
||||||
|
|
||||||
@@ -672,7 +659,6 @@ if __name__ == "__main__":
|
|||||||
self.show_bar = not self.show_bar
|
self.show_bar = not self.show_bar
|
||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
|
|
||||||
view = await self.push_view(DockView())
|
view = await self.push_view(DockView())
|
||||||
|
|
||||||
header = Header()
|
header = Header()
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ import rich.repr
|
|||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from ._error_tools import friendly_list
|
||||||
|
from .constants import NULL_SPACING
|
||||||
|
from .errors import StyleTypeError, StyleValueError
|
||||||
from .scalar import (
|
from .scalar import (
|
||||||
get_symbols,
|
get_symbols,
|
||||||
UNIT_SYMBOL,
|
UNIT_SYMBOL,
|
||||||
@@ -23,16 +26,15 @@ from .scalar import (
|
|||||||
ScalarOffset,
|
ScalarOffset,
|
||||||
ScalarParseError,
|
ScalarParseError,
|
||||||
)
|
)
|
||||||
from ..geometry import Spacing, SpacingDimensions
|
|
||||||
from .constants import NULL_SPACING
|
|
||||||
from .errors import StyleTypeError, StyleValueError
|
|
||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
from ._error_tools import friendly_list
|
from ..geometry import Spacing, SpacingDimensions
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from ..layout import Layout
|
||||||
from .styles import Styles
|
from .styles import Styles
|
||||||
from .styles import DockGroup
|
from .styles import DockGroup
|
||||||
from .._box import BoxType
|
from .._box import BoxType
|
||||||
|
from ..layouts.factory import LayoutName
|
||||||
|
|
||||||
|
|
||||||
class ScalarProperty:
|
class ScalarProperty:
|
||||||
@@ -367,7 +369,7 @@ class SpacingProperty:
|
|||||||
ValueError: When the value is malformed, e.g. a ``tuple`` with a length that is
|
ValueError: When the value is malformed, e.g. a ``tuple`` with a length that is
|
||||||
not 1, 2, or 4.
|
not 1, 2, or 4.
|
||||||
"""
|
"""
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
spacing = Spacing.unpack(spacing)
|
spacing = Spacing.unpack(spacing)
|
||||||
setattr(obj, self._internal_name, spacing)
|
setattr(obj, self._internal_name, spacing)
|
||||||
|
|
||||||
@@ -398,7 +400,7 @@ class DocksProperty:
|
|||||||
obj (Styles): The ``Styles`` object.
|
obj (Styles): The ``Styles`` object.
|
||||||
docks (Iterable[DockGroup]): Iterable of DockGroups
|
docks (Iterable[DockGroup]): Iterable of DockGroups
|
||||||
"""
|
"""
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
if docks is None:
|
if docks is None:
|
||||||
obj._rule_docks = None
|
obj._rule_docks = None
|
||||||
else:
|
else:
|
||||||
@@ -431,10 +433,43 @@ class DockProperty:
|
|||||||
obj (Styles): The ``Styles`` object
|
obj (Styles): The ``Styles`` object
|
||||||
spacing (str | None): The spacing to use.
|
spacing (str | None): The spacing to use.
|
||||||
"""
|
"""
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
obj._rule_dock = spacing
|
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:
|
class OffsetProperty:
|
||||||
"""Descriptor for getting and setting the offset property.
|
"""Descriptor for getting and setting the offset property.
|
||||||
Offset consists of two values, x and y, that a widget's position
|
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
|
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.
|
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):
|
if isinstance(offset, ScalarOffset):
|
||||||
setattr(obj, self._internal_name, offset)
|
setattr(obj, self._internal_name, offset)
|
||||||
return offset
|
return offset
|
||||||
@@ -600,7 +635,7 @@ class NameProperty:
|
|||||||
Raises:
|
Raises:
|
||||||
StyleTypeError: If the value is not a ``str``.
|
StyleTypeError: If the value is not a ``str``.
|
||||||
"""
|
"""
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
if not isinstance(name, str):
|
if not isinstance(name, str):
|
||||||
raise StyleTypeError(f"{self._name} must be a str")
|
raise StyleTypeError(f"{self._name} must be a str")
|
||||||
setattr(obj, self._internal_name, name)
|
setattr(obj, self._internal_name, name)
|
||||||
@@ -619,7 +654,7 @@ class NameListProperty:
|
|||||||
def __set__(
|
def __set__(
|
||||||
self, obj: Styles, names: str | tuple[str] | None = None
|
self, obj: Styles, names: str | tuple[str] | None = None
|
||||||
) -> str | tuple[str] | None:
|
) -> str | tuple[str] | None:
|
||||||
obj.refresh(True)
|
obj.refresh(layout=True)
|
||||||
names_value: tuple[str, ...] | None = None
|
names_value: tuple[str, ...] | None = None
|
||||||
if isinstance(names, str):
|
if isinstance(names, str):
|
||||||
names_value = tuple(name.strip().lower() for name in names.split(" "))
|
names_value = tuple(name.strip().lower() for name in names.split(" "))
|
||||||
|
|||||||
@@ -6,18 +6,19 @@ import rich.repr
|
|||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from ._error_tools import friendly_list
|
||||||
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
|
from .constants import VALID_BORDER, VALID_EDGE, VALID_DISPLAY, VALID_VISIBILITY
|
||||||
from .errors import DeclarationError
|
from .errors import DeclarationError
|
||||||
from ._error_tools import friendly_list
|
|
||||||
from .._duration import _duration_as_seconds
|
|
||||||
from .._easing import EASING
|
|
||||||
from ..geometry import Spacing, SpacingDimensions
|
|
||||||
from .model import Declaration
|
from .model import Declaration
|
||||||
from .scalar import Scalar, ScalarOffset, Unit, ScalarError
|
from .scalar import Scalar, ScalarOffset, Unit, ScalarError
|
||||||
from .styles import DockGroup, Styles
|
from .styles import DockGroup, Styles
|
||||||
from .types import Edge, Display, Visibility
|
|
||||||
from .tokenize import Token
|
from .tokenize import Token
|
||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
|
from .types import Edge, Display, Visibility
|
||||||
|
from .._duration import _duration_as_seconds
|
||||||
|
from .._easing import EASING
|
||||||
|
from ..geometry import Spacing, SpacingDimensions
|
||||||
|
from ..layouts.factory import get_layout, LayoutName, MissingLayout, LAYOUT_MAP
|
||||||
|
|
||||||
|
|
||||||
class StylesBuilder:
|
class StylesBuilder:
|
||||||
@@ -59,7 +60,7 @@ class StylesBuilder:
|
|||||||
self.styles.important.add(rule_name)
|
self.styles.important.add(rule_name)
|
||||||
try:
|
try:
|
||||||
process_method(declaration.name, tokens, important)
|
process_method(declaration.name, tokens, important)
|
||||||
except DeclarationError as error:
|
except DeclarationError:
|
||||||
raise
|
raise
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.error(declaration.name, declaration.token, str(error))
|
self.error(declaration.name, declaration.token, str(error))
|
||||||
@@ -128,7 +129,7 @@ class StylesBuilder:
|
|||||||
append = space.append
|
append = space.append
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
(token_name, value, _, _, location) = token
|
(token_name, value, _, _, location) = token
|
||||||
if token_name == "scalar":
|
if token_name in ("number", "scalar"):
|
||||||
try:
|
try:
|
||||||
append(int(value))
|
append(int(value))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -288,7 +289,16 @@ class StylesBuilder:
|
|||||||
if len(tokens) != 1:
|
if len(tokens) != 1:
|
||||||
self.error(name, tokens[0], "unexpected tokens in declaration")
|
self.error(name, tokens[0], "unexpected tokens in declaration")
|
||||||
else:
|
else:
|
||||||
self.styles._rule_layout = tokens[0].value
|
value = tokens[0].value
|
||||||
|
layout_name = cast(LayoutName, value)
|
||||||
|
try:
|
||||||
|
self.styles._rule_layout = get_layout(layout_name)
|
||||||
|
except MissingLayout:
|
||||||
|
self.error(
|
||||||
|
name,
|
||||||
|
tokens[0],
|
||||||
|
f"invalid value for layout (received {value!r}, expected {friendly_list(LAYOUT_MAP.keys())})",
|
||||||
|
)
|
||||||
|
|
||||||
def process_text(self, name: str, tokens: list[Token], important: bool) -> None:
|
def process_text(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||||
style_definition = " ".join(token.value for token in tokens)
|
style_definition = " ".join(token.value for token in tokens)
|
||||||
|
|||||||
@@ -5,14 +5,15 @@ import rich.repr
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Iterable
|
from typing import Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
from .. import log
|
|
||||||
from ..dom import DOMNode
|
|
||||||
from .styles import Styles
|
from .styles import Styles
|
||||||
from .tokenize import Token
|
from .tokenize import Token
|
||||||
from .types import Specificity3
|
from .types import Specificity3
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..dom import DOMNode
|
||||||
|
|
||||||
|
|
||||||
class SelectorType(Enum):
|
class SelectorType(Enum):
|
||||||
UNIVERSAL = 1
|
UNIVERSAL = 1
|
||||||
|
|||||||
@@ -1,32 +1,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import sys
|
|
||||||
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
from rich import print
|
|
||||||
from rich.color import Color
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
from rich.color import Color
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from .. import log
|
|
||||||
from .._animator import SimpleAnimation, Animation, EasingFunction
|
|
||||||
from .._types import MessageTarget
|
|
||||||
from .errors import StyleValueError
|
|
||||||
from .. import events
|
|
||||||
from ._error_tools import friendly_list
|
|
||||||
from .types import Specificity3, Specificity4
|
|
||||||
from .constants import (
|
|
||||||
VALID_DISPLAY,
|
|
||||||
VALID_VISIBILITY,
|
|
||||||
VALID_LAYOUT,
|
|
||||||
NULL_SPACING,
|
|
||||||
)
|
|
||||||
from .scalar_animation import ScalarAnimation
|
|
||||||
from ..geometry import NULL_OFFSET, Offset, Spacing
|
|
||||||
from .scalar import Scalar, ScalarOffset, Unit
|
|
||||||
from .transition import Transition
|
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
BorderProperty,
|
BorderProperty,
|
||||||
BoxProperty,
|
BoxProperty,
|
||||||
@@ -42,11 +24,26 @@ from ._style_properties import (
|
|||||||
StyleProperty,
|
StyleProperty,
|
||||||
StyleFlagsProperty,
|
StyleFlagsProperty,
|
||||||
TransitionsProperty,
|
TransitionsProperty,
|
||||||
|
LayoutProperty,
|
||||||
)
|
)
|
||||||
|
from .constants import (
|
||||||
|
VALID_DISPLAY,
|
||||||
|
VALID_VISIBILITY,
|
||||||
|
)
|
||||||
|
from .scalar import Scalar, ScalarOffset, Unit
|
||||||
|
from .scalar_animation import ScalarAnimation
|
||||||
|
from .transition import Transition
|
||||||
from .types import Display, Edge, Visibility
|
from .types import Display, Edge, Visibility
|
||||||
|
|
||||||
|
|
||||||
|
from .types import Specificity3, Specificity4
|
||||||
|
from .. import log
|
||||||
|
from .._animator import Animation, EasingFunction
|
||||||
|
from ..geometry import Spacing
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from ..layout import Layout
|
||||||
from ..dom import DOMNode
|
from ..dom import DOMNode
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +61,7 @@ class Styles:
|
|||||||
|
|
||||||
_rule_display: Display | None = None
|
_rule_display: Display | None = None
|
||||||
_rule_visibility: Visibility | None = None
|
_rule_visibility: Visibility | None = None
|
||||||
_rule_layout: str | None = None
|
_rule_layout: "Layout" | None = None
|
||||||
|
|
||||||
_rule_text_color: Color | None = None
|
_rule_text_color: Color | None = None
|
||||||
_rule_text_background: Color | None = None
|
_rule_text_background: Color | None = None
|
||||||
@@ -104,7 +101,7 @@ class Styles:
|
|||||||
|
|
||||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||||
layout = StringEnumProperty(VALID_LAYOUT, "dock")
|
layout = LayoutProperty()
|
||||||
|
|
||||||
text = StyleProperty()
|
text = StyleProperty()
|
||||||
text_color = ColorProperty()
|
text_color = ColorProperty()
|
||||||
@@ -273,6 +270,7 @@ class Styles:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
setattr(styles, f"_rule_{key}", value)
|
setattr(styles, f"_rule_{key}", value)
|
||||||
|
|
||||||
if self.node is not None:
|
if self.node is not None:
|
||||||
self.node.on_style_change()
|
self.node.on_style_change()
|
||||||
|
|
||||||
@@ -423,7 +421,7 @@ if __name__ == "__main__":
|
|||||||
styles.dock = "bar"
|
styles.dock = "bar"
|
||||||
styles.layers = "foo bar"
|
styles.layers = "foo bar"
|
||||||
|
|
||||||
from rich import inspect, print
|
from rich import print
|
||||||
|
|
||||||
print(styles.text_style)
|
print(styles.text_style)
|
||||||
print(styles.text)
|
print(styles.text)
|
||||||
|
|||||||
@@ -108,18 +108,37 @@ class Stylesheet:
|
|||||||
yield selector_set.specificity
|
yield selector_set.specificity
|
||||||
|
|
||||||
def apply(self, node: DOMNode) -> None:
|
def apply(self, node: DOMNode) -> None:
|
||||||
|
"""Apply the stylesheet to a DOM node.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
node (DOMNode): The ``DOMNode`` to apply the stylesheet to.
|
||||||
|
Applies the styles defined in this ``Stylesheet`` to the node.
|
||||||
|
If the same rule is defined multiple times for the node (e.g. multiple
|
||||||
|
classes modifying the same CSS property), then only the most specific
|
||||||
|
rule will be applied.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Dictionary of rule attribute names e.g. "text_background" to list of tuples.
|
||||||
|
# The tuples contain the rule specificity, and the value for that rule.
|
||||||
|
# We can use this to determine, for a given rule, whether we should apply it
|
||||||
|
# or not by examining the specificity. If we have two rules for the
|
||||||
|
# same attribute, then we can choose the most specific rule and use that.
|
||||||
rule_attributes: dict[str, list[tuple[Specificity4, object]]]
|
rule_attributes: dict[str, list[tuple[Specificity4, object]]]
|
||||||
rule_attributes = defaultdict(list)
|
rule_attributes = defaultdict(list)
|
||||||
|
|
||||||
_check_rule = self._check_rule
|
_check_rule = self._check_rule
|
||||||
|
|
||||||
|
# TODO: The line below breaks inline styles and animations
|
||||||
node.styles.reset()
|
node.styles.reset()
|
||||||
|
|
||||||
# Get the default node CSS rules
|
# Collect default node CSS rules
|
||||||
for key, default_specificity, value in node._default_rules:
|
for key, default_specificity, value in node._default_rules:
|
||||||
rule_attributes[key].append((default_specificity, value))
|
rule_attributes[key].append((default_specificity, value))
|
||||||
|
|
||||||
# Apply styles on top of the default node CSS rules
|
# Collect the rules defined in the stylesheet
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
for specificity in _check_rule(rule, node):
|
for specificity in _check_rule(rule, node):
|
||||||
for key, rule_specificity, value in rule.styles.extract_rules(
|
for key, rule_specificity, value in rule.styles.extract_rules(
|
||||||
@@ -127,6 +146,7 @@ class Stylesheet:
|
|||||||
):
|
):
|
||||||
rule_attributes[key].append((rule_specificity, value))
|
rule_attributes[key].append((rule_specificity, value))
|
||||||
|
|
||||||
|
# For each rule declared for this node, keep only the most specific one
|
||||||
get_first_item = itemgetter(0)
|
get_first_item = itemgetter(0)
|
||||||
node_rules = [
|
node_rules = [
|
||||||
(name, max(specificity_rules, key=get_first_item)[1])
|
(name, max(specificity_rules, key=get_first_item)[1])
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, cast, Iterable, Iterator, TYPE_CHECKING
|
from typing import Iterable, Iterator, TYPE_CHECKING
|
||||||
|
|
||||||
from rich.highlighter import ReprHighlighter
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
from rich.highlighter import ReprHighlighter
|
||||||
from rich.pretty import Pretty
|
from rich.pretty import Pretty
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
from rich.tree import Tree
|
from rich.tree import Tree
|
||||||
|
|
||||||
|
from ._node_list import NodeList
|
||||||
from .css._error_tools import friendly_list
|
from .css._error_tools import friendly_list
|
||||||
from .css.constants import VALID_DISPLAY
|
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||||
from .css.errors import StyleValueError
|
from .css.errors import StyleValueError
|
||||||
from .css.styles import Styles
|
from .css.styles import Styles
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
from ._node_list import NodeList
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .css.query import DOMQuery
|
from .css.query import DOMQuery
|
||||||
from .widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
class NoParent(Exception):
|
class NoParent(Exception):
|
||||||
@@ -160,6 +158,22 @@ class DOMNode(MessagePump):
|
|||||||
f"expected {friendly_list(VALID_DISPLAY)})",
|
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
|
@property
|
||||||
def z(self) -> tuple[int, ...]:
|
def z(self) -> tuple[int, ...]:
|
||||||
"""Get the z index tuple for this node.
|
"""Get the z index tuple for this node.
|
||||||
|
|||||||
@@ -415,6 +415,25 @@ class Region(NamedTuple):
|
|||||||
)
|
)
|
||||||
return new_region
|
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:
|
def intersection(self, region: Region) -> Region:
|
||||||
"""Get that covers both regions.
|
"""Get that covers both regions.
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,17 @@ class WidgetPlacement(NamedTuple):
|
|||||||
widget: Widget | None = None
|
widget: Widget | None = None
|
||||||
order: int = 0
|
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
|
@rich.repr.auto
|
||||||
class LayoutUpdate:
|
class LayoutUpdate:
|
||||||
@@ -199,6 +210,15 @@ class Layout(ABC):
|
|||||||
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
|
raise NoWidget(f"No widget under screen coordinate ({x}, {y})")
|
||||||
|
|
||||||
def get_style_at(self, x: int, y: int) -> Style:
|
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:
|
try:
|
||||||
widget, region = self.get_widget_at(x, y)
|
widget, region = self.get_widget_at(x, y)
|
||||||
except NoWidget:
|
except NoWidget:
|
||||||
@@ -217,6 +237,18 @@ class Layout(ABC):
|
|||||||
return Style.null()
|
return Style.null()
|
||||||
|
|
||||||
def get_widget_region(self, widget: Widget) -> Region:
|
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:
|
try:
|
||||||
region, *_ = self.map[widget]
|
region, *_ = self.map[widget]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -270,7 +302,7 @@ class Layout(ABC):
|
|||||||
|
|
||||||
for widget, region, _order, clip in widget_regions:
|
for widget, region, _order, clip in widget_regions:
|
||||||
|
|
||||||
if not widget.is_visual:
|
if not (widget.is_visual and widget.visible):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
lines = widget._get_lines()
|
lines = widget._get_lines()
|
||||||
|
|||||||
@@ -5,14 +5,10 @@ from collections import defaultdict
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
from typing import Iterable, TYPE_CHECKING, NamedTuple, Sequence
|
||||||
|
|
||||||
|
|
||||||
from .. import log
|
|
||||||
from ..dom import DOMNode
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
|
from ..css.types import Edge
|
||||||
from ..geometry import Offset, Region, Size
|
from ..geometry import Offset, Region, Size
|
||||||
from ..layout import Layout, WidgetPlacement
|
from ..layout import Layout, WidgetPlacement
|
||||||
from ..layout_map import LayoutMap
|
|
||||||
from ..css.types import Edge
|
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
@@ -20,12 +16,9 @@ if sys.version_info >= (3, 8):
|
|||||||
else:
|
else:
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..widget import Widget
|
|
||||||
from ..view import View
|
from ..view import View
|
||||||
|
|
||||||
|
|
||||||
DockEdge = Literal["top", "right", "bottom", "left"]
|
DockEdge = Literal["top", "right", "bottom", "left"]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
from __future__ import annotations
|
import sys
|
||||||
|
|
||||||
from ..layout import Layout
|
from ..layout import Layout
|
||||||
from .dock import DockLayout
|
from ..layouts.dock import DockLayout
|
||||||
from .grid import GridLayout
|
from ..layouts.grid import GridLayout
|
||||||
from .vertical import VerticalLayout
|
from ..layouts.vertical import VerticalLayout
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
from typing import Literal
|
||||||
|
else:
|
||||||
|
from typing_extensions import Literal
|
||||||
|
|
||||||
|
LayoutName = Literal["dock", "grid", "vertical"]
|
||||||
|
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
|
||||||
|
|
||||||
|
|
||||||
class MissingLayout(Exception):
|
class MissingLayout(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
|
def get_layout(name: LayoutName) -> Layout:
|
||||||
|
|
||||||
|
|
||||||
def get_layout(name: str) -> Layout:
|
|
||||||
"""Get a named layout object.
|
"""Get a named layout object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -25,7 +30,8 @@ def get_layout(name: str) -> Layout:
|
|||||||
Returns:
|
Returns:
|
||||||
Layout: A layout object.
|
Layout: A layout object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
layout_class = LAYOUT_MAP.get(name)
|
layout_class = LAYOUT_MAP.get(name)
|
||||||
if layout_class is None:
|
if layout_class is None:
|
||||||
raise MissingLayout("no layout called {name!r}")
|
raise MissingLayout(f"no layout called {name!r}, valid layouts")
|
||||||
return layout_class()
|
return layout_class()
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from operator import itemgetter
|
|
||||||
from logging import getLogger
|
|
||||||
from itertools import cycle, product
|
from itertools import cycle, product
|
||||||
import sys
|
from logging import getLogger
|
||||||
|
from operator import itemgetter
|
||||||
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
from typing import Iterable, NamedTuple, TYPE_CHECKING
|
||||||
|
|
||||||
from .._layout_resolve import layout_resolve
|
from .._layout_resolve import layout_resolve
|
||||||
from ..geometry import Size, Offset, Region
|
from ..geometry import Size, Offset, Region
|
||||||
from ..layout import Layout, WidgetPlacement
|
from ..layout import Layout, WidgetPlacement
|
||||||
from ..widget import Widget
|
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..widget import Widget
|
from ..widget import Widget
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ from typing import Iterable, TYPE_CHECKING
|
|||||||
|
|
||||||
from ..geometry import Offset, Region, Size, Spacing, SpacingDimensions
|
from ..geometry import Offset, Region, Size, Spacing, SpacingDimensions
|
||||||
from ..layout import Layout, WidgetPlacement
|
from ..layout import Layout, WidgetPlacement
|
||||||
from ..widget import Widget
|
|
||||||
from .._loop import loop_last
|
from .._loop import loop_last
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.color import Color
|
from rich.color import Color
|
||||||
from rich.console import Console, ConsoleOptions, RenderResult, RenderableType
|
from rich.console import ConsoleOptions, RenderResult, RenderableType
|
||||||
from rich.segment import Segment, Segments
|
from rich.segment import Segment
|
||||||
from rich.style import Style, StyleType
|
from rich.style import Style, StyleType
|
||||||
|
|
||||||
|
from textual.reactive import Reactive
|
||||||
from . import events
|
from . import events
|
||||||
from .geometry import Offset
|
|
||||||
from ._types import MessageTarget
|
from ._types import MessageTarget
|
||||||
|
from .geometry import Offset
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .widget import Reactive, Widget
|
from .widget import Widget
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
|
|||||||
@@ -1,55 +1,26 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from itertools import chain
|
from typing import Callable, Iterable
|
||||||
from typing import Callable, Iterable, ClassVar, TYPE_CHECKING
|
|
||||||
|
|
||||||
from rich.console import RenderableType
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
from rich.console import RenderableType
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from . import events
|
from . import errors, events, messages
|
||||||
from . import errors
|
|
||||||
from . import log
|
|
||||||
from . import messages
|
|
||||||
from .layout import Layout, NoWidget, WidgetPlacement
|
|
||||||
from .layouts.factory import get_layout
|
|
||||||
from .geometry import Size, Offset, Region
|
from .geometry import Size, Offset, Region
|
||||||
|
from .layout import Layout, NoWidget, WidgetPlacement
|
||||||
from .reactive import Reactive, watch
|
from .reactive import Reactive, watch
|
||||||
|
from .widget import Widget
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class View(Widget):
|
class View(Widget):
|
||||||
|
|
||||||
STYLES = """
|
STYLES = """
|
||||||
|
layout: dock;
|
||||||
docks: main=top;
|
docks: main=top;
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||||
|
|
||||||
from .layouts.dock import DockLayout
|
|
||||||
|
|
||||||
self._layout: Layout = DockLayout()
|
|
||||||
|
|
||||||
self.mouse_over: Widget | None = None
|
self.mouse_over: Widget | None = None
|
||||||
self.widgets: set[Widget] = set()
|
self.widgets: set[Widget] = set()
|
||||||
self._mouse_style: Style = Style()
|
self._mouse_style: Style = Style()
|
||||||
@@ -69,17 +40,34 @@ class View(Widget):
|
|||||||
cls.layout_factory = layout
|
cls.layout_factory = layout
|
||||||
super().__init_subclass__(**kwargs)
|
super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
layout = LayoutProperty()
|
|
||||||
|
|
||||||
background: Reactive[str] = Reactive("")
|
background: Reactive[str] = Reactive("")
|
||||||
scroll_x: Reactive[int] = Reactive(0)
|
scroll_x: Reactive[int] = Reactive(0)
|
||||||
scroll_y: Reactive[int] = Reactive(0)
|
scroll_y: Reactive[int] = Reactive(0)
|
||||||
virtual_size = Reactive(Size(0, 0))
|
virtual_size = Reactive(Size(0, 0))
|
||||||
|
|
||||||
async def watch_background(self, value: str) -> None:
|
async def watch_background(self, value: str) -> None:
|
||||||
self._layout.background = value
|
self.layout.background = value
|
||||||
self.app.refresh()
|
self.app.refresh()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def layout(self) -> Layout:
|
||||||
|
"""Convenience property for accessing ``view.styles.layout``.
|
||||||
|
|
||||||
|
Returns: The Layout associated with this view
|
||||||
|
"""
|
||||||
|
return self.styles.layout
|
||||||
|
|
||||||
|
@layout.setter
|
||||||
|
def layout(self, new_value: Layout) -> None:
|
||||||
|
"""Convenience property setter for setting ``view.styles.layout``.
|
||||||
|
Args:
|
||||||
|
new_value:
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None
|
||||||
|
"""
|
||||||
|
self.styles.layout = new_value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scroll(self) -> Offset:
|
def scroll(self) -> Offset:
|
||||||
return Offset(self.scroll_x, self.scroll_y)
|
return Offset(self.scroll_x, self.scroll_y)
|
||||||
@@ -105,18 +93,23 @@ class View(Widget):
|
|||||||
return self.app.is_mounted(widget)
|
return self.app.is_mounted(widget)
|
||||||
|
|
||||||
def render(self) -> RenderableType:
|
def render(self) -> RenderableType:
|
||||||
return self._layout
|
return self.layout
|
||||||
|
|
||||||
def get_offset(self, widget: Widget) -> Offset:
|
def get_offset(self, widget: Widget) -> Offset:
|
||||||
return self._layout.get_offset(widget)
|
return self.layout.get_offset(widget)
|
||||||
|
|
||||||
def get_arrangement(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
def get_arrangement(self, size: Size, scroll: Offset) -> Iterable[WidgetPlacement]:
|
||||||
cached_size, cached_scroll, arrangement = self._cached_arrangement
|
cached_size, cached_scroll, arrangement = self._cached_arrangement
|
||||||
if cached_size == size and cached_scroll == scroll:
|
if cached_size == size and cached_scroll == scroll:
|
||||||
return arrangement
|
return arrangement
|
||||||
arrangement = list(self._layout.arrange(self, size, scroll))
|
|
||||||
self._cached_arrangement = (size, scroll, arrangement)
|
placements = [
|
||||||
return arrangement
|
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:
|
async def handle_update(self, message: messages.Update) -> None:
|
||||||
if self.is_root_view:
|
if self.is_root_view:
|
||||||
@@ -124,7 +117,7 @@ class View(Widget):
|
|||||||
widget = message.widget
|
widget = message.widget
|
||||||
assert isinstance(widget, Widget)
|
assert isinstance(widget, Widget)
|
||||||
|
|
||||||
display_update = self._layout.update_widget(self.console, widget)
|
display_update = self.layout.update_widget(self.console, widget)
|
||||||
if display_update is not None:
|
if display_update is not None:
|
||||||
self.app.display(display_update)
|
self.app.display(display_update)
|
||||||
|
|
||||||
@@ -141,7 +134,7 @@ class View(Widget):
|
|||||||
async def refresh_layout(self) -> None:
|
async def refresh_layout(self) -> None:
|
||||||
self._cached_arrangement = (Size(), Offset(), [])
|
self._cached_arrangement = (Size(), Offset(), [])
|
||||||
try:
|
try:
|
||||||
await self._layout.mount_all(self)
|
await self.layout.mount_all(self)
|
||||||
if not self.is_root_view:
|
if not self.is_root_view:
|
||||||
await self.app.view.refresh_layout()
|
await self.app.view.refresh_layout()
|
||||||
return
|
return
|
||||||
@@ -149,8 +142,8 @@ class View(Widget):
|
|||||||
if not self.size:
|
if not self.size:
|
||||||
return
|
return
|
||||||
|
|
||||||
hidden, shown, resized = self._layout.reflow(self, Size(*self.console.size))
|
hidden, shown, resized = self.layout.reflow(self, Size(*self.console.size))
|
||||||
assert self._layout.map is not None
|
assert self.layout.map is not None
|
||||||
|
|
||||||
for widget in hidden:
|
for widget in hidden:
|
||||||
widget.post_message_no_wait(events.Hide(self))
|
widget.post_message_no_wait(events.Hide(self))
|
||||||
@@ -160,7 +153,7 @@ class View(Widget):
|
|||||||
send_resize = shown
|
send_resize = shown
|
||||||
send_resize.update(resized)
|
send_resize.update(resized)
|
||||||
|
|
||||||
for widget, region, unclipped_region in self._layout:
|
for widget, region, unclipped_region in self.layout:
|
||||||
widget._update_size(unclipped_region.size)
|
widget._update_size(unclipped_region.size)
|
||||||
if widget in send_resize:
|
if widget in send_resize:
|
||||||
widget.post_message_no_wait(
|
widget.post_message_no_wait(
|
||||||
@@ -177,13 +170,13 @@ class View(Widget):
|
|||||||
event.stop()
|
event.stop()
|
||||||
|
|
||||||
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
|
||||||
return self._layout.get_widget_at(x, y)
|
return self.layout.get_widget_at(x, y)
|
||||||
|
|
||||||
def get_style_at(self, x: int, y: int) -> Style:
|
def get_style_at(self, x: int, y: int) -> Style:
|
||||||
return self._layout.get_style_at(x, y)
|
return self.layout.get_style_at(x, y)
|
||||||
|
|
||||||
def get_widget_region(self, widget: Widget) -> Region:
|
def get_widget_region(self, widget: Widget) -> Region:
|
||||||
return self._layout.get_widget_region(widget)
|
return self.layout.get_widget_region(widget)
|
||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
async def watch_background(value: str) -> None:
|
async def watch_background(value: str) -> None:
|
||||||
@@ -192,8 +185,8 @@ class View(Widget):
|
|||||||
watch(self.app, "background", watch_background)
|
watch(self.app, "background", watch_background)
|
||||||
|
|
||||||
async def on_idle(self, event: events.Idle) -> None:
|
async def on_idle(self, event: events.Idle) -> None:
|
||||||
if self._layout.check_update():
|
if self.layout.check_update():
|
||||||
self._layout.reset_update()
|
self.layout.reset_update()
|
||||||
await self.refresh_layout()
|
await self.refresh_layout()
|
||||||
|
|
||||||
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
async def _on_mouse_move(self, event: events.MouseMove) -> None:
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ class DockView(View):
|
|||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
dock = Dock(edge, widgets, z)
|
dock = Dock(edge, widgets, z)
|
||||||
assert isinstance(self._layout, DockLayout)
|
assert isinstance(self.layout, DockLayout)
|
||||||
self._layout.docks.append(dock)
|
self.layout.docks.append(dock)
|
||||||
for widget in widgets:
|
for widget in widgets:
|
||||||
if id is not None:
|
if id is not None:
|
||||||
widget._id = id
|
widget._id = id
|
||||||
@@ -60,8 +60,8 @@ class DockView(View):
|
|||||||
grid = GridLayout(gap=gap, gutter=gutter, align=align)
|
grid = GridLayout(gap=gap, gutter=gutter, align=align)
|
||||||
view = View(layout=grid, id=id, name=name)
|
view = View(layout=grid, id=id, name=name)
|
||||||
dock = Dock(edge, (view,), z)
|
dock = Dock(edge, (view,), z)
|
||||||
assert isinstance(self._layout, DockLayout)
|
assert isinstance(self.layout, DockLayout)
|
||||||
self._layout.docks.append(dock)
|
self.layout.docks.append(dock)
|
||||||
if size is not do_not_set:
|
if size is not do_not_set:
|
||||||
view.layout_size = cast(Optional[int], size)
|
view.layout_size = cast(Optional[int], size)
|
||||||
if name is None:
|
if name is None:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class WindowView(View, layout=VerticalLayout):
|
|||||||
super().__init__(name=name, layout=layout)
|
super().__init__(name=name, layout=layout)
|
||||||
|
|
||||||
async def update(self, widget: Widget | RenderableType) -> None:
|
async def update(self, widget: Widget | RenderableType) -> None:
|
||||||
layout = self._layout
|
layout = self.layout
|
||||||
assert isinstance(layout, VerticalLayout)
|
assert isinstance(layout, VerticalLayout)
|
||||||
layout.clear()
|
layout.clear()
|
||||||
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
||||||
@@ -46,7 +46,7 @@ class WindowView(View, layout=VerticalLayout):
|
|||||||
await self.emit(WindowChange(self))
|
await self.emit(WindowChange(self))
|
||||||
|
|
||||||
async def handle_layout(self, message: messages.Layout) -> None:
|
async def handle_layout(self, message: messages.Layout) -> None:
|
||||||
self._layout.require_update()
|
self.layout.require_update()
|
||||||
message.stop()
|
message.stop()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
@@ -54,11 +54,11 @@ class WindowView(View, layout=VerticalLayout):
|
|||||||
await self.emit(WindowChange(self))
|
await self.emit(WindowChange(self))
|
||||||
|
|
||||||
async def watch_scroll_x(self, value: int) -> None:
|
async def watch_scroll_x(self, value: int) -> None:
|
||||||
self._layout.require_update()
|
self.layout.require_update()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
async def watch_scroll_y(self, value: int) -> None:
|
async def watch_scroll_y(self, value: int) -> None:
|
||||||
self._layout.require_update()
|
self.layout.require_update()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
async def on_resize(self, event: events.Resize) -> None:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from logging import PercentStyle, getLogger
|
from logging import getLogger
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
Awaitable,
|
Awaitable,
|
||||||
@@ -11,35 +11,29 @@ from typing import (
|
|||||||
NamedTuple,
|
NamedTuple,
|
||||||
cast,
|
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 errors
|
||||||
|
from . import events
|
||||||
from ._animator import BoundAnimator
|
from ._animator import BoundAnimator
|
||||||
from ._border import Border, BORDER_STYLES
|
from ._border import Border
|
||||||
from ._callback import invoke
|
from ._callback import invoke
|
||||||
from .blank import Blank
|
|
||||||
from .dom import DOMNode
|
|
||||||
from ._context import active_app
|
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 .message import Message
|
||||||
from .messages import Layout, Update
|
from .messages import Layout, Update
|
||||||
from .reactive import Reactive, watch
|
from .reactive import watch
|
||||||
from ._types import Lines
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .app import App
|
|
||||||
from .view import View
|
from .view import View
|
||||||
|
|
||||||
log = getLogger("rich")
|
log = getLogger("rich")
|
||||||
@@ -163,9 +157,6 @@ class Widget(DOMNode):
|
|||||||
styles = self.styles
|
styles = self.styles
|
||||||
parent_text_style = self.parent.text_style
|
parent_text_style = self.parent.text_style
|
||||||
|
|
||||||
if styles.visibility == "hidden":
|
|
||||||
renderable = Blank(parent_text_style)
|
|
||||||
else:
|
|
||||||
text_style = styles.text
|
text_style = styles.text
|
||||||
renderable_text_style = parent_text_style + text_style
|
renderable_text_style = parent_text_style + text_style
|
||||||
if renderable_text_style:
|
if renderable_text_style:
|
||||||
@@ -177,12 +168,7 @@ class Widget(DOMNode):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if styles.has_border:
|
if styles.has_border:
|
||||||
renderable = Border(
|
renderable = Border(renderable, styles.border, style=renderable_text_style)
|
||||||
renderable, styles.border, style=renderable_text_style
|
|
||||||
)
|
|
||||||
|
|
||||||
if styles.has_margin:
|
|
||||||
renderable = Padding(renderable, styles.margin, style=parent_text_style)
|
|
||||||
|
|
||||||
if styles.has_outline:
|
if styles.has_outline:
|
||||||
renderable = Border(
|
renderable = Border(
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import rich.repr
|
|||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
from .. import events
|
from .. import events
|
||||||
from ..widget import Reactive, Widget
|
from ..reactive import Reactive
|
||||||
|
from ..widget import Widget
|
||||||
|
|
||||||
log = getLogger("rich")
|
log = getLogger("rich")
|
||||||
|
|
||||||
|
|||||||
@@ -93,13 +93,13 @@ class ScrollView(View):
|
|||||||
await self.window.update(renderable)
|
await self.window.update(renderable)
|
||||||
|
|
||||||
async def on_mount(self, event: events.Mount) -> None:
|
async def on_mount(self, event: events.Mount) -> None:
|
||||||
assert isinstance(self._layout, GridLayout)
|
assert isinstance(self.layout, GridLayout)
|
||||||
self._layout.place(
|
self.layout.place(
|
||||||
content=self.window,
|
content=self.window,
|
||||||
vscroll=self.vscroll,
|
vscroll=self.vscroll,
|
||||||
hscroll=self.hscroll,
|
hscroll=self.hscroll,
|
||||||
)
|
)
|
||||||
await self._layout.mount_all(self)
|
await self.layout.mount_all(self)
|
||||||
|
|
||||||
def home(self) -> None:
|
def home(self) -> None:
|
||||||
self.x = self.y = 0
|
self.x = self.y = 0
|
||||||
@@ -215,10 +215,10 @@ class ScrollView(View):
|
|||||||
self.vscroll.virtual_size = virtual_height
|
self.vscroll.virtual_size = virtual_height
|
||||||
self.vscroll.window_size = height
|
self.vscroll.window_size = height
|
||||||
|
|
||||||
assert isinstance(self._layout, GridLayout)
|
assert isinstance(self.layout, GridLayout)
|
||||||
|
|
||||||
vscroll_change = self._layout.show_column("vscroll", virtual_height > height)
|
vscroll_change = self.layout.show_column("vscroll", virtual_height > height)
|
||||||
hscroll_change = self._layout.show_row("hscroll", virtual_width > width)
|
hscroll_change = self.layout.show_row("hscroll", virtual_width > width)
|
||||||
if hscroll_change or vscroll_change:
|
if hscroll_change or vscroll_change:
|
||||||
self.refresh(layout=True)
|
self.refresh(layout=True)
|
||||||
|
|
||||||
|
|||||||
14
tests/layouts/test_factory.py
Normal file
14
tests/layouts/test_factory.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from textual.layouts.dock import DockLayout
|
||||||
|
from textual.layouts.factory import get_layout, MissingLayout
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_layout_valid_layout():
|
||||||
|
layout = get_layout("dock")
|
||||||
|
assert type(layout) is DockLayout
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_layout_invalid_layout():
|
||||||
|
with pytest.raises(MissingLayout):
|
||||||
|
get_layout("invalid")
|
||||||
@@ -4,6 +4,27 @@ from rich.color import Color, ColorType
|
|||||||
from textual.css.scalar import Scalar, Unit
|
from textual.css.scalar import Scalar, Unit
|
||||||
from textual.css.stylesheet import Stylesheet, StylesheetParseError
|
from textual.css.stylesheet import Stylesheet, StylesheetParseError
|
||||||
from textual.css.transition import Transition
|
from textual.css.transition import Transition
|
||||||
|
from textual.layouts.dock import DockLayout
|
||||||
|
|
||||||
|
|
||||||
|
class TestParseLayout:
|
||||||
|
def test_valid_layout_name(self):
|
||||||
|
css = "#some-widget { layout: dock; }"
|
||||||
|
|
||||||
|
stylesheet = Stylesheet()
|
||||||
|
stylesheet.parse(css)
|
||||||
|
|
||||||
|
styles = stylesheet.rules[0].styles
|
||||||
|
assert isinstance(styles.layout, DockLayout)
|
||||||
|
|
||||||
|
def test_invalid_layout_name(self):
|
||||||
|
css = "#some-widget { layout: invalidlayout; }"
|
||||||
|
|
||||||
|
stylesheet = Stylesheet()
|
||||||
|
with pytest.raises(StylesheetParseError) as ex:
|
||||||
|
stylesheet.parse(css)
|
||||||
|
|
||||||
|
assert ex.value.errors is not None
|
||||||
|
|
||||||
|
|
||||||
class TestParseText:
|
class TestParseText:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from textual.geometry import clamp, Offset, Size, Region
|
from textual.geometry import clamp, Offset, Size, Region, Spacing
|
||||||
|
|
||||||
|
|
||||||
def test_dimensions_region():
|
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)
|
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():
|
def test_region_intersection():
|
||||||
assert Region(0, 0, 100, 50).intersection(Region(10, 10, 10, 10)) == Region(
|
assert Region(0, 0, 100, 50).intersection(Region(10, 10, 10, 10)) == Region(
|
||||||
10, 10, 10, 10
|
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