Skip to content

Commit

Permalink
Optimizations (#3837)
Browse files Browse the repository at this point in the history
* optimize layout

* optimization

* test fix

* startup optimization

* simplify

* faster content height
  • Loading branch information
willmcgugan authored Dec 9, 2023
1 parent 82d6e3e commit 5eea51c
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 36 deletions.
15 changes: 13 additions & 2 deletions src/textual/_callback.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import asyncio
from functools import lru_cache
from functools import lru_cache, partial
from inspect import isawaitable, signature
from typing import TYPE_CHECKING, Any, Callable

Expand All @@ -14,8 +14,19 @@
INVOKE_TIMEOUT_WARNING = 3


@lru_cache(maxsize=2048)
def count_parameters(func: Callable) -> int:
"""Count the number of parameters in a callable"""
if isinstance(func, partial):
return _count_parameters(func.func) + len(func.args)
if hasattr(func, "__self__"):
# Bound method
func = func.__func__ # type: ignore
return _count_parameters(func) - 1
return _count_parameters(func)


@lru_cache(maxsize=2048)
def _count_parameters(func: Callable) -> int:
"""Count the number of parameters in a callable"""
return len(signature(func).parameters)

Expand Down
5 changes: 2 additions & 3 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -829,8 +829,8 @@ def watch_dark(self, dark: bool) -> None:
This method handles the transition between light and dark mode when you
change the [dark][textual.app.App.dark] attribute.
"""
self.set_class(dark, "-dark-mode")
self.set_class(not dark, "-light-mode")
self.set_class(dark, "-dark-mode", update=False)
self.set_class(not dark, "-light-mode", update=False)
self.call_later(self.refresh_css)

def get_driver_class(self) -> Type[Driver]:
Expand Down Expand Up @@ -1586,7 +1586,6 @@ def update_styles(self, node: DOMNode) -> None:
will be added, and this method is called to apply the corresponding
:hover styles.
"""

descendants = node.walk_children(with_self=True)
self.stylesheet.update_nodes(descendants, animate=True)

Expand Down
2 changes: 1 addition & 1 deletion src/textual/css/_style_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ def __get__(self, obj: StylesBase, objtype: type[StylesBase] | None = None) -> s
Returns:
The string property value.
"""
return cast(str, obj.get_rule(self.name, self._default))
return obj.get_rule(self.name, self._default) # type: ignore

def _before_refresh(self, obj: StylesBase, value: str | None) -> None:
"""Do any housekeeping before asking for a layout refresh after a value change."""
Expand Down
28 changes: 17 additions & 11 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -1176,19 +1176,20 @@ def has_class(self, *class_names: str) -> bool:
"""
return self._classes.issuperset(class_names)

def set_class(self, add: bool, *class_names: str) -> Self:
def set_class(self, add: bool, *class_names: str, update: bool = True) -> Self:
"""Add or remove class(es) based on a condition.
Args:
add: Add the classes if True, otherwise remove them.
update: Also update styles.
Returns:
Self.
"""
if add:
self.add_class(*class_names)
self.add_class(*class_names, update=update)
else:
self.remove_class(*class_names)
self.remove_class(*class_names, update=update)
return self

def set_classes(self, classes: str | Iterable[str]) -> Self:
Expand All @@ -1209,16 +1210,18 @@ def _update_styles(self) -> None:
Should be called whenever CSS classes / pseudo classes change.
"""
try:
self.app.update_styles(self)
except NoActiveAppError:
pass
if self._is_mounted:
try:
self.app.update_styles(self)
except NoActiveAppError:
pass

def add_class(self, *class_names: str) -> Self:
def add_class(self, *class_names: str, update: bool = True) -> Self:
"""Add class names to this Node.
Args:
*class_names: CSS class names to add.
update: Also update styles.
Returns:
Self.
Expand All @@ -1228,14 +1231,16 @@ def add_class(self, *class_names: str) -> Self:
self._classes.update(class_names)
if old_classes == self._classes:
return self
self._update_styles()
if update:
self._update_styles()
return self

def remove_class(self, *class_names: str) -> Self:
def remove_class(self, *class_names: str, update: bool = True) -> Self:
"""Remove class names from this Node.
Args:
*class_names: CSS class names to remove.
update: Also update styles.
Returns:
Self.
Expand All @@ -1245,7 +1250,8 @@ def remove_class(self, *class_names: str) -> Self:
self._classes.difference_update(class_names)
if old_classes == self._classes:
return self
self._update_styles()
if update:
self._update_styles()
return self

def toggle_class(self, *class_names: str) -> Self:
Expand Down
22 changes: 16 additions & 6 deletions src/textual/layouts/horizontal.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,26 @@ def arrange(

_Region = Region
_WidgetPlacement = WidgetPlacement
for widget, box_model, margin in zip(children, box_models, margins):
for widget, (content_width, content_height, box_margin), margin in zip(
children, box_models, margins
):
overlay = widget.styles.overlay == "screen"
content_width, content_height, box_margin = box_model
offset_y = box_margin.top
next_x = x + content_width
region = _Region(
int(x), offset_y, int(next_x - int(x)), int(content_height)
)
add_placement(
_WidgetPlacement(region, box_model.margin, widget, 0, False, overlay)
_WidgetPlacement(
_Region(
int(x),
offset_y,
int(next_x - int(x)),
int(content_height),
),
box_margin,
widget,
0,
False,
overlay,
)
)
if not overlay:
x = next_x + margin
Expand Down
24 changes: 16 additions & 8 deletions src/textual/layouts/vertical.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,25 @@ def arrange(

_Region = Region
_WidgetPlacement = WidgetPlacement

for widget, box_model, margin in zip(children, box_models, margins):
for widget, (content_width, content_height, box_margin), margin in zip(
children, box_models, margins
):
overlay = widget.styles.overlay == "screen"
content_width, content_height, box_margin = box_model
next_y = y + content_height

region = _Region(
box_margin.left, int(y), int(content_width), int(next_y) - int(y)
)
add_placement(
_WidgetPlacement(region, box_model.margin, widget, 0, False, overlay)
_WidgetPlacement(
_Region(
box_margin.left,
int(y),
int(content_width),
int(next_y) - int(y),
),
box_margin,
widget,
0,
False,
overlay,
)
)
if not overlay:
y = next_y + margin
Expand Down
20 changes: 16 additions & 4 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -1180,10 +1180,22 @@ def get_content_height(self, container: Size, viewport: Size, width: int) -> int
return self._content_height_cache[1]

renderable = self.render()
options = self._console.options.update_width(width).update(highlight=False)
segments = self._console.render(renderable, options)
# Cheaper than counting the lines returned from render_lines!
height = sum([text.count("\n") for text, _, _ in segments])
if isinstance(renderable, Text):
height = len(
renderable.wrap(
self._console,
width,
no_wrap=renderable.no_wrap,
tab_size=renderable.tab_size or 8,
)
)
else:
options = self._console.options.update_width(width).update(
highlight=False
)
segments = self._console.render(renderable, options)
# Cheaper than counting the lines returned from render_lines!
height = sum([text.count("\n") for text, _, _ in segments])
self._content_height_cache = (cache_key, height)

return height
Expand Down
2 changes: 1 addition & 1 deletion src/textual/widgets/_collapsible.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def _update_collapsed(self, collapsed: bool) -> None:
except NoMatches:
pass

def _on_mount(self) -> None:
def _on_mount(self, event: events.Mount) -> None:
"""Initialise collapsed state."""
self._update_collapsed(self.collapsed)

Expand Down
5 changes: 5 additions & 0 deletions tests/test_arrange.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from textual._arrange import TOP_Z, arrange
from textual._layout import WidgetPlacement
from textual.app import App
from textual.geometry import Region, Size, Spacing
from textual.widget import Widget

Expand All @@ -16,6 +17,7 @@ def test_arrange_empty():

def test_arrange_dock_top():
container = Widget(id="container")
container._parent = App()
child = Widget(id="child")
header = Widget(id="header")
header.styles.dock = "top"
Expand All @@ -34,6 +36,7 @@ def test_arrange_dock_top():

def test_arrange_dock_left():
container = Widget(id="container")
container._parent = App()
child = Widget(id="child")
header = Widget(id="header")
header.styles.dock = "left"
Expand All @@ -51,6 +54,7 @@ def test_arrange_dock_left():

def test_arrange_dock_right():
container = Widget(id="container")
container._parent = App()
child = Widget(id="child")
header = Widget(id="header")
header.styles.dock = "right"
Expand All @@ -68,6 +72,7 @@ def test_arrange_dock_right():

def test_arrange_dock_bottom():
container = Widget(id="container")
container._parent = App()
child = Widget(id="child")
header = Widget(id="header")
header.styles.dock = "bottom"
Expand Down

0 comments on commit 5eea51c

Please sign in to comment.