mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -10,11 +10,10 @@ Widget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#thing {
|
#thing {
|
||||||
|
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 10;
|
height: auto;
|
||||||
background:magenta;
|
background:magenta;
|
||||||
margin: 3;
|
margin: 1;
|
||||||
padding: 1;
|
padding: 1;
|
||||||
border: solid white;
|
border: solid white;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
from textual.app import App
|
from textual.app import App, ComposeResult
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
from textual.widgets import Static
|
from textual.widgets import Static
|
||||||
|
|
||||||
|
|
||||||
class Thing(Widget):
|
class Thing(Widget):
|
||||||
def render(self):
|
def render(self):
|
||||||
return Text.from_markup("Hello, World. [b magenta]Lorem impsum.")
|
return "Hello, 3434 World.\n[b]Lorem impsum."
|
||||||
|
|
||||||
|
|
||||||
class AlignApp(App):
|
class AlignApp(App):
|
||||||
def on_load(self):
|
def on_load(self):
|
||||||
self.bind("t", "log_tree")
|
self.bind("t", "log_tree")
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def compose(self) -> ComposeResult:
|
||||||
self.log("MOUNTED")
|
yield Thing(id="thing")
|
||||||
self.mount(thing=Thing(), thing2=Static("0123456789"), thing3=Widget())
|
yield Static("foo", id="thing2")
|
||||||
|
yield Widget(id="thing3")
|
||||||
|
|
||||||
def action_log_tree(self):
|
def action_log_tree(self):
|
||||||
self.log(self.screen.tree)
|
self.log(self.screen.tree)
|
||||||
|
|
||||||
|
|
||||||
AlignApp.run(css_path="align.css", log_path="textual.log", watch_css=True)
|
app = AlignApp(css_path="align.css")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class BasicApp(App):
|
|||||||
self.focused.styles.border_top = ("solid", "invalid-color")
|
self.focused.styles.border_top = ("solid", "invalid-color")
|
||||||
|
|
||||||
|
|
||||||
app = BasicApp(css_path="uber.css", log_path="textual.log", log_verbosity=1)
|
app = BasicApp(css_path="uber.css")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run()
|
app.run()
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
log_verbosity: int = 1,
|
log_verbosity: int = 1,
|
||||||
title: str = "Textual Application",
|
title: str = "Textual Application",
|
||||||
css_path: str | PurePath | None = None,
|
css_path: str | PurePath | None = None,
|
||||||
watch_css: bool = True,
|
watch_css: bool = False,
|
||||||
):
|
):
|
||||||
"""Textual application base class
|
"""Textual application base class
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
log_verbosity (int, optional): Log verbosity from 0-3. Defaults to 1.
|
log_verbosity (int, optional): Log verbosity from 0-3. Defaults to 1.
|
||||||
title (str, optional): Default title of the application. Defaults to "Textual Application".
|
title (str, optional): Default title of the application. Defaults to "Textual Application".
|
||||||
css_path (str | PurePath | None, optional): Path to CSS or ``None`` for no CSS file. Defaults to None.
|
css_path (str | PurePath | None, optional): Path to CSS or ``None`` for no CSS file. Defaults to None.
|
||||||
watch_css (bool, optional): Watch CSS for changes. Defaults to True.
|
watch_css (bool, optional): Watch CSS for changes. Defaults to False.
|
||||||
"""
|
"""
|
||||||
# N.B. This must be done *before* we call the parent constructor, because MessagePump's
|
# N.B. This must be done *before* we call the parent constructor, because MessagePump's
|
||||||
# constructor instantiates a `asyncio.PriorityQueue` and in Python versions older than 3.10
|
# constructor instantiates a `asyncio.PriorityQueue` and in Python versions older than 3.10
|
||||||
@@ -172,18 +172,20 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
self._require_styles_update = False
|
self._require_styles_update = False
|
||||||
|
|
||||||
self.css_path = css_path
|
self.css_path = css_path
|
||||||
self.css_monitor = (
|
|
||||||
FileMonitor(css_path, self._on_css_change)
|
|
||||||
if (watch_css and css_path)
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", ""))
|
self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", ""))
|
||||||
|
|
||||||
self.registry: set[MessagePump] = set()
|
self.registry: set[MessagePump] = set()
|
||||||
self.devtools = DevtoolsClient()
|
self.devtools = DevtoolsClient()
|
||||||
self._return_value: ReturnType | None = None
|
self._return_value: ReturnType | None = None
|
||||||
self._focus_timer: Timer | None = None
|
self._focus_timer: Timer | None = None
|
||||||
|
|
||||||
|
self.css_monitor = (
|
||||||
|
FileMonitor(css_path, self._on_css_change)
|
||||||
|
if ((watch_css or self.debug) and css_path)
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
title: Reactive[str] = Reactive("Textual")
|
title: Reactive[str] = Reactive("Textual")
|
||||||
@@ -636,9 +638,6 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
async def process_messages(self) -> None:
|
async def process_messages(self) -> None:
|
||||||
self._set_active()
|
self._set_active()
|
||||||
log("---")
|
|
||||||
log(f"driver={self.driver_class}")
|
|
||||||
log(f"asyncio running loop={asyncio.get_running_loop()!r}")
|
|
||||||
|
|
||||||
if self.devtools_enabled:
|
if self.devtools_enabled:
|
||||||
try:
|
try:
|
||||||
@@ -646,6 +645,12 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
self.log(f"Connected to devtools ({self.devtools.url})")
|
self.log(f"Connected to devtools ({self.devtools.url})")
|
||||||
except DevtoolsConnectionError:
|
except DevtoolsConnectionError:
|
||||||
self.log(f"Couldn't connect to devtools ({self.devtools.url})")
|
self.log(f"Couldn't connect to devtools ({self.devtools.url})")
|
||||||
|
|
||||||
|
self.log("---")
|
||||||
|
self.log(driver=self.driver_class)
|
||||||
|
self.log(loop=asyncio.get_running_loop())
|
||||||
|
self.log(features=self.features)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.css_path is not None:
|
if self.css_path is not None:
|
||||||
self.stylesheet.read(self.css_path)
|
self.stylesheet.read(self.css_path)
|
||||||
@@ -666,8 +671,6 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
try:
|
try:
|
||||||
load_event = events.Load(sender=self)
|
load_event = events.Load(sender=self)
|
||||||
await self.dispatch_message(load_event)
|
await self.dispatch_message(load_event)
|
||||||
# Wait for the load event to be processed, so we don't go in to application mode beforehand
|
|
||||||
# await load_event.wait()
|
|
||||||
|
|
||||||
driver = self._driver = self.driver_class(self.console, self)
|
driver = self._driver = self.driver_class(self.console, self)
|
||||||
driver.start_application_mode()
|
driver.start_application_mode()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def get_box_model(
|
|||||||
container: Size,
|
container: Size,
|
||||||
viewport: Size,
|
viewport: Size,
|
||||||
get_content_width: Callable[[Size, Size], int],
|
get_content_width: Callable[[Size, Size], int],
|
||||||
get_content_height: Callable[[Size, Size], int],
|
get_content_height: Callable[[Size, Size, int], int],
|
||||||
) -> BoxModel:
|
) -> BoxModel:
|
||||||
"""Resolve the box model for this Styles.
|
"""Resolve the box model for this Styles.
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ def get_box_model(
|
|||||||
if not has_rule("height"):
|
if not has_rule("height"):
|
||||||
height = container.height
|
height = container.height
|
||||||
elif styles.height.is_auto:
|
elif styles.height.is_auto:
|
||||||
height = get_content_height(container, viewport)
|
height = get_content_height(container, viewport, width)
|
||||||
if not is_content_box:
|
if not is_content_box:
|
||||||
height += gutter.height
|
height += gutter.height
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -376,8 +376,11 @@ class Region(NamedTuple):
|
|||||||
"""
|
"""
|
||||||
x1, y1, x2, y2 = self.corners
|
x1, y1, x2, y2 = self.corners
|
||||||
ox, oy, ox2, oy2 = other.corners
|
ox, oy, ox2, oy2 = other.corners
|
||||||
return (x2 >= ox >= x1 and y2 >= oy >= y1) and (
|
return (
|
||||||
x2 >= ox2 >= x1 and y2 >= oy2 >= y1
|
(x2 >= ox >= x1)
|
||||||
|
and (y2 >= oy >= y1)
|
||||||
|
and (x2 >= ox2 >= x1)
|
||||||
|
and (y2 >= oy2 >= y1)
|
||||||
)
|
)
|
||||||
|
|
||||||
def translate(self, x: int = 0, y: int = 0) -> Region:
|
def translate(self, x: int = 0, y: int = 0) -> Region:
|
||||||
|
|||||||
@@ -145,14 +145,40 @@ class Widget(DOMNode):
|
|||||||
)
|
)
|
||||||
return box_model
|
return box_model
|
||||||
|
|
||||||
def get_content_width(self, container_size: Size, parent_size: Size) -> int:
|
def get_content_width(self, container_size: Size, viewport_size: Size) -> int:
|
||||||
|
"""Gets the width of the content area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_size (Size): Size of the container (immediate parent) widget.
|
||||||
|
viewport_size (Size): Size of the viewport.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The optimal width of the content.
|
||||||
|
"""
|
||||||
console = self.app.console
|
console = self.app.console
|
||||||
renderable = self.render()
|
renderable = self.render()
|
||||||
measurement = Measurement.get(console, console.options, renderable)
|
measurement = Measurement.get(console, console.options, renderable)
|
||||||
return measurement.maximum
|
return measurement.maximum
|
||||||
|
|
||||||
def get_content_height(self, container_size: Size, parent_size: Size) -> int:
|
def get_content_height(
|
||||||
return container_size.height
|
self, container_size: Size, viewport_size: Size, width: int
|
||||||
|
) -> int:
|
||||||
|
"""Gets the height (number of lines) in the content area.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
container_size (Size): Size of the container (immediate parent) widget.
|
||||||
|
viewport_size (Size): Size of the viewport.
|
||||||
|
width (int): Width of renderable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The height of the content.
|
||||||
|
"""
|
||||||
|
renderable = self.render()
|
||||||
|
options = self.console.options.update_width(width)
|
||||||
|
segments = self.console.render(renderable, options)
|
||||||
|
# Cheaper than counting the lines returned from render_lines!
|
||||||
|
height = sum(text.count("\n") for text, _, _ in segments)
|
||||||
|
return height
|
||||||
|
|
||||||
async def watch_scroll_x(self, new_value: float) -> None:
|
async def watch_scroll_x(self, new_value: float) -> None:
|
||||||
self.horizontal_scrollbar.position = int(new_value)
|
self.horizontal_scrollbar.position = int(new_value)
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from textual.app import App
|
||||||
from textual.css.errors import StyleValueError
|
from textual.css.errors import StyleValueError
|
||||||
from textual.css.scalar import Scalar, Unit
|
from textual.geometry import Size
|
||||||
from textual.widget import Widget
|
from textual.widget import Widget
|
||||||
|
|
||||||
|
|
||||||
@@ -32,3 +33,35 @@ def test_widget_set_visible_invalid_string():
|
|||||||
widget.visible = "nope! no widget for me!"
|
widget.visible = "nope! no widget for me!"
|
||||||
|
|
||||||
assert widget.visible
|
assert widget.visible
|
||||||
|
|
||||||
|
|
||||||
|
def test_widget_content_width():
|
||||||
|
class TextWidget(Widget):
|
||||||
|
def __init__(self, text: str, id: str) -> None:
|
||||||
|
self.text = text
|
||||||
|
super().__init__(id=id)
|
||||||
|
|
||||||
|
def render(self) -> str:
|
||||||
|
return self.text
|
||||||
|
|
||||||
|
widget1 = TextWidget("foo", id="widget1")
|
||||||
|
widget2 = TextWidget("foo\nbar", id="widget2")
|
||||||
|
widget3 = TextWidget("foo\nbar\nbaz", id="widget3")
|
||||||
|
|
||||||
|
app = App()
|
||||||
|
app._set_active()
|
||||||
|
|
||||||
|
width = widget1.get_content_width(Size(20, 20), Size(80, 24))
|
||||||
|
height = widget1.get_content_height(Size(20, 20), Size(80, 24), width)
|
||||||
|
assert width == 3
|
||||||
|
assert height == 1
|
||||||
|
|
||||||
|
width = widget2.get_content_width(Size(20, 20), Size(80, 24))
|
||||||
|
height = widget2.get_content_height(Size(20, 20), Size(80, 24), width)
|
||||||
|
assert width == 3
|
||||||
|
assert height == 2
|
||||||
|
|
||||||
|
width = widget3.get_content_width(Size(20, 20), Size(80, 24))
|
||||||
|
height = widget3.get_content_height(Size(20, 20), Size(80, 24), width)
|
||||||
|
assert width == 3
|
||||||
|
assert height == 3
|
||||||
|
|||||||
Reference in New Issue
Block a user