Skip to content

Commit 1b2e860

Browse files
authored
Merge pull request #4393 from Textualize/inline-screen-fix
Inline screen fix
2 parents e08c3f9 + acb23d3 commit 1b2e860

File tree

10 files changed

+130
-46
lines changed

10 files changed

+130
-46
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## [0.56.0] - Unreleased
99

10+
### Added
11+
12+
- Added `Size.with_width` and `Size.with_height` https://github.com/Textualize/textual/pull/4393
13+
14+
### Fixed
15+
16+
- Fixed issue with inline mode and multiple screens https://github.com/Textualize/textual/pull/4393
17+
1018
### Changed
1119

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

22+
1423
## [0.55.1] - 2024-04-2
1524

1625
### Fixed

src/textual/_compositor.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -977,16 +977,25 @@ def _get_renders(
977977

978978
for widget, region, clip in widget_regions:
979979
if contains_region(clip, region):
980-
yield region, clip, widget.render_lines(
981-
_Region(0, 0, region.width, region.height)
980+
yield (
981+
region,
982+
clip,
983+
widget.render_lines(_Region(0, 0, region.width, region.height)),
982984
)
983985
else:
984986
new_x, new_y, new_width, new_height = intersection(region, clip)
985987
if new_width and new_height:
986-
yield region, clip, widget.render_lines(
987-
_Region(
988-
new_x - region.x, new_y - region.y, new_width, new_height
989-
)
988+
yield (
989+
region,
990+
clip,
991+
widget.render_lines(
992+
_Region(
993+
new_x - region.x,
994+
new_y - region.y,
995+
new_width,
996+
new_height,
997+
)
998+
),
990999
)
9911000

9921001
def render_update(

src/textual/app.py

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,15 @@ def size(self) -> Size:
10351035
width, height = self.console.size
10361036
return Size(width, height)
10371037

1038+
def _get_inline_height(self) -> int:
1039+
"""Get the inline height (height when in inline mode).
1040+
1041+
Returns:
1042+
Height in lines.
1043+
"""
1044+
size = self.size
1045+
return max(screen._get_inline_height(size) for screen in self._screen_stack)
1046+
10381047
@property
10391048
def log(self) -> Logger:
10401049
"""The textual logger.
@@ -2324,6 +2333,41 @@ def _print_error_renderables(self) -> None:
23242333

23252334
self._exit_renderables.clear()
23262335

2336+
def _build_driver(
2337+
self, headless: bool, inline: bool, mouse: bool, size: tuple[int, int] | None
2338+
) -> Driver:
2339+
"""Construct a driver instance.
2340+
2341+
Args:
2342+
headless: Request headless driver.
2343+
inline: Request inline driver.
2344+
mouse: Request mouse support.
2345+
size: Initial size.
2346+
2347+
Returns:
2348+
Driver instance.
2349+
"""
2350+
driver: Driver
2351+
driver_class: type[Driver]
2352+
if headless:
2353+
from .drivers.headless_driver import HeadlessDriver
2354+
2355+
driver_class = HeadlessDriver
2356+
elif inline and not WINDOWS:
2357+
from .drivers.linux_inline_driver import LinuxInlineDriver
2358+
2359+
driver_class = LinuxInlineDriver
2360+
else:
2361+
driver_class = self.driver_class
2362+
2363+
driver = self._driver = driver_class(
2364+
self,
2365+
debug=constants.DEBUG,
2366+
mouse=mouse,
2367+
size=size,
2368+
)
2369+
return driver
2370+
23272371
async def _process_messages(
23282372
self,
23292373
ready_callback: CallbackType | None = None,
@@ -2438,23 +2482,9 @@ async def invoke_ready_callback() -> None:
24382482
load_event = events.Load()
24392483
await self._dispatch_message(load_event)
24402484

2441-
driver: Driver
2442-
2443-
driver_class: type[Driver]
2444-
if headless:
2445-
from .drivers.headless_driver import HeadlessDriver
2446-
2447-
driver_class = HeadlessDriver
2448-
elif inline:
2449-
from .drivers.linux_inline_driver import LinuxInlineDriver
2450-
2451-
driver_class = LinuxInlineDriver
2452-
else:
2453-
driver_class = self.driver_class
2454-
2455-
driver = self._driver = driver_class(
2456-
self,
2457-
debug=constants.DEBUG,
2485+
driver = self._driver = self._build_driver(
2486+
headless=headless,
2487+
inline=inline,
24582488
mouse=mouse,
24592489
size=terminal_size,
24602490
)
@@ -2471,7 +2501,7 @@ async def invoke_ready_callback() -> None:
24712501
if self._driver.is_inline:
24722502
cursor_x, cursor_y = self._previous_cursor_position
24732503
self._driver.write(
2474-
Control.move(-cursor_x, -cursor_y).segment.text
2504+
Control.move(-cursor_x, -cursor_y + 1).segment.text
24752505
)
24762506
if inline_no_clear:
24772507
console = Console()

src/textual/drivers/linux_driver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ def on_terminal_resize(signum, stack) -> None:
237237
termios.tcsetattr(self.fileno, termios.TCSANOW, newattr)
238238

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

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

328328
def close(self) -> None:

src/textual/drivers/linux_inline_driver.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
@rich.repr.auto(angular=True)
2626
class LinuxInlineDriver(Driver):
27-
2827
def __init__(
2928
self,
3029
app: App,
@@ -157,7 +156,6 @@ def more_data() -> bool:
157156
selector.close()
158157

159158
def start_application_mode(self) -> None:
160-
161159
loop = asyncio.get_running_loop()
162160

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

180178
self.write("\x1b[?25l") # Hide cursor
181-
self.write("\033[?1004h\n") # Enable FocusIn/FocusOut.
179+
self.write("\033[?1004h") # Enable FocusIn/FocusOut.
182180

183181
self._enable_mouse_support()
182+
self.write("\n")
183+
self.flush()
184184
try:
185185
self.attrs_before = termios.tcgetattr(self.fileno)
186186
except termios.error:
@@ -260,7 +260,7 @@ def stop_application_mode(self) -> None:
260260
self._disable_bracketed_paste()
261261
self.disable_input()
262262

263-
self.write("\x1b[A\x1b[J")
263+
self.write("\x1b[2A\x1b[J")
264264

265265
if self.attrs_before is not None:
266266
try:
@@ -269,6 +269,6 @@ def stop_application_mode(self) -> None:
269269
pass
270270

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

274274
self.flush()

src/textual/drivers/web_driver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def do_exit() -> None:
134134
self._enable_mouse_support()
135135

136136
self.write("\x1b[?25l") # Hide cursor
137-
self.write("\033[?1003h\n")
137+
self.write("\033[?1003h")
138138

139139
size = Size(80, 24) if self._size is None else Size(*self._size)
140140
event = events.Resize(size, size)

src/textual/drivers/windows_driver.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ def start_application_mode(self) -> None:
9595
self.write("\x1b[?1049h") # Enable alt screen
9696
self._enable_mouse_support()
9797
self.write("\x1b[?25l") # Hide cursor
98-
self.write("\033[?1003h\n")
99-
self.write("\033[?1004h\n") # Enable FocusIn/FocusOut.
98+
self.write("\033[?1003h")
99+
self.write("\033[?1004h") # Enable FocusIn/FocusOut.
100+
self.flush()
100101
self._enable_bracketed_paste()
101102

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

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

131132
def close(self) -> None:

src/textual/geometry.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,28 @@ def line_range(self) -> range:
194194
"""A range object that covers values between 0 and `height`."""
195195
return range(self.height)
196196

197+
def with_width(self, width: int) -> Size:
198+
"""Get a new Size with just the width changed.
199+
200+
Args:
201+
width: New width.
202+
203+
Returns:
204+
New Size instance.
205+
"""
206+
return Size(width, self.height)
207+
208+
def with_height(self, height: int) -> Size:
209+
"""Get a new Size with just the height changed.
210+
211+
Args:
212+
width: New height.
213+
214+
Returns:
215+
New Size instance.
216+
"""
217+
return Size(self.width, height)
218+
197219
def __add__(self, other: object) -> Size:
198220
if isinstance(other, tuple):
199221
width, height = self

src/textual/screen.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -672,28 +672,28 @@ async def _on_idle(self, event: events.Idle) -> None:
672672
def _compositor_refresh(self) -> None:
673673
"""Perform a compositor refresh."""
674674

675-
if self.app.is_inline:
676-
size = self.app.size
677-
self.app._display(
675+
app = self.app
676+
if app.is_inline:
677+
app._display(
678678
self,
679679
self._compositor.render_inline(
680-
Size(size.width, self._get_inline_height(size)),
681-
screen_stack=self.app._background_screens,
680+
app.size.with_height(app._get_inline_height()),
681+
screen_stack=app._background_screens,
682682
),
683683
)
684684
self._dirty_widgets.clear()
685685
self._compositor._dirty_regions.clear()
686686

687-
elif self is self.app.screen:
687+
elif self is app.screen:
688688
# Top screen
689689
update = self._compositor.render_update(
690-
screen_stack=self.app._background_screens
690+
screen_stack=app._background_screens
691691
)
692-
self.app._display(self, update)
692+
app._display(self, update)
693693
self._dirty_widgets.clear()
694694
elif self in self.app._background_screens and self._compositor._dirty_regions:
695695
# Background screen
696-
self.app.screen.refresh(*self._compositor._dirty_regions)
696+
app.screen.refresh(*self._compositor._dirty_regions)
697697
self._compositor._dirty_regions.clear()
698698
self._dirty_widgets.clear()
699699

@@ -775,7 +775,7 @@ def _refresh_layout(self, size: Size | None = None, scroll: bool = False) -> Non
775775
"""Refresh the layout (can change size and positions of widgets)."""
776776
size = self.outer_size if size is None else size
777777
if self.app.is_inline:
778-
size = Size(size.width, self._get_inline_height(self.app.size))
778+
size = size.with_height(self.app._get_inline_height())
779779
if not size:
780780
return
781781
self._compositor.update_widgets(self._dirty_widgets)
@@ -840,7 +840,10 @@ def _refresh_layout(self, size: Size | None = None, scroll: bool = False) -> Non
840840
self.app._handle_exception(error)
841841
return
842842
if self.is_current:
843-
self._compositor_refresh()
843+
if self.app.is_inline:
844+
self._update_timer.resume()
845+
else:
846+
self._compositor_refresh()
844847

845848
if self.app._dom_ready:
846849
self.screen_layout_refresh_signal.publish()

tests/test_geometry.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,3 +476,13 @@ def test_inflect():
476476
assert Region(10, 10, 30, 20).inflect(
477477
x_axis=-1, margin=Spacing(2, 2, 2, 2)
478478
) == Region(-24, 34, 30, 20)
479+
480+
481+
def test_size_with_height():
482+
"""Test Size.with_height"""
483+
assert Size(1, 2).with_height(10) == Size(1, 10)
484+
485+
486+
def test_size_with_width():
487+
"""Test Size.with_width"""
488+
assert Size(1, 2).with_width(10) == Size(10, 2)

0 commit comments

Comments
 (0)