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 view-from-css
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")
|
||||||
@@ -10,7 +10,7 @@ from rich.tree import Tree
|
|||||||
|
|
||||||
from ._node_list import NodeList
|
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
|
||||||
@@ -158,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.
|
||||||
|
|||||||
@@ -199,6 +199,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 +226,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 +291,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()
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ from ._border import Border
|
|||||||
from ._callback import invoke
|
from ._callback import invoke
|
||||||
from ._context import active_app
|
from ._context import active_app
|
||||||
from ._types import Lines
|
from ._types import Lines
|
||||||
from .blank import Blank
|
|
||||||
from .dom import DOMNode
|
from .dom import DOMNode
|
||||||
from .geometry import Size, Spacing
|
from .geometry import Size, Spacing
|
||||||
from .message import Message
|
from .message import Message
|
||||||
@@ -158,34 +157,29 @@ 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":
|
text_style = styles.text
|
||||||
renderable = Blank(parent_text_style)
|
renderable_text_style = parent_text_style + text_style
|
||||||
else:
|
if renderable_text_style:
|
||||||
text_style = styles.text
|
renderable = Styled(renderable, renderable_text_style)
|
||||||
renderable_text_style = parent_text_style + text_style
|
|
||||||
if renderable_text_style:
|
|
||||||
renderable = Styled(renderable, renderable_text_style)
|
|
||||||
|
|
||||||
if styles.has_padding:
|
if styles.has_padding:
|
||||||
renderable = Padding(
|
renderable = Padding(
|
||||||
renderable, styles.padding, style=renderable_text_style
|
renderable, styles.padding, style=renderable_text_style
|
||||||
)
|
)
|
||||||
|
|
||||||
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:
|
if styles.has_margin:
|
||||||
renderable = Padding(renderable, styles.margin, style=parent_text_style)
|
renderable = Padding(renderable, styles.margin, style=parent_text_style)
|
||||||
|
|
||||||
if styles.has_outline:
|
if styles.has_outline:
|
||||||
renderable = Border(
|
renderable = Border(
|
||||||
renderable,
|
renderable,
|
||||||
styles.outline,
|
styles.outline,
|
||||||
outline=True,
|
outline=True,
|
||||||
style=renderable_text_style,
|
style=renderable_text_style,
|
||||||
)
|
)
|
||||||
|
|
||||||
return renderable
|
return renderable
|
||||||
|
|
||||||
|
|||||||
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