Skip to content

Commit

Permalink
Merge pull request #4393 from Textualize/inline-screen-fix
Browse files Browse the repository at this point in the history
Inline screen fix
  • Loading branch information
willmcgugan authored Apr 6, 2024
2 parents e08c3f9 + acb23d3 commit 1b2e860
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 46 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [0.56.0] - Unreleased

### Added

- Added `Size.with_width` and `Size.with_height` https://github.com/Textualize/textual/pull/4393

### Fixed

- Fixed issue with inline mode and multiple screens https://github.com/Textualize/textual/pull/4393

### Changed

- self.prevent can be used in a widget constructor to prevent messages on mount https://github.com/Textualize/textual/pull/4392


## [0.55.1] - 2024-04-2

### Fixed
Expand Down
21 changes: 15 additions & 6 deletions src/textual/_compositor.py
Original file line number Diff line number Diff line change
Expand Up @@ -977,16 +977,25 @@ def _get_renders(

for widget, region, clip in widget_regions:
if contains_region(clip, region):
yield region, clip, widget.render_lines(
_Region(0, 0, region.width, region.height)
yield (
region,
clip,
widget.render_lines(_Region(0, 0, region.width, region.height)),
)
else:
new_x, new_y, new_width, new_height = intersection(region, clip)
if new_width and new_height:
yield region, clip, widget.render_lines(
_Region(
new_x - region.x, new_y - region.y, new_width, new_height
)
yield (
region,
clip,
widget.render_lines(
_Region(
new_x - region.x,
new_y - region.y,
new_width,
new_height,
)
),
)

def render_update(
Expand Down
66 changes: 48 additions & 18 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1035,6 +1035,15 @@ def size(self) -> Size:
width, height = self.console.size
return Size(width, height)

def _get_inline_height(self) -> int:
"""Get the inline height (height when in inline mode).
Returns:
Height in lines.
"""
size = self.size
return max(screen._get_inline_height(size) for screen in self._screen_stack)

@property
def log(self) -> Logger:
"""The textual logger.
Expand Down Expand Up @@ -2324,6 +2333,41 @@ def _print_error_renderables(self) -> None:

self._exit_renderables.clear()

def _build_driver(
self, headless: bool, inline: bool, mouse: bool, size: tuple[int, int] | None
) -> Driver:
"""Construct a driver instance.
Args:
headless: Request headless driver.
inline: Request inline driver.
mouse: Request mouse support.
size: Initial size.
Returns:
Driver instance.
"""
driver: Driver
driver_class: type[Driver]
if headless:
from .drivers.headless_driver import HeadlessDriver

driver_class = HeadlessDriver
elif inline and not WINDOWS:
from .drivers.linux_inline_driver import LinuxInlineDriver

driver_class = LinuxInlineDriver
else:
driver_class = self.driver_class

driver = self._driver = driver_class(
self,
debug=constants.DEBUG,
mouse=mouse,
size=size,
)
return driver

async def _process_messages(
self,
ready_callback: CallbackType | None = None,
Expand Down Expand Up @@ -2438,23 +2482,9 @@ async def invoke_ready_callback() -> None:
load_event = events.Load()
await self._dispatch_message(load_event)

driver: Driver

driver_class: type[Driver]
if headless:
from .drivers.headless_driver import HeadlessDriver

driver_class = HeadlessDriver
elif inline:
from .drivers.linux_inline_driver import LinuxInlineDriver

driver_class = LinuxInlineDriver
else:
driver_class = self.driver_class

driver = self._driver = driver_class(
self,
debug=constants.DEBUG,
driver = self._driver = self._build_driver(
headless=headless,
inline=inline,
mouse=mouse,
size=terminal_size,
)
Expand All @@ -2471,7 +2501,7 @@ async def invoke_ready_callback() -> None:
if self._driver.is_inline:
cursor_x, cursor_y = self._previous_cursor_position
self._driver.write(
Control.move(-cursor_x, -cursor_y).segment.text
Control.move(-cursor_x, -cursor_y + 1).segment.text
)
if inline_no_clear:
console = Console()
Expand Down
4 changes: 2 additions & 2 deletions src/textual/drivers/linux_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ def on_terminal_resize(signum, stack) -> None:
termios.tcsetattr(self.fileno, termios.TCSANOW, newattr)

self.write("\x1b[?25l") # Hide cursor
self.write("\033[?1004h\n") # Enable FocusIn/FocusOut.
self.write("\033[?1004h") # Enable FocusIn/FocusOut.
self.flush()
self._key_thread = Thread(target=self._run_input_thread)
send_size_event()
Expand Down Expand Up @@ -322,7 +322,7 @@ def stop_application_mode(self) -> None:

# Alt screen false, show cursor
self.write("\x1b[?1049l" + "\x1b[?25h")
self.write("\033[?1004l\n") # Disable FocusIn/FocusOut.
self.write("\033[?1004l") # Disable FocusIn/FocusOut.
self.flush()

def close(self) -> None:
Expand Down
10 changes: 5 additions & 5 deletions src/textual/drivers/linux_inline_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@

@rich.repr.auto(angular=True)
class LinuxInlineDriver(Driver):

def __init__(
self,
app: App,
Expand Down Expand Up @@ -157,7 +156,6 @@ def more_data() -> bool:
selector.close()

def start_application_mode(self) -> None:

loop = asyncio.get_running_loop()

def send_size_event() -> None:
Expand All @@ -178,9 +176,11 @@ def on_terminal_resize(signum, stack) -> None:
signal.signal(signal.SIGWINCH, on_terminal_resize)

self.write("\x1b[?25l") # Hide cursor
self.write("\033[?1004h\n") # Enable FocusIn/FocusOut.
self.write("\033[?1004h") # Enable FocusIn/FocusOut.

self._enable_mouse_support()
self.write("\n")
self.flush()
try:
self.attrs_before = termios.tcgetattr(self.fileno)
except termios.error:
Expand Down Expand Up @@ -260,7 +260,7 @@ def stop_application_mode(self) -> None:
self._disable_bracketed_paste()
self.disable_input()

self.write("\x1b[A\x1b[J")
self.write("\x1b[2A\x1b[J")

if self.attrs_before is not None:
try:
Expand All @@ -269,6 +269,6 @@ def stop_application_mode(self) -> None:
pass

self.write("\x1b[?25h") # Show cursor
self.write("\033[?1004l\n") # Disable FocusIn/FocusOut.
self.write("\033[?1004l") # Disable FocusIn/FocusOut.

self.flush()
2 changes: 1 addition & 1 deletion src/textual/drivers/web_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ def do_exit() -> None:
self._enable_mouse_support()

self.write("\x1b[?25l") # Hide cursor
self.write("\033[?1003h\n")
self.write("\033[?1003h")

size = Size(80, 24) if self._size is None else Size(*self._size)
event = events.Resize(size, size)
Expand Down
7 changes: 4 additions & 3 deletions src/textual/drivers/windows_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ def start_application_mode(self) -> None:
self.write("\x1b[?1049h") # Enable alt screen
self._enable_mouse_support()
self.write("\x1b[?25l") # Hide cursor
self.write("\033[?1003h\n")
self.write("\033[?1004h\n") # Enable FocusIn/FocusOut.
self.write("\033[?1003h")
self.write("\033[?1004h") # Enable FocusIn/FocusOut.
self.flush()
self._enable_bracketed_paste()

self._event_thread = win32.EventMonitor(
Expand Down Expand Up @@ -125,7 +126,7 @@ def stop_application_mode(self) -> None:

# Disable alt screen, show cursor
self.write("\x1b[?1049l" + "\x1b[?25h")
self.write("\033[?1004l\n") # Disable FocusIn/FocusOut.
self.write("\033[?1004l") # Disable FocusIn/FocusOut.
self.flush()

def close(self) -> None:
Expand Down
22 changes: 22 additions & 0 deletions src/textual/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,28 @@ def line_range(self) -> range:
"""A range object that covers values between 0 and `height`."""
return range(self.height)

def with_width(self, width: int) -> Size:
"""Get a new Size with just the width changed.
Args:
width: New width.
Returns:
New Size instance.
"""
return Size(width, self.height)

def with_height(self, height: int) -> Size:
"""Get a new Size with just the height changed.
Args:
width: New height.
Returns:
New Size instance.
"""
return Size(self.width, height)

def __add__(self, other: object) -> Size:
if isinstance(other, tuple):
width, height = self
Expand Down
25 changes: 14 additions & 11 deletions src/textual/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,28 +672,28 @@ async def _on_idle(self, event: events.Idle) -> None:
def _compositor_refresh(self) -> None:
"""Perform a compositor refresh."""

if self.app.is_inline:
size = self.app.size
self.app._display(
app = self.app
if app.is_inline:
app._display(
self,
self._compositor.render_inline(
Size(size.width, self._get_inline_height(size)),
screen_stack=self.app._background_screens,
app.size.with_height(app._get_inline_height()),
screen_stack=app._background_screens,
),
)
self._dirty_widgets.clear()
self._compositor._dirty_regions.clear()

elif self is self.app.screen:
elif self is app.screen:
# Top screen
update = self._compositor.render_update(
screen_stack=self.app._background_screens
screen_stack=app._background_screens
)
self.app._display(self, update)
app._display(self, update)
self._dirty_widgets.clear()
elif self in self.app._background_screens and self._compositor._dirty_regions:
# Background screen
self.app.screen.refresh(*self._compositor._dirty_regions)
app.screen.refresh(*self._compositor._dirty_regions)
self._compositor._dirty_regions.clear()
self._dirty_widgets.clear()

Expand Down Expand Up @@ -775,7 +775,7 @@ def _refresh_layout(self, size: Size | None = None, scroll: bool = False) -> Non
"""Refresh the layout (can change size and positions of widgets)."""
size = self.outer_size if size is None else size
if self.app.is_inline:
size = Size(size.width, self._get_inline_height(self.app.size))
size = size.with_height(self.app._get_inline_height())
if not size:
return
self._compositor.update_widgets(self._dirty_widgets)
Expand Down Expand Up @@ -840,7 +840,10 @@ def _refresh_layout(self, size: Size | None = None, scroll: bool = False) -> Non
self.app._handle_exception(error)
return
if self.is_current:
self._compositor_refresh()
if self.app.is_inline:
self._update_timer.resume()
else:
self._compositor_refresh()

if self.app._dom_ready:
self.screen_layout_refresh_signal.publish()
Expand Down
10 changes: 10 additions & 0 deletions tests/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,3 +476,13 @@ def test_inflect():
assert Region(10, 10, 30, 20).inflect(
x_axis=-1, margin=Spacing(2, 2, 2, 2)
) == Region(-24, 34, 30, 20)


def test_size_with_height():
"""Test Size.with_height"""
assert Size(1, 2).with_height(10) == Size(1, 10)


def test_size_with_width():
"""Test Size.with_width"""
assert Size(1, 2).with_width(10) == Size(10, 2)

0 comments on commit 1b2e860

Please sign in to comment.