Merge pull request #476 from Textualize/auto-height

added auto height
This commit is contained in:
Will McGugan
2022-05-05 15:10:18 +01:00
committed by GitHub
8 changed files with 98 additions and 30 deletions

View File

@@ -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;

View File

@@ -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()

View File

@@ -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()

View File

@@ -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()

View File

@@ -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:

View File

@@ -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:

View File

@@ -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)

View File

@@ -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