input docs an exampels

This commit is contained in:
Will McGugan
2022-09-24 17:04:00 +01:00
parent a41270f7d7
commit 53d8e02d0d
23 changed files with 322 additions and 45 deletions

View File

@@ -7,6 +7,7 @@ The `Key` event is sent to a widget when the user presses a key on the keyboard.
## Attributes ## Attributes
| attribute | type | purpose | | attribute | type | purpose |
| --------- | ---- | ------------------------ | | --------- | ----------- | ----------------------------------------------------------- |
| `key` | str | The key that was pressed | | `key` | str | Name of the key that was pressed. |
| `char` | str or None | The character that was pressed, or None it isn't printable. |

View File

@@ -2,7 +2,7 @@
The `MouseMove` event is sent to a widget when the mouse pointer is moved over a widget. The `MouseMove` event is sent to a widget when the mouse pointer is moved over a widget.
- [x] Bubbles - [ ] Bubbles
- [x] Verbose - [x] Verbose
## Attributes ## Attributes

View File

@@ -0,0 +1,21 @@
from textual.app import App, ComposeResult
from textual.widgets import TextLog
from textual import events
class InputApp(App):
"""App to display key events."""
def compose(self) -> ComposeResult:
yield TextLog()
def on_key(self, event: events.Key) -> None:
self.query_one(TextLog).write(event)
def key_space(self) -> None:
self.bell()
if __name__ == "__main__":
app = InputApp()
app.run()

View File

@@ -0,0 +1,12 @@
Screen {
layout: horizontal;
}
KeyLogger {
width: 1fr;
border: blank;
}
KeyLogger:focus {
border: wide $accent;
}

View File

@@ -0,0 +1,23 @@
from textual.app import App, ComposeResult
from textual.widgets import TextLog
from textual import events
class KeyLogger(TextLog):
def on_key(self, event: events.Key) -> None:
self.write(event)
class InputApp(App):
"""App to display key events."""
CSS_PATH = "input02.css"
def compose(self) -> ComposeResult:
yield KeyLogger()
yield KeyLogger()
if __name__ == "__main__":
app = InputApp()
app.run()

View File

@@ -0,0 +1,24 @@
Screen {
layers: log ball;
}
TextLog {
layer: log;
}
PlayArea {
background: transparent;
layer: ball;
}
Ball {
layer: ball;
width: auto;
height: 1;
background: $secondary;
border: tall $secondary;
color: $background;
box-sizing: content-box;
text-style: bold;
padding: 0 4;
}

View File

@@ -0,0 +1,31 @@
from textual.app import App, ComposeResult
from textual import events
from textual.layout import Container
from textual.widgets import Static, TextLog
class PlayArea(Container):
def on_mount(self) -> None:
self.capture_mouse()
def on_mouse_move(self, event: events.MouseMove) -> None:
self.screen.query_one(TextLog).write(event)
self.query_one(Ball).offset = event.offset - (8, 2)
class Ball(Static):
pass
class MouseApp(App):
CSS_PATH = "mouse01.css"
def compose(self) -> ComposeResult:
yield TextLog()
yield PlayArea(Ball("Textual"))
if __name__ == "__main__":
app = MouseApp()
app.run()

26
docs/guide/input.md Normal file
View File

@@ -0,0 +1,26 @@
# Input
This chapter will discuss how to make your app respond to input in the form of key presses and mouse actions.
!!! quote
More Input!
— Johnny Five
## Key events
The most fundamental way to receive input in via [Key](./events/key) events. Let's take a closer look at key events with an app that will display key events as you type.
=== "key01.py"
```python title="key01.py" hl_lines="12-13"
--8<-- "docs/examples/guide/input/key01.py"
```
=== "Output"
```{.textual path="docs/examples/guide/input/key01.py", press="T,e,x,t,u,a,l,!,_"}
```
Note the key event handler which

View File

@@ -14,6 +14,7 @@ nav:
- "guide/CSS.md" - "guide/CSS.md"
- "guide/layout.md" - "guide/layout.md"
- "guide/events.md" - "guide/events.md"
- "guide/input.md"
- "guide/actions.md" - "guide/actions.md"
- "guide/reactivity.md" - "guide/reactivity.md"
- "guide/widgets.md" - "guide/widgets.md"

View File

@@ -276,7 +276,12 @@ class Animator:
if duration is not None: if duration is not None:
animation_duration = duration animation_duration = duration
else: else:
animation_duration = abs(value - start_value) / (speed or 50) if hasattr(value, "get_distance_to"):
animation_duration = value.get_distance_to(start_value) / (
speed or 50
)
else:
animation_duration = abs(value - start_value) / (speed or 50)
animation = SimpleAnimation( animation = SimpleAnimation(
obj, obj,

View File

@@ -501,7 +501,18 @@ class Compositor:
raise errors.NoWidget("Widget is not in layout") raise errors.NoWidget("Widget is not in layout")
def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]: def get_widget_at(self, x: int, y: int) -> tuple[Widget, Region]:
"""Get the widget under the given point or None.""" """Get the widget under a given coordinate.
Args:
x (int): X Coordinate.
y (int): Y Coordinate.
Raises:
errors.NoWidget: If there is not widget underneath (x, y).
Returns:
tuple[Widget, Region]: A tuple of the widget and its region.
"""
# TODO: Optimize with some line based lookup # TODO: Optimize with some line based lookup
contains = Region.contains contains = Region.contains
for widget, cropped_region, region, *_ in self: for widget, cropped_region, region, *_ in self:
@@ -509,6 +520,21 @@ class Compositor:
return widget, region return widget, region
raise errors.NoWidget(f"No widget under screen coordinate ({x}, {y})") raise errors.NoWidget(f"No widget under screen coordinate ({x}, {y})")
def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]:
"""Get all widgets under a given coordinate.
Args:
x (int): X coordinate.
y (int): Y coordinate.
Returns:
Iterable[tuple[Widget, Region]]: Sequence of (WIDGET, REGION) tuples.
"""
contains = Region.contains
for widget, cropped_region, region, *_ in self:
if contains(cropped_region, x, y) and widget.visible:
yield widget, region
def get_style_at(self, x: int, y: int) -> Style: def get_style_at(self, x: int, y: int) -> Style:
"""Get the Style at the given cell or Style.null() """Get the Style at the given cell or Style.null()

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
import unicodedata
import re import re
from typing import Any, Callable, Generator, Iterable from typing import Any, Callable, Generator, Iterable
@@ -228,7 +229,9 @@ class XTermParser(Parser[events.Event]):
for event in sequence_to_key_events(character): for event in sequence_to_key_events(character):
on_token(event) on_token(event)
def _sequence_to_key_events(self, sequence: str) -> Iterable[events.Key]: def _sequence_to_key_events(
self, sequence: str, _unicode_name=unicodedata.name
) -> Iterable[events.Key]:
"""Map a sequence of code points on to a sequence of keys. """Map a sequence of code points on to a sequence of keys.
Args: Args:
@@ -246,4 +249,16 @@ class XTermParser(Parser[events.Event]):
self.sender, key.value, sequence if len(sequence) == 1 else None self.sender, key.value, sequence if len(sequence) == 1 else None
) )
elif len(sequence) == 1: elif len(sequence) == 1:
yield events.Key(self.sender, sequence, sequence) try:
if not sequence.isalnum():
name = (
_unicode_name(sequence)
.lower()
.replace("-", "_")
.replace(" ", "_")
)
else:
name = sequence
yield events.Key(self.sender, name, sequence)
except:
yield events.Key(self.sender, sequence, sequence)

View File

@@ -1447,7 +1447,8 @@ class App(Generic[ReturnType], DOMNode):
elif event.key == "shift+tab": elif event.key == "shift+tab":
self.focus_previous() self.focus_previous()
else: else:
await self.press(event.key) if not (await self.press(event.key)):
await self.dispatch_key(event)
async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None: async def _on_shutdown_request(self, event: events.ShutdownRequest) -> None:
log("shutdown request") log("shutdown request")

View File

@@ -16,7 +16,7 @@ a method which evaluates the query, such as first() and last().
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Iterator, TypeVar, overload from typing import Generic, TYPE_CHECKING, Iterator, TypeVar, overload
import rich.repr import rich.repr
@@ -42,8 +42,11 @@ class WrongType(QueryError):
pass pass
QueryType = TypeVar("QueryType", bound="Widget")
@rich.repr.auto(angular=True) @rich.repr.auto(angular=True)
class DOMQuery: class DOMQuery(Generic[QueryType]):
__slots__ = [ __slots__ = [
"_node", "_node",
"_nodes", "_nodes",
@@ -78,7 +81,7 @@ class DOMQuery:
return self._node return self._node
@property @property
def nodes(self) -> list[Widget]: def nodes(self) -> list[QueryType]:
"""Lazily evaluate nodes.""" """Lazily evaluate nodes."""
from ..widget import Widget from ..widget import Widget
@@ -103,21 +106,21 @@ class DOMQuery:
"""True if non-empty, otherwise False.""" """True if non-empty, otherwise False."""
return bool(self.nodes) return bool(self.nodes)
def __iter__(self) -> Iterator[Widget]: def __iter__(self) -> Iterator[QueryType]:
return iter(self.nodes) return iter(self.nodes)
def __reversed__(self) -> Iterator[Widget]: def __reversed__(self) -> Iterator[QueryType]:
return reversed(self.nodes) return reversed(self.nodes)
@overload @overload
def __getitem__(self, index: int) -> Widget: def __getitem__(self, index: int) -> QueryType:
... ...
@overload @overload
def __getitem__(self, index: slice) -> list[Widget]: def __getitem__(self, index: slice) -> list[QueryType]:
... ...
def __getitem__(self, index: int | slice) -> Widget | list[Widget]: def __getitem__(self, index: int | slice) -> QueryType | list[QueryType]:
return self.nodes[index] return self.nodes[index]
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
@@ -133,7 +136,7 @@ class DOMQuery:
for selectors in self._excludes for selectors in self._excludes
) )
def filter(self, selector: str) -> DOMQuery: def filter(self, selector: str) -> DOMQuery[QueryType]:
"""Filter this set by the given CSS selector. """Filter this set by the given CSS selector.
Args: Args:
@@ -145,7 +148,7 @@ class DOMQuery:
return DOMQuery(self.node, filter=selector, parent=self) return DOMQuery(self.node, filter=selector, parent=self)
def exclude(self, selector: str) -> DOMQuery: def exclude(self, selector: str) -> DOMQuery[QueryType]:
"""Exclude nodes that match a given selector. """Exclude nodes that match a given selector.
Args: Args:
@@ -166,7 +169,9 @@ class DOMQuery:
def first(self, expect_type: type[ExpectType]) -> ExpectType: def first(self, expect_type: type[ExpectType]) -> ExpectType:
... ...
def first(self, expect_type: type[ExpectType] | None = None) -> Widget | ExpectType: def first(
self, expect_type: type[ExpectType] | None = None
) -> QueryType | ExpectType:
"""Get the *first* match node. """Get the *first* match node.
Args: Args:
@@ -199,7 +204,9 @@ class DOMQuery:
def last(self, expect_type: type[ExpectType]) -> ExpectType: def last(self, expect_type: type[ExpectType]) -> ExpectType:
... ...
def last(self, expect_type: type[ExpectType] | None = None) -> Widget | ExpectType: def last(
self, expect_type: type[ExpectType] | None = None
) -> QueryType | ExpectType:
"""Get the *last* match node. """Get the *last* match node.
Args: Args:
@@ -251,7 +258,7 @@ class DOMQuery:
if isinstance(node, filter_type): if isinstance(node, filter_type):
yield node yield node
def set_class(self, add: bool, *class_names: str) -> DOMQuery: def set_class(self, add: bool, *class_names: str) -> DOMQuery[QueryType]:
"""Set the given class name(s) according to a condition. """Set the given class name(s) according to a condition.
Args: Args:
@@ -264,31 +271,33 @@ class DOMQuery:
node.set_class(add, *class_names) node.set_class(add, *class_names)
return self return self
def add_class(self, *class_names: str) -> DOMQuery: def add_class(self, *class_names: str) -> DOMQuery[QueryType]:
"""Add the given class name(s) to nodes.""" """Add the given class name(s) to nodes."""
for node in self: for node in self:
node.add_class(*class_names) node.add_class(*class_names)
return self return self
def remove_class(self, *class_names: str) -> DOMQuery: def remove_class(self, *class_names: str) -> DOMQuery[QueryType]:
"""Remove the given class names from the nodes.""" """Remove the given class names from the nodes."""
for node in self: for node in self:
node.remove_class(*class_names) node.remove_class(*class_names)
return self return self
def toggle_class(self, *class_names: str) -> DOMQuery: def toggle_class(self, *class_names: str) -> DOMQuery[QueryType]:
"""Toggle the given class names from matched nodes.""" """Toggle the given class names from matched nodes."""
for node in self: for node in self:
node.toggle_class(*class_names) node.toggle_class(*class_names)
return self return self
def remove(self) -> DOMQuery: def remove(self) -> DOMQuery[QueryType]:
"""Remove matched nodes from the DOM""" """Remove matched nodes from the DOM"""
for node in self: for node in self:
node.remove() node.remove()
return self return self
def set_styles(self, css: str | None = None, **update_styles) -> DOMQuery: def set_styles(
self, css: str | None = None, **update_styles
) -> DOMQuery[QueryType]:
"""Set styles on matched nodes. """Set styles on matched nodes.
Args: Args:
@@ -308,7 +317,9 @@ class DOMQuery:
node.refresh(layout=True) node.refresh(layout=True)
return self return self
def refresh(self, *, repaint: bool = True, layout: bool = False) -> DOMQuery: def refresh(
self, *, repaint: bool = True, layout: bool = False
) -> DOMQuery[QueryType]:
"""Refresh matched nodes. """Refresh matched nodes.
Args: Args:

View File

@@ -327,6 +327,14 @@ class ScalarOffset(NamedTuple):
"""Get a null scalar offset (0, 0).""" """Get a null scalar offset (0, 0)."""
return NULL_SCALAR return NULL_SCALAR
@classmethod
def from_offset(cls, offset: tuple[int, int]) -> ScalarOffset:
x, y = offset
return cls(
Scalar(x, Unit.CELLS, Unit.WIDTH),
Scalar(y, Unit.CELLS, Unit.HEIGHT),
)
def __bool__(self) -> bool: def __bool__(self) -> bool:
x, y = self x, y = self
return bool(x.value or y.value) return bool(x.value or y.value)

View File

@@ -675,7 +675,17 @@ class DOMNode(MessagePump):
return child return child
raise NoMatchingNodesError(f"No child found with id={id!r}") raise NoMatchingNodesError(f"No child found with id={id!r}")
def query(self, selector: str | None = None) -> DOMQuery: ExpectType = TypeVar("ExpectType", bound="Widget")
@overload
def query(self, selector: str | None) -> DOMQuery:
...
@overload
def query(self, selector: type[ExpectType]) -> DOMQuery[ExpectType]:
...
def query(self, selector: str | type | None = None) -> DOMQuery:
"""Get a DOM query matching a selector. """Get a DOM query matching a selector.
Args: Args:
@@ -686,9 +696,13 @@ class DOMNode(MessagePump):
""" """
from .css.query import DOMQuery from .css.query import DOMQuery
return DOMQuery(self, filter=selector) query: str | None
if isinstance(selector, str) or selector is None:
query = selector
else:
query = selector.__name__
ExpectType = TypeVar("ExpectType") return DOMQuery(self, filter=query)
@overload @overload
def query_one(self, selector: str) -> Widget: def query_one(self, selector: str) -> Widget:
@@ -723,7 +737,7 @@ class DOMNode(MessagePump):
query_selector = selector query_selector = selector
else: else:
query_selector = selector.__name__ query_selector = selector.__name__
query = DOMQuery(self, filter=query_selector) query: DOMQuery[Widget] = DOMQuery(self, filter=query_selector)
if expect_type is None: if expect_type is None:
return query.first() return query.first()

View File

@@ -7,7 +7,6 @@ from rich.style import Style
from ._types import MessageTarget from ._types import MessageTarget
from .geometry import Offset, Size from .geometry import Offset, Size
from .keys import KEY_VALUES, Keys
from .message import Message from .message import Message
MouseEventT = TypeVar("MouseEventT", bound="MouseEvent") MouseEventT = TypeVar("MouseEventT", bound="MouseEvent")
@@ -317,6 +316,14 @@ class MouseEvent(InputEvent, bubble=True):
yield "meta", self.meta, False yield "meta", self.meta, False
yield "ctrl", self.ctrl, False yield "ctrl", self.ctrl, False
@property
def offset(self) -> Offset:
return Offset(self.x, self.y)
@property
def screen_offset(self) -> Offset:
return Offset(self.screen_x, self.screen_y)
@property @property
def style(self) -> Style: def style(self) -> Style:
return self._style or Style() return self._style or Style()
@@ -325,7 +332,7 @@ class MouseEvent(InputEvent, bubble=True):
def style(self, style: Style) -> None: def style(self, style: Style) -> None:
self._style = style self._style = style
def offset(self, x: int, y: int) -> MouseEvent: def _apply_offset(self, x: int, y: int) -> MouseEvent:
return self.__class__( return self.__class__(
self.sender, self.sender,
x=self.x + x, x=self.x + x,
@@ -343,7 +350,7 @@ class MouseEvent(InputEvent, bubble=True):
@rich.repr.auto @rich.repr.auto
class MouseMove(MouseEvent, bubble=True, verbose=True): class MouseMove(MouseEvent, bubble=False, verbose=True):
"""Sent when the mouse cursor moves.""" """Sent when the mouse cursor moves."""

View File

@@ -1,6 +1,6 @@
from __future__ import annotations from __future__ import annotations
from typing import ClassVar from typing import ClassVar, TYPE_CHECKING
import rich.repr import rich.repr
@@ -8,6 +8,9 @@ from . import _clock
from .case import camel_to_snake from .case import camel_to_snake
from ._types import MessageTarget as MessageTarget from ._types import MessageTarget as MessageTarget
if TYPE_CHECKING:
from .widget import Widget
@rich.repr.auto @rich.repr.auto
class Message: class Message:
@@ -109,3 +112,11 @@ class Message:
""" """
self._stop_propagation = stop self._stop_propagation = stop
return self return self
async def _bubble_to(self, widget: Widget) -> None:
"""Bubble to a widget (typically the parent).
Args:
widget (Widget): Target of bubble.
"""
await widget.post_message(self)

View File

@@ -417,7 +417,7 @@ class MessagePump(metaclass=MessagePumpMeta):
# parent is sender, so we stop propagation after parent # parent is sender, so we stop propagation after parent
message.stop() message.stop()
if self.is_parent_active and not self._parent._closing: if self.is_parent_active and not self._parent._closing:
await self._parent.post_message(message) await message._bubble_to(self._parent)
def check_idle(self) -> None: def check_idle(self) -> None:
"""Prompt the message pump to call idle if the queue is empty.""" """Prompt the message pump to call idle if the queue is empty."""

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from typing import Iterable
import rich.repr import rich.repr
from rich.console import RenderableType from rich.console import RenderableType
@@ -106,6 +107,18 @@ class Screen(Widget):
""" """
return self._compositor.get_widget_at(x, y) return self._compositor.get_widget_at(x, y)
def get_widgets_at(self, x: int, y: int) -> Iterable[tuple[Widget, Region]]:
"""Get all widgets under a given coordinate.
Args:
x (int): X coordinate.
y (int): Y coordinate.
Returns:
Iterable[tuple[Widget, Region]]: Sequence of (WIDGET, REGION) tuples.
"""
return self._compositor.get_widgets_at(x, y)
def get_style_at(self, x: int, y: int) -> Style: def get_style_at(self, x: int, y: int) -> Style:
"""Get the style under a given coordinate. """Get the style under a given coordinate.
@@ -315,7 +328,9 @@ class Screen(Widget):
event._set_forwarded() event._set_forwarded()
await self.post_message(event) await self.post_message(event)
else: else:
await widget._forward_event(event.offset(-region.x, -region.y)) await widget._forward_event(
event._apply_offset(-region.x, -region.y)
)
elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)): elif isinstance(event, (events.MouseScrollDown, events.MouseScrollUp)):
try: try:

View File

@@ -97,13 +97,13 @@ class ScrollView(Widget):
virtual_size (Size): New virtual size. virtual_size (Size): New virtual size.
container_size (Size): New container size. container_size (Size): New container size.
""" """
if self._size != size or virtual_size != self.virtual_size:
virtual_size = self.virtual_size
if self._size != size:
self._size = size self._size = size
self._container_size = size virtual_size = self.virtual_size
self._container_size = size - self.gutter.totals
self._scroll_update(virtual_size) self._scroll_update(virtual_size)
self.scroll_to(self.scroll_x, self.scroll_y) self.scroll_to(self.scroll_x, self.scroll_y)
self.refresh()
def render(self) -> RenderableType: def render(self) -> RenderableType:
"""Render the scrollable region (if `render_lines` is not implemented). """Render the scrollable region (if `render_lines` is not implemented).

View File

@@ -28,6 +28,7 @@ from ._layout import Layout
from ._segment_tools import align_lines from ._segment_tools import align_lines
from ._styles_cache import StylesCache from ._styles_cache import StylesCache
from ._types import Lines from ._types import Lines
from .css.scalar import ScalarOffset
from .binding import NoBinding from .binding import NoBinding
from .box_model import BoxModel, get_box_model from .box_model import BoxModel, get_box_model
from .dom import DOMNode, NoScreen from .dom import DOMNode, NoScreen
@@ -255,6 +256,14 @@ class Widget(DOMNode):
self.allow_horizontal_scroll or self.allow_vertical_scroll self.allow_horizontal_scroll or self.allow_vertical_scroll
) )
@property
def offset(self) -> Offset:
return self.styles.offset.resolve(self.size, self.app.size)
@offset.setter
def offset(self, offset: Offset) -> None:
self.styles.offset = ScalarOffset.from_offset(offset)
def get_component_rich_style(self, name: str) -> Style: def get_component_rich_style(self, name: str) -> Style:
"""Get a *Rich* style for a component. """Get a *Rich* style for a component.
@@ -1526,7 +1535,11 @@ class Widget(DOMNode):
virtual_size (Size): Virtual (scrollable) size. virtual_size (Size): Virtual (scrollable) size.
container_size (Size): Container size (size of parent). container_size (Size): Container size (size of parent).
""" """
if self._size != size or self.virtual_size != virtual_size: if (
self._size != size
or self.virtual_size != virtual_size
or self._container_size != container_size
):
self._size = size self._size = size
self.virtual_size = virtual_size self.virtual_size = virtual_size
self._container_size = container_size self._container_size = container_size
@@ -1543,6 +1556,7 @@ class Widget(DOMNode):
""" """
self._refresh_scrollbars() self._refresh_scrollbars()
width, height = self.container_size width, height = self.container_size
if self.show_vertical_scrollbar: if self.show_vertical_scrollbar:
self.vertical_scrollbar.window_virtual_size = virtual_size.height self.vertical_scrollbar.window_virtual_size = virtual_size.height
self.vertical_scrollbar.window_size = ( self.vertical_scrollbar.window_size = (

View File

@@ -1,6 +1,10 @@
from __future__ import annotations from __future__ import annotations
from typing import cast
from rich.console import RenderableType from rich.console import RenderableType
from rich.pretty import Pretty
from rich.protocol import is_renderable
from rich.segment import Segment from rich.segment import Segment
from ..reactive import var from ..reactive import var
@@ -46,19 +50,26 @@ class TextLog(ScrollView, can_focus=True):
def _on_styles_updated(self) -> None: def _on_styles_updated(self) -> None:
self._line_cache.clear() self._line_cache.clear()
def write(self, content: RenderableType) -> None: def write(self, content: RenderableType | object) -> None:
"""Write text or a rich renderable. """Write text or a rich renderable.
Args: Args:
content (RenderableType): Rich renderable (or text). content (RenderableType): Rich renderable (or text).
""" """
renderable: RenderableType
if not is_renderable(content):
renderable = Pretty(content)
else:
renderable = cast(RenderableType, content)
console = self.app.console console = self.app.console
width = max(self.min_width, self.size.width or self.min_width) width = max(self.min_width, self.size.width or self.min_width)
render_options = console.options.update_width(width) render_options = console.options.update_width(width)
if not self.wrap: if not self.wrap:
render_options = render_options.update(overflow="ignore", no_wrap=True) render_options = render_options.update(overflow="ignore", no_wrap=True)
segments = self.app.console.render(content, render_options) segments = self.app.console.render(renderable, render_options)
lines = list(Segment.split_lines(segments)) lines = list(Segment.split_lines(segments))
self.max_width = max( self.max_width = max(
self.max_width, self.max_width,