From bc497e0abe4b69d8eb9c5271e3b419cbbbfc2892 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 5 May 2022 14:38:22 +0100 Subject: [PATCH 1/4] added auto height --- sandbox/align.css | 5 ++--- sandbox/align.py | 16 ++++++++++------ sandbox/uber.py | 4 +++- src/textual/app.py | 27 +++++++++++++++------------ src/textual/box_model.py | 6 ++++-- src/textual/geometry.py | 7 +++++-- src/textual/widget.py | 32 +++++++++++++++++++++++++++++--- tests/test_widget.py | 35 ++++++++++++++++++++++++++++++++++- 8 files changed, 102 insertions(+), 30 deletions(-) diff --git a/sandbox/align.css b/sandbox/align.css index f976925ed..f61901c48 100644 --- a/sandbox/align.css +++ b/sandbox/align.css @@ -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; diff --git a/sandbox/align.py b/sandbox/align.py index 19cd4a933..f3f18c93d 100644 --- a/sandbox/align.py +++ b/sandbox/align.py @@ -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() diff --git a/sandbox/uber.py b/sandbox/uber.py index 18edb4dbd..5dc1c20fa 100644 --- a/sandbox/uber.py +++ b/sandbox/uber.py @@ -76,7 +76,9 @@ 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() diff --git a/src/textual/app.py b/src/textual/app.py index 9005634c6..4ee2f63ff 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -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() diff --git a/src/textual/box_model.py b/src/textual/box_model.py index 5fc434a07..e96f88940 100644 --- a/src/textual/box_model.py +++ b/src/textual/box_model.py @@ -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,9 @@ def get_box_model( if not has_rule("height"): height = container.height elif styles.height.is_auto: - height = get_content_height(container, viewport) + print("get_content_height", container, viewport, width) + height = get_content_height(container, viewport, width) + print("height", height) if not is_content_box: height += gutter.height else: diff --git a/src/textual/geometry.py b/src/textual/geometry.py index a1d9ba962..324707178 100644 --- a/src/textual/geometry.py +++ b/src/textual/geometry.py @@ -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: diff --git a/src/textual/widget.py b/src/textual/widget.py index 7bd885d9c..ce7300961 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -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(segment.text.count("\n") for segment in segments) + return height async def watch_scroll_x(self, new_value: float) -> None: self.horizontal_scrollbar.position = int(new_value) diff --git a/tests/test_widget.py b/tests/test_widget.py index abc92039c..341b7965d 100644 --- a/tests/test_widget.py +++ b/tests/test_widget.py @@ -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 From afee5c0ed1df08b5f38a24c0082f3392be03c6be Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 5 May 2022 14:39:54 +0100 Subject: [PATCH 2/4] simplify --- sandbox/uber.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sandbox/uber.py b/sandbox/uber.py index 5dc1c20fa..b8dcbffc4 100644 --- a/sandbox/uber.py +++ b/sandbox/uber.py @@ -76,9 +76,7 @@ class BasicApp(App): self.focused.styles.border_top = ("solid", "invalid-color") -app = BasicApp( - css_path="uber.css", -) +app = BasicApp(css_path="uber.css") if __name__ == "__main__": app.run() From 190a802eef2b5c53e6eaec9088c29148439e595b Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 5 May 2022 14:41:13 +0100 Subject: [PATCH 3/4] removed prints --- src/textual/box_model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/textual/box_model.py b/src/textual/box_model.py index e96f88940..fd6c3ed5b 100644 --- a/src/textual/box_model.py +++ b/src/textual/box_model.py @@ -53,9 +53,7 @@ def get_box_model( if not has_rule("height"): height = container.height elif styles.height.is_auto: - print("get_content_height", container, viewport, width) height = get_content_height(container, viewport, width) - print("height", height) if not is_content_box: height += gutter.height else: From 19220404114435cf012aa6d74ba3032d35844e49 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 5 May 2022 14:45:30 +0100 Subject: [PATCH 4/4] tweak --- src/textual/widget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/textual/widget.py b/src/textual/widget.py index ce7300961..91bcd0005 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -177,7 +177,7 @@ class Widget(DOMNode): options = self.console.options.update_width(width) segments = self.console.render(renderable, options) # Cheaper than counting the lines returned from render_lines! - height = sum(segment.text.count("\n") for segment in segments) + height = sum(text.count("\n") for text, _, _ in segments) return height async def watch_scroll_x(self, new_value: float) -> None: