mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
@@ -10,11 +10,10 @@ Widget {
|
||||
}
|
||||
|
||||
#thing {
|
||||
|
||||
width: auto;
|
||||
height: 10;
|
||||
height: auto;
|
||||
background:magenta;
|
||||
margin: 3;
|
||||
margin: 1;
|
||||
padding: 1;
|
||||
border: solid white;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
from rich.text import Text
|
||||
|
||||
from textual.app import App
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widget import Widget
|
||||
from textual.widgets import Static
|
||||
|
||||
|
||||
class Thing(Widget):
|
||||
def render(self):
|
||||
return Text.from_markup("Hello, World. [b magenta]Lorem impsum.")
|
||||
return "Hello, 3434 World.\n[b]Lorem impsum."
|
||||
|
||||
|
||||
class AlignApp(App):
|
||||
def on_load(self):
|
||||
self.bind("t", "log_tree")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self.log("MOUNTED")
|
||||
self.mount(thing=Thing(), thing2=Static("0123456789"), thing3=Widget())
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Thing(id="thing")
|
||||
yield Static("foo", id="thing2")
|
||||
yield Widget(id="thing3")
|
||||
|
||||
def action_log_tree(self):
|
||||
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")
|
||||
|
||||
|
||||
app = BasicApp(css_path="uber.css", log_path="textual.log", log_verbosity=1)
|
||||
app = BasicApp(css_path="uber.css")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
|
||||
@@ -110,7 +110,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
log_verbosity: int = 1,
|
||||
title: str = "Textual Application",
|
||||
css_path: str | PurePath | None = None,
|
||||
watch_css: bool = True,
|
||||
watch_css: bool = False,
|
||||
):
|
||||
"""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.
|
||||
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.
|
||||
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
|
||||
# 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.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.registry: set[MessagePump] = set()
|
||||
self.devtools = DevtoolsClient()
|
||||
self._return_value: ReturnType | 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__()
|
||||
|
||||
title: Reactive[str] = Reactive("Textual")
|
||||
@@ -636,9 +638,6 @@ class App(Generic[ReturnType], DOMNode):
|
||||
|
||||
async def process_messages(self) -> None:
|
||||
self._set_active()
|
||||
log("---")
|
||||
log(f"driver={self.driver_class}")
|
||||
log(f"asyncio running loop={asyncio.get_running_loop()!r}")
|
||||
|
||||
if self.devtools_enabled:
|
||||
try:
|
||||
@@ -646,6 +645,12 @@ class App(Generic[ReturnType], DOMNode):
|
||||
self.log(f"Connected to devtools ({self.devtools.url})")
|
||||
except DevtoolsConnectionError:
|
||||
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:
|
||||
if self.css_path is not None:
|
||||
self.stylesheet.read(self.css_path)
|
||||
@@ -666,8 +671,6 @@ class App(Generic[ReturnType], DOMNode):
|
||||
try:
|
||||
load_event = events.Load(sender=self)
|
||||
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.start_application_mode()
|
||||
|
||||
@@ -18,7 +18,7 @@ def get_box_model(
|
||||
container: Size,
|
||||
viewport: Size,
|
||||
get_content_width: Callable[[Size, Size], int],
|
||||
get_content_height: Callable[[Size, Size], int],
|
||||
get_content_height: Callable[[Size, Size, int], int],
|
||||
) -> BoxModel:
|
||||
"""Resolve the box model for this Styles.
|
||||
|
||||
@@ -53,7 +53,7 @@ def get_box_model(
|
||||
if not has_rule("height"):
|
||||
height = container.height
|
||||
elif styles.height.is_auto:
|
||||
height = get_content_height(container, viewport)
|
||||
height = get_content_height(container, viewport, width)
|
||||
if not is_content_box:
|
||||
height += gutter.height
|
||||
else:
|
||||
|
||||
@@ -376,8 +376,11 @@ class Region(NamedTuple):
|
||||
"""
|
||||
x1, y1, x2, y2 = self.corners
|
||||
ox, oy, ox2, oy2 = other.corners
|
||||
return (x2 >= ox >= x1 and y2 >= oy >= y1) and (
|
||||
x2 >= ox2 >= x1 and y2 >= oy2 >= y1
|
||||
return (
|
||||
(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:
|
||||
|
||||
@@ -145,14 +145,40 @@ class Widget(DOMNode):
|
||||
)
|
||||
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
|
||||
renderable = self.render()
|
||||
measurement = Measurement.get(console, console.options, renderable)
|
||||
return measurement.maximum
|
||||
|
||||
def get_content_height(self, container_size: Size, parent_size: Size) -> int:
|
||||
return container_size.height
|
||||
def get_content_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:
|
||||
self.horizontal_scrollbar.position = int(new_value)
|
||||
|
||||
@@ -3,8 +3,9 @@ from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
|
||||
from textual.app import App
|
||||
from textual.css.errors import StyleValueError
|
||||
from textual.css.scalar import Scalar, Unit
|
||||
from textual.geometry import Size
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
@@ -32,3 +33,35 @@ def test_widget_set_visible_invalid_string():
|
||||
widget.visible = "nope! no widget for me!"
|
||||
|
||||
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