Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/prompt_toolkit/layout/containers.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

979 statements  

1""" 

2Container for the layout. 

3(Containers can contain other containers or user interface controls.) 

4""" 

5 

6from __future__ import annotations 

7 

8from abc import ABCMeta, abstractmethod 

9from collections.abc import Callable, Sequence 

10from enum import Enum 

11from functools import partial 

12from typing import TYPE_CHECKING, Union, cast 

13 

14from prompt_toolkit.application.current import get_app 

15from prompt_toolkit.cache import SimpleCache 

16from prompt_toolkit.data_structures import Point 

17from prompt_toolkit.filters import ( 

18 FilterOrBool, 

19 emacs_insert_mode, 

20 to_filter, 

21 vi_insert_mode, 

22) 

23from prompt_toolkit.formatted_text import ( 

24 AnyFormattedText, 

25 StyleAndTextTuples, 

26 to_formatted_text, 

27) 

28from prompt_toolkit.formatted_text.utils import ( 

29 fragment_list_to_text, 

30 fragment_list_width, 

31) 

32from prompt_toolkit.key_binding import KeyBindingsBase 

33from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 

34from prompt_toolkit.utils import get_cwidth, take_using_weights, to_int, to_str 

35 

36from .controls import ( 

37 DummyControl, 

38 FormattedTextControl, 

39 GetLinePrefixCallable, 

40 UIContent, 

41 UIControl, 

42) 

43from .dimension import ( 

44 AnyDimension, 

45 Dimension, 

46 max_layout_dimensions, 

47 sum_layout_dimensions, 

48 to_dimension, 

49) 

50from .margins import Margin 

51from .mouse_handlers import MouseHandlers 

52from .screen import _CHAR_CACHE, Screen, WritePosition 

53from .utils import explode_text_fragments 

54 

55if TYPE_CHECKING: 

56 from typing import Protocol, TypeGuard 

57 

58 from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone 

59 

60 

61__all__ = [ 

62 "AnyContainer", 

63 "Container", 

64 "HorizontalAlign", 

65 "VerticalAlign", 

66 "HSplit", 

67 "VSplit", 

68 "FloatContainer", 

69 "Float", 

70 "WindowAlign", 

71 "Window", 

72 "WindowRenderInfo", 

73 "ConditionalContainer", 

74 "ScrollOffsets", 

75 "ColorColumn", 

76 "to_container", 

77 "to_window", 

78 "is_container", 

79 "DynamicContainer", 

80] 

81 

82 

83class Container(metaclass=ABCMeta): 

84 """ 

85 Base class for user interface layout. 

86 """ 

87 

88 @abstractmethod 

89 def reset(self) -> None: 

90 """ 

91 Reset the state of this container and all the children. 

92 (E.g. reset scroll offsets, etc...) 

93 """ 

94 

95 @abstractmethod 

96 def preferred_width(self, max_available_width: int) -> Dimension: 

97 """ 

98 Return a :class:`~prompt_toolkit.layout.Dimension` that represents the 

99 desired width for this container. 

100 """ 

101 

102 @abstractmethod 

103 def preferred_height(self, width: int, max_available_height: int) -> Dimension: 

104 """ 

105 Return a :class:`~prompt_toolkit.layout.Dimension` that represents the 

106 desired height for this container. 

107 """ 

108 

109 @abstractmethod 

110 def write_to_screen( 

111 self, 

112 screen: Screen, 

113 mouse_handlers: MouseHandlers, 

114 write_position: WritePosition, 

115 parent_style: str, 

116 erase_bg: bool, 

117 z_index: int | None, 

118 ) -> None: 

119 """ 

120 Write the actual content to the screen. 

121 

122 :param screen: :class:`~prompt_toolkit.layout.screen.Screen` 

123 :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`. 

124 :param parent_style: Style string to pass to the :class:`.Window` 

125 object. This will be applied to all content of the windows. 

126 :class:`.VSplit` and :class:`.HSplit` can use it to pass their 

127 style down to the windows that they contain. 

128 :param z_index: Used for propagating z_index from parent to child. 

129 """ 

130 

131 def is_modal(self) -> bool: 

132 """ 

133 When this container is modal, key bindings from parent containers are 

134 not taken into account if a user control in this container is focused. 

135 """ 

136 return False 

137 

138 def get_key_bindings(self) -> KeyBindingsBase | None: 

139 """ 

140 Returns a :class:`.KeyBindings` object. These bindings become active when any 

141 user control in this container has the focus, except if any containers 

142 between this container and the focused user control is modal. 

143 """ 

144 return None 

145 

146 @abstractmethod 

147 def get_children(self) -> list[Container]: 

148 """ 

149 Return the list of child :class:`.Container` objects. 

150 """ 

151 return [] 

152 

153 

154if TYPE_CHECKING: 

155 

156 class MagicContainer(Protocol): 

157 """ 

158 Any object that implements ``__pt_container__`` represents a container. 

159 """ 

160 

161 def __pt_container__(self) -> AnyContainer: ... 

162 

163 

164AnyContainer = Union[Container, "MagicContainer"] 

165 

166 

167def _window_too_small() -> Window: 

168 "Create a `Window` that displays the 'Window too small' text." 

169 return Window( 

170 FormattedTextControl(text=[("class:window-too-small", " Window too small... ")]) 

171 ) 

172 

173 

174class VerticalAlign(Enum): 

175 "Alignment for `HSplit`." 

176 

177 TOP = "TOP" 

178 CENTER = "CENTER" 

179 BOTTOM = "BOTTOM" 

180 JUSTIFY = "JUSTIFY" 

181 

182 

183class HorizontalAlign(Enum): 

184 "Alignment for `VSplit`." 

185 

186 LEFT = "LEFT" 

187 CENTER = "CENTER" 

188 RIGHT = "RIGHT" 

189 JUSTIFY = "JUSTIFY" 

190 

191 

192class _Split(Container): 

193 """ 

194 The common parts of `VSplit` and `HSplit`. 

195 """ 

196 

197 def __init__( 

198 self, 

199 children: Sequence[AnyContainer], 

200 window_too_small: Container | None = None, 

201 padding: AnyDimension = Dimension.exact(0), 

202 padding_char: str | None = None, 

203 padding_style: str = "", 

204 width: AnyDimension = None, 

205 height: AnyDimension = None, 

206 z_index: int | None = None, 

207 modal: bool = False, 

208 key_bindings: KeyBindingsBase | None = None, 

209 style: str | Callable[[], str] = "", 

210 ) -> None: 

211 self.children = [to_container(c) for c in children] 

212 self.window_too_small = window_too_small or _window_too_small() 

213 self.padding = padding 

214 self.padding_char = padding_char 

215 self.padding_style = padding_style 

216 

217 self.width = width 

218 self.height = height 

219 self.z_index = z_index 

220 

221 self.modal = modal 

222 self.key_bindings = key_bindings 

223 self.style = style 

224 

225 def is_modal(self) -> bool: 

226 return self.modal 

227 

228 def get_key_bindings(self) -> KeyBindingsBase | None: 

229 return self.key_bindings 

230 

231 def get_children(self) -> list[Container]: 

232 return self.children 

233 

234 

235class HSplit(_Split): 

236 """ 

237 Several layouts, one stacked above/under the other. :: 

238 

239 +--------------------+ 

240 | | 

241 +--------------------+ 

242 | | 

243 +--------------------+ 

244 

245 By default, this doesn't display a horizontal line between the children, 

246 but if this is something you need, then create a HSplit as follows:: 

247 

248 HSplit(children=[ ... ], padding_char='-', 

249 padding=1, padding_style='#ffff00') 

250 

251 :param children: List of child :class:`.Container` objects. 

252 :param window_too_small: A :class:`.Container` object that is displayed if 

253 there is not enough space for all the children. By default, this is a 

254 "Window too small" message. 

255 :param align: `VerticalAlign` value. 

256 :param width: When given, use this width instead of looking at the children. 

257 :param height: When given, use this height instead of looking at the children. 

258 :param z_index: (int or None) When specified, this can be used to bring 

259 element in front of floating elements. `None` means: inherit from parent. 

260 :param style: A style string. 

261 :param modal: ``True`` or ``False``. 

262 :param key_bindings: ``None`` or a :class:`.KeyBindings` object. 

263 

264 :param padding: (`Dimension` or int), size to be used for the padding. 

265 :param padding_char: Character to be used for filling in the padding. 

266 :param padding_style: Style to applied to the padding. 

267 """ 

268 

269 def __init__( 

270 self, 

271 children: Sequence[AnyContainer], 

272 window_too_small: Container | None = None, 

273 align: VerticalAlign = VerticalAlign.JUSTIFY, 

274 padding: AnyDimension = 0, 

275 padding_char: str | None = None, 

276 padding_style: str = "", 

277 width: AnyDimension = None, 

278 height: AnyDimension = None, 

279 z_index: int | None = None, 

280 modal: bool = False, 

281 key_bindings: KeyBindingsBase | None = None, 

282 style: str | Callable[[], str] = "", 

283 ) -> None: 

284 super().__init__( 

285 children=children, 

286 window_too_small=window_too_small, 

287 padding=padding, 

288 padding_char=padding_char, 

289 padding_style=padding_style, 

290 width=width, 

291 height=height, 

292 z_index=z_index, 

293 modal=modal, 

294 key_bindings=key_bindings, 

295 style=style, 

296 ) 

297 

298 self.align = align 

299 

300 self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = ( 

301 SimpleCache(maxsize=1) 

302 ) 

303 self._remaining_space_window = Window() # Dummy window. 

304 

305 def preferred_width(self, max_available_width: int) -> Dimension: 

306 if self.width is not None: 

307 return to_dimension(self.width) 

308 

309 if self.children: 

310 dimensions = [c.preferred_width(max_available_width) for c in self.children] 

311 return max_layout_dimensions(dimensions) 

312 else: 

313 return Dimension() 

314 

315 def preferred_height(self, width: int, max_available_height: int) -> Dimension: 

316 if self.height is not None: 

317 return to_dimension(self.height) 

318 

319 dimensions = [ 

320 c.preferred_height(width, max_available_height) for c in self._all_children 

321 ] 

322 return sum_layout_dimensions(dimensions) 

323 

324 def reset(self) -> None: 

325 for c in self.children: 

326 c.reset() 

327 

328 @property 

329 def _all_children(self) -> list[Container]: 

330 """ 

331 List of child objects, including padding. 

332 """ 

333 

334 def get() -> list[Container]: 

335 result: list[Container] = [] 

336 

337 # Padding Top. 

338 if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM): 

339 result.append(Window(width=Dimension(preferred=0))) 

340 

341 # The children with padding. 

342 for child in self.children: 

343 result.append(child) 

344 result.append( 

345 Window( 

346 height=self.padding, 

347 char=self.padding_char, 

348 style=self.padding_style, 

349 ) 

350 ) 

351 if result: 

352 result.pop() 

353 

354 # Padding right. 

355 if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP): 

356 result.append(Window(width=Dimension(preferred=0))) 

357 

358 return result 

359 

360 return self._children_cache.get(tuple(self.children), get) 

361 

362 def write_to_screen( 

363 self, 

364 screen: Screen, 

365 mouse_handlers: MouseHandlers, 

366 write_position: WritePosition, 

367 parent_style: str, 

368 erase_bg: bool, 

369 z_index: int | None, 

370 ) -> None: 

371 """ 

372 Render the prompt to a `Screen` instance. 

373 

374 :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class 

375 to which the output has to be written. 

376 """ 

377 sizes = self._divide_heights(write_position) 

378 style = parent_style + " " + to_str(self.style) 

379 z_index = z_index if self.z_index is None else self.z_index 

380 

381 if sizes is None: 

382 self.window_too_small.write_to_screen( 

383 screen, mouse_handlers, write_position, style, erase_bg, z_index 

384 ) 

385 else: 

386 # 

387 ypos = write_position.ypos 

388 xpos = write_position.xpos 

389 width = write_position.width 

390 

391 # Draw child panes. 

392 for s, c in zip(sizes, self._all_children): 

393 c.write_to_screen( 

394 screen, 

395 mouse_handlers, 

396 WritePosition(xpos, ypos, width, s), 

397 style, 

398 erase_bg, 

399 z_index, 

400 ) 

401 ypos += s 

402 

403 # Fill in the remaining space. This happens when a child control 

404 # refuses to take more space and we don't have any padding. Adding a 

405 # dummy child control for this (in `self._all_children`) is not 

406 # desired, because in some situations, it would take more space, even 

407 # when it's not required. This is required to apply the styling. 

408 remaining_height = write_position.ypos + write_position.height - ypos 

409 if remaining_height > 0: 

410 self._remaining_space_window.write_to_screen( 

411 screen, 

412 mouse_handlers, 

413 WritePosition(xpos, ypos, width, remaining_height), 

414 style, 

415 erase_bg, 

416 z_index, 

417 ) 

418 

419 def _divide_heights(self, write_position: WritePosition) -> list[int] | None: 

420 """ 

421 Return the heights for all rows. 

422 Or None when there is not enough space. 

423 """ 

424 if not self.children: 

425 return [] 

426 

427 width = write_position.width 

428 height = write_position.height 

429 

430 # Calculate heights. 

431 dimensions = [c.preferred_height(width, height) for c in self._all_children] 

432 

433 # Sum dimensions 

434 sum_dimensions = sum_layout_dimensions(dimensions) 

435 

436 # If there is not enough space for both. 

437 # Don't do anything. 

438 if sum_dimensions.min > height: 

439 return None 

440 

441 # Find optimal sizes. (Start with minimal size, increase until we cover 

442 # the whole height.) 

443 sizes = [d.min for d in dimensions] 

444 

445 child_generator = take_using_weights( 

446 items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] 

447 ) 

448 

449 i = next(child_generator) 

450 

451 # Increase until we meet at least the 'preferred' size. 

452 preferred_stop = min(height, sum_dimensions.preferred) 

453 preferred_dimensions = [d.preferred for d in dimensions] 

454 

455 while sum(sizes) < preferred_stop: 

456 if sizes[i] < preferred_dimensions[i]: 

457 sizes[i] += 1 

458 i = next(child_generator) 

459 

460 # Increase until we use all the available space. (or until "max") 

461 if not get_app().is_done: 

462 max_stop = min(height, sum_dimensions.max) 

463 max_dimensions = [d.max for d in dimensions] 

464 

465 while sum(sizes) < max_stop: 

466 if sizes[i] < max_dimensions[i]: 

467 sizes[i] += 1 

468 i = next(child_generator) 

469 

470 return sizes 

471 

472 

473class VSplit(_Split): 

474 """ 

475 Several layouts, one stacked left/right of the other. :: 

476 

477 +---------+----------+ 

478 | | | 

479 | | | 

480 +---------+----------+ 

481 

482 By default, this doesn't display a vertical line between the children, but 

483 if this is something you need, then create a HSplit as follows:: 

484 

485 VSplit(children=[ ... ], padding_char='|', 

486 padding=1, padding_style='#ffff00') 

487 

488 :param children: List of child :class:`.Container` objects. 

489 :param window_too_small: A :class:`.Container` object that is displayed if 

490 there is not enough space for all the children. By default, this is a 

491 "Window too small" message. 

492 :param align: `HorizontalAlign` value. 

493 :param width: When given, use this width instead of looking at the children. 

494 :param height: When given, use this height instead of looking at the children. 

495 :param z_index: (int or None) When specified, this can be used to bring 

496 element in front of floating elements. `None` means: inherit from parent. 

497 :param style: A style string. 

498 :param modal: ``True`` or ``False``. 

499 :param key_bindings: ``None`` or a :class:`.KeyBindings` object. 

500 

501 :param padding: (`Dimension` or int), size to be used for the padding. 

502 :param padding_char: Character to be used for filling in the padding. 

503 :param padding_style: Style to applied to the padding. 

504 """ 

505 

506 def __init__( 

507 self, 

508 children: Sequence[AnyContainer], 

509 window_too_small: Container | None = None, 

510 align: HorizontalAlign = HorizontalAlign.JUSTIFY, 

511 padding: AnyDimension = 0, 

512 padding_char: str | None = None, 

513 padding_style: str = "", 

514 width: AnyDimension = None, 

515 height: AnyDimension = None, 

516 z_index: int | None = None, 

517 modal: bool = False, 

518 key_bindings: KeyBindingsBase | None = None, 

519 style: str | Callable[[], str] = "", 

520 ) -> None: 

521 super().__init__( 

522 children=children, 

523 window_too_small=window_too_small, 

524 padding=padding, 

525 padding_char=padding_char, 

526 padding_style=padding_style, 

527 width=width, 

528 height=height, 

529 z_index=z_index, 

530 modal=modal, 

531 key_bindings=key_bindings, 

532 style=style, 

533 ) 

534 

535 self.align = align 

536 

537 self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = ( 

538 SimpleCache(maxsize=1) 

539 ) 

540 self._remaining_space_window = Window() # Dummy window. 

541 

542 def preferred_width(self, max_available_width: int) -> Dimension: 

543 if self.width is not None: 

544 return to_dimension(self.width) 

545 

546 dimensions = [ 

547 c.preferred_width(max_available_width) for c in self._all_children 

548 ] 

549 

550 return sum_layout_dimensions(dimensions) 

551 

552 def preferred_height(self, width: int, max_available_height: int) -> Dimension: 

553 if self.height is not None: 

554 return to_dimension(self.height) 

555 

556 # At the point where we want to calculate the heights, the widths have 

557 # already been decided. So we can trust `width` to be the actual 

558 # `width` that's going to be used for the rendering. So, 

559 # `divide_widths` is supposed to use all of the available width. 

560 # Using only the `preferred` width caused a bug where the reported 

561 # height was more than required. (we had a `BufferControl` which did 

562 # wrap lines because of the smaller width returned by `_divide_widths`. 

563 

564 sizes = self._divide_widths(width) 

565 children = self._all_children 

566 

567 if sizes is None: 

568 return Dimension() 

569 else: 

570 dimensions = [ 

571 c.preferred_height(s, max_available_height) 

572 for s, c in zip(sizes, children) 

573 ] 

574 return max_layout_dimensions(dimensions) 

575 

576 def reset(self) -> None: 

577 for c in self.children: 

578 c.reset() 

579 

580 @property 

581 def _all_children(self) -> list[Container]: 

582 """ 

583 List of child objects, including padding. 

584 """ 

585 

586 def get() -> list[Container]: 

587 result: list[Container] = [] 

588 

589 # Padding left. 

590 if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT): 

591 result.append(Window(width=Dimension(preferred=0))) 

592 

593 # The children with padding. 

594 for child in self.children: 

595 result.append(child) 

596 result.append( 

597 Window( 

598 width=self.padding, 

599 char=self.padding_char, 

600 style=self.padding_style, 

601 ) 

602 ) 

603 if result: 

604 result.pop() 

605 

606 # Padding right. 

607 if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT): 

608 result.append(Window(width=Dimension(preferred=0))) 

609 

610 return result 

611 

612 return self._children_cache.get(tuple(self.children), get) 

613 

614 def _divide_widths(self, width: int) -> list[int] | None: 

615 """ 

616 Return the widths for all columns. 

617 Or None when there is not enough space. 

618 """ 

619 children = self._all_children 

620 

621 if not children: 

622 return [] 

623 

624 # Calculate widths. 

625 dimensions = [c.preferred_width(width) for c in children] 

626 preferred_dimensions = [d.preferred for d in dimensions] 

627 

628 # Sum dimensions 

629 sum_dimensions = sum_layout_dimensions(dimensions) 

630 

631 # If there is not enough space for both. 

632 # Don't do anything. 

633 if sum_dimensions.min > width: 

634 return None 

635 

636 # Find optimal sizes. (Start with minimal size, increase until we cover 

637 # the whole width.) 

638 sizes = [d.min for d in dimensions] 

639 

640 child_generator = take_using_weights( 

641 items=list(range(len(dimensions))), weights=[d.weight for d in dimensions] 

642 ) 

643 

644 i = next(child_generator) 

645 

646 # Increase until we meet at least the 'preferred' size. 

647 preferred_stop = min(width, sum_dimensions.preferred) 

648 

649 while sum(sizes) < preferred_stop: 

650 if sizes[i] < preferred_dimensions[i]: 

651 sizes[i] += 1 

652 i = next(child_generator) 

653 

654 # Increase until we use all the available space. 

655 max_dimensions = [d.max for d in dimensions] 

656 max_stop = min(width, sum_dimensions.max) 

657 

658 while sum(sizes) < max_stop: 

659 if sizes[i] < max_dimensions[i]: 

660 sizes[i] += 1 

661 i = next(child_generator) 

662 

663 return sizes 

664 

665 def write_to_screen( 

666 self, 

667 screen: Screen, 

668 mouse_handlers: MouseHandlers, 

669 write_position: WritePosition, 

670 parent_style: str, 

671 erase_bg: bool, 

672 z_index: int | None, 

673 ) -> None: 

674 """ 

675 Render the prompt to a `Screen` instance. 

676 

677 :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class 

678 to which the output has to be written. 

679 """ 

680 if not self.children: 

681 return 

682 

683 children = self._all_children 

684 sizes = self._divide_widths(write_position.width) 

685 style = parent_style + " " + to_str(self.style) 

686 z_index = z_index if self.z_index is None else self.z_index 

687 

688 # If there is not enough space. 

689 if sizes is None: 

690 self.window_too_small.write_to_screen( 

691 screen, mouse_handlers, write_position, style, erase_bg, z_index 

692 ) 

693 return 

694 

695 # Calculate heights, take the largest possible, but not larger than 

696 # write_position.height. 

697 heights = [ 

698 child.preferred_height(width, write_position.height).preferred 

699 for width, child in zip(sizes, children) 

700 ] 

701 height = max(write_position.height, min(write_position.height, max(heights))) 

702 

703 # 

704 ypos = write_position.ypos 

705 xpos = write_position.xpos 

706 

707 # Draw all child panes. 

708 for s, c in zip(sizes, children): 

709 c.write_to_screen( 

710 screen, 

711 mouse_handlers, 

712 WritePosition(xpos, ypos, s, height), 

713 style, 

714 erase_bg, 

715 z_index, 

716 ) 

717 xpos += s 

718 

719 # Fill in the remaining space. This happens when a child control 

720 # refuses to take more space and we don't have any padding. Adding a 

721 # dummy child control for this (in `self._all_children`) is not 

722 # desired, because in some situations, it would take more space, even 

723 # when it's not required. This is required to apply the styling. 

724 remaining_width = write_position.xpos + write_position.width - xpos 

725 if remaining_width > 0: 

726 self._remaining_space_window.write_to_screen( 

727 screen, 

728 mouse_handlers, 

729 WritePosition(xpos, ypos, remaining_width, height), 

730 style, 

731 erase_bg, 

732 z_index, 

733 ) 

734 

735 

736class FloatContainer(Container): 

737 """ 

738 Container which can contain another container for the background, as well 

739 as a list of floating containers on top of it. 

740 

741 Example Usage:: 

742 

743 FloatContainer(content=Window(...), 

744 floats=[ 

745 Float(xcursor=True, 

746 ycursor=True, 

747 content=CompletionsMenu(...)) 

748 ]) 

749 

750 :param z_index: (int or None) When specified, this can be used to bring 

751 element in front of floating elements. `None` means: inherit from parent. 

752 This is the z_index for the whole `Float` container as a whole. 

753 """ 

754 

755 def __init__( 

756 self, 

757 content: AnyContainer, 

758 floats: list[Float], 

759 modal: bool = False, 

760 key_bindings: KeyBindingsBase | None = None, 

761 style: str | Callable[[], str] = "", 

762 z_index: int | None = None, 

763 ) -> None: 

764 self.content = to_container(content) 

765 self.floats = floats 

766 

767 self.modal = modal 

768 self.key_bindings = key_bindings 

769 self.style = style 

770 self.z_index = z_index 

771 

772 def reset(self) -> None: 

773 self.content.reset() 

774 

775 for f in self.floats: 

776 f.content.reset() 

777 

778 def preferred_width(self, max_available_width: int) -> Dimension: 

779 return self.content.preferred_width(max_available_width) 

780 

781 def preferred_height(self, width: int, max_available_height: int) -> Dimension: 

782 """ 

783 Return the preferred height of the float container. 

784 (We don't care about the height of the floats, they should always fit 

785 into the dimensions provided by the container.) 

786 """ 

787 return self.content.preferred_height(width, max_available_height) 

788 

789 def write_to_screen( 

790 self, 

791 screen: Screen, 

792 mouse_handlers: MouseHandlers, 

793 write_position: WritePosition, 

794 parent_style: str, 

795 erase_bg: bool, 

796 z_index: int | None, 

797 ) -> None: 

798 style = parent_style + " " + to_str(self.style) 

799 z_index = z_index if self.z_index is None else self.z_index 

800 

801 self.content.write_to_screen( 

802 screen, mouse_handlers, write_position, style, erase_bg, z_index 

803 ) 

804 

805 for number, fl in enumerate(self.floats): 

806 # z_index of a Float is computed by summing the z_index of the 

807 # container and the `Float`. 

808 new_z_index = (z_index or 0) + fl.z_index 

809 style = parent_style + " " + to_str(self.style) 

810 

811 # If the float that we have here, is positioned relative to the 

812 # cursor position, but the Window that specifies the cursor 

813 # position is not drawn yet, because it's a Float itself, we have 

814 # to postpone this calculation. (This is a work-around, but good 

815 # enough for now.) 

816 postpone = fl.xcursor is not None or fl.ycursor is not None 

817 

818 if postpone: 

819 new_z_index = ( 

820 number + 10**8 

821 ) # Draw as late as possible, but keep the order. 

822 screen.draw_with_z_index( 

823 z_index=new_z_index, 

824 draw_func=partial( 

825 self._draw_float, 

826 fl, 

827 screen, 

828 mouse_handlers, 

829 write_position, 

830 style, 

831 erase_bg, 

832 new_z_index, 

833 ), 

834 ) 

835 else: 

836 self._draw_float( 

837 fl, 

838 screen, 

839 mouse_handlers, 

840 write_position, 

841 style, 

842 erase_bg, 

843 new_z_index, 

844 ) 

845 

846 def _draw_float( 

847 self, 

848 fl: Float, 

849 screen: Screen, 

850 mouse_handlers: MouseHandlers, 

851 write_position: WritePosition, 

852 style: str, 

853 erase_bg: bool, 

854 z_index: int | None, 

855 ) -> None: 

856 "Draw a single Float." 

857 # When a menu_position was given, use this instead of the cursor 

858 # position. (These cursor positions are absolute, translate again 

859 # relative to the write_position.) 

860 # Note: This should be inside the for-loop, because one float could 

861 # set the cursor position to be used for the next one. 

862 cpos = screen.get_menu_position( 

863 fl.attach_to_window or get_app().layout.current_window 

864 ) 

865 cursor_position = Point( 

866 x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos 

867 ) 

868 

869 fl_width = fl.get_width() 

870 fl_height = fl.get_height() 

871 width: int 

872 height: int 

873 xpos: int 

874 ypos: int 

875 

876 # Left & width given. 

877 if fl.left is not None and fl_width is not None: 

878 xpos = fl.left 

879 width = fl_width 

880 # Left & right given -> calculate width. 

881 elif fl.left is not None and fl.right is not None: 

882 xpos = fl.left 

883 width = write_position.width - fl.left - fl.right 

884 # Width & right given -> calculate left. 

885 elif fl_width is not None and fl.right is not None: 

886 xpos = write_position.width - fl.right - fl_width 

887 width = fl_width 

888 # Near x position of cursor. 

889 elif fl.xcursor: 

890 if fl_width is None: 

891 width = fl.content.preferred_width(write_position.width).preferred 

892 width = min(write_position.width, width) 

893 else: 

894 width = fl_width 

895 

896 xpos = cursor_position.x 

897 if xpos + width > write_position.width: 

898 xpos = max(0, write_position.width - width) 

899 # Only width given -> center horizontally. 

900 elif fl_width: 

901 xpos = int((write_position.width - fl_width) / 2) 

902 width = fl_width 

903 # Otherwise, take preferred width from float content. 

904 else: 

905 width = fl.content.preferred_width(write_position.width).preferred 

906 

907 if fl.left is not None: 

908 xpos = fl.left 

909 elif fl.right is not None: 

910 xpos = max(0, write_position.width - width - fl.right) 

911 else: # Center horizontally. 

912 xpos = max(0, int((write_position.width - width) / 2)) 

913 

914 # Trim. 

915 width = min(width, write_position.width - xpos) 

916 

917 # Top & height given. 

918 if fl.top is not None and fl_height is not None: 

919 ypos = fl.top 

920 height = fl_height 

921 # Top & bottom given -> calculate height. 

922 elif fl.top is not None and fl.bottom is not None: 

923 ypos = fl.top 

924 height = write_position.height - fl.top - fl.bottom 

925 # Height & bottom given -> calculate top. 

926 elif fl_height is not None and fl.bottom is not None: 

927 ypos = write_position.height - fl_height - fl.bottom 

928 height = fl_height 

929 # Near cursor. 

930 elif fl.ycursor: 

931 ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1) 

932 

933 if fl_height is None: 

934 height = fl.content.preferred_height( 

935 width, write_position.height 

936 ).preferred 

937 else: 

938 height = fl_height 

939 

940 # Reduce height if not enough space. (We can use the height 

941 # when the content requires it.) 

942 if height > write_position.height - ypos: 

943 if write_position.height - ypos + 1 >= ypos: 

944 # When the space below the cursor is more than 

945 # the space above, just reduce the height. 

946 height = write_position.height - ypos 

947 else: 

948 # Otherwise, fit the float above the cursor. 

949 height = min(height, cursor_position.y) 

950 ypos = cursor_position.y - height 

951 

952 # Only height given -> center vertically. 

953 elif fl_height: 

954 ypos = int((write_position.height - fl_height) / 2) 

955 height = fl_height 

956 # Otherwise, take preferred height from content. 

957 else: 

958 height = fl.content.preferred_height(width, write_position.height).preferred 

959 

960 if fl.top is not None: 

961 ypos = fl.top 

962 elif fl.bottom is not None: 

963 ypos = max(0, write_position.height - height - fl.bottom) 

964 else: # Center vertically. 

965 ypos = max(0, int((write_position.height - height) / 2)) 

966 

967 # Trim. 

968 height = min(height, write_position.height - ypos) 

969 

970 # Write float. 

971 # (xpos and ypos can be negative: a float can be partially visible.) 

972 if height > 0 and width > 0: 

973 wp = WritePosition( 

974 xpos=xpos + write_position.xpos, 

975 ypos=ypos + write_position.ypos, 

976 width=width, 

977 height=height, 

978 ) 

979 

980 if not fl.hide_when_covering_content or self._area_is_empty(screen, wp): 

981 fl.content.write_to_screen( 

982 screen, 

983 mouse_handlers, 

984 wp, 

985 style, 

986 erase_bg=not fl.transparent(), 

987 z_index=z_index, 

988 ) 

989 

990 def _area_is_empty(self, screen: Screen, write_position: WritePosition) -> bool: 

991 """ 

992 Return True when the area below the write position is still empty. 

993 (For floats that should not hide content underneath.) 

994 """ 

995 wp = write_position 

996 

997 for y in range(wp.ypos, wp.ypos + wp.height): 

998 if y in screen.data_buffer: 

999 row = screen.data_buffer[y] 

1000 

1001 for x in range(wp.xpos, wp.xpos + wp.width): 

1002 c = row[x] 

1003 if c.char != " ": 

1004 return False 

1005 

1006 return True 

1007 

1008 def is_modal(self) -> bool: 

1009 return self.modal 

1010 

1011 def get_key_bindings(self) -> KeyBindingsBase | None: 

1012 return self.key_bindings 

1013 

1014 def get_children(self) -> list[Container]: 

1015 children = [self.content] 

1016 children.extend(f.content for f in self.floats) 

1017 return children 

1018 

1019 

1020class Float: 

1021 """ 

1022 Float for use in a :class:`.FloatContainer`. 

1023 Except for the `content` parameter, all other options are optional. 

1024 

1025 :param content: :class:`.Container` instance. 

1026 

1027 :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`. 

1028 :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`. 

1029 

1030 :param left: Distance to the left edge of the :class:`.FloatContainer`. 

1031 :param right: Distance to the right edge of the :class:`.FloatContainer`. 

1032 :param top: Distance to the top of the :class:`.FloatContainer`. 

1033 :param bottom: Distance to the bottom of the :class:`.FloatContainer`. 

1034 

1035 :param attach_to_window: Attach to the cursor from this window, instead of 

1036 the current window. 

1037 :param hide_when_covering_content: Hide the float when it covers content underneath. 

1038 :param allow_cover_cursor: When `False`, make sure to display the float 

1039 below the cursor. Not on top of the indicated position. 

1040 :param z_index: Z-index position. For a Float, this needs to be at least 

1041 one. It is relative to the z_index of the parent container. 

1042 :param transparent: :class:`.Filter` indicating whether this float needs to be 

1043 drawn transparently. 

1044 """ 

1045 

1046 def __init__( 

1047 self, 

1048 content: AnyContainer, 

1049 top: int | None = None, 

1050 right: int | None = None, 

1051 bottom: int | None = None, 

1052 left: int | None = None, 

1053 width: int | Callable[[], int] | None = None, 

1054 height: int | Callable[[], int] | None = None, 

1055 xcursor: bool = False, 

1056 ycursor: bool = False, 

1057 attach_to_window: AnyContainer | None = None, 

1058 hide_when_covering_content: bool = False, 

1059 allow_cover_cursor: bool = False, 

1060 z_index: int = 1, 

1061 transparent: bool = False, 

1062 ) -> None: 

1063 assert z_index >= 1 

1064 

1065 self.left = left 

1066 self.right = right 

1067 self.top = top 

1068 self.bottom = bottom 

1069 

1070 self.width = width 

1071 self.height = height 

1072 

1073 self.xcursor = xcursor 

1074 self.ycursor = ycursor 

1075 

1076 self.attach_to_window = ( 

1077 to_window(attach_to_window) if attach_to_window else None 

1078 ) 

1079 

1080 self.content = to_container(content) 

1081 self.hide_when_covering_content = hide_when_covering_content 

1082 self.allow_cover_cursor = allow_cover_cursor 

1083 self.z_index = z_index 

1084 self.transparent = to_filter(transparent) 

1085 

1086 def get_width(self) -> int | None: 

1087 if callable(self.width): 

1088 return self.width() 

1089 return self.width 

1090 

1091 def get_height(self) -> int | None: 

1092 if callable(self.height): 

1093 return self.height() 

1094 return self.height 

1095 

1096 def __repr__(self) -> str: 

1097 return f"Float(content={self.content!r})" 

1098 

1099 

1100class WindowRenderInfo: 

1101 """ 

1102 Render information for the last render time of this control. 

1103 It stores mapping information between the input buffers (in case of a 

1104 :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual 

1105 render position on the output screen. 

1106 

1107 (Could be used for implementation of the Vi 'H' and 'L' key bindings as 

1108 well as implementing mouse support.) 

1109 

1110 :param ui_content: The original :class:`.UIContent` instance that contains 

1111 the whole input, without clipping. (ui_content) 

1112 :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance. 

1113 :param vertical_scroll: The vertical scroll of the :class:`.Window` instance. 

1114 :param window_width: The width of the window that displays the content, 

1115 without the margins. 

1116 :param window_height: The height of the window that displays the content. 

1117 :param configured_scroll_offsets: The scroll offsets as configured for the 

1118 :class:`Window` instance. 

1119 :param visible_line_to_row_col: Mapping that maps the row numbers on the 

1120 displayed screen (starting from zero for the first visible line) to 

1121 (row, col) tuples pointing to the row and column of the :class:`.UIContent`. 

1122 :param rowcol_to_yx: Mapping that maps (row, column) tuples representing 

1123 coordinates of the :class:`UIContent` to (y, x) absolute coordinates at 

1124 the rendered screen. 

1125 """ 

1126 

1127 def __init__( 

1128 self, 

1129 window: Window, 

1130 ui_content: UIContent, 

1131 horizontal_scroll: int, 

1132 vertical_scroll: int, 

1133 window_width: int, 

1134 window_height: int, 

1135 configured_scroll_offsets: ScrollOffsets, 

1136 visible_line_to_row_col: dict[int, tuple[int, int]], 

1137 rowcol_to_yx: dict[tuple[int, int], tuple[int, int]], 

1138 x_offset: int, 

1139 y_offset: int, 

1140 wrap_lines: bool, 

1141 ) -> None: 

1142 self.window = window 

1143 self.ui_content = ui_content 

1144 self.vertical_scroll = vertical_scroll 

1145 self.window_width = window_width # Width without margins. 

1146 self.window_height = window_height 

1147 

1148 self.configured_scroll_offsets = configured_scroll_offsets 

1149 self.visible_line_to_row_col = visible_line_to_row_col 

1150 self.wrap_lines = wrap_lines 

1151 

1152 self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x 

1153 # screen coordinates. 

1154 self._x_offset = x_offset 

1155 self._y_offset = y_offset 

1156 

1157 @property 

1158 def visible_line_to_input_line(self) -> dict[int, int]: 

1159 return { 

1160 visible_line: rowcol[0] 

1161 for visible_line, rowcol in self.visible_line_to_row_col.items() 

1162 } 

1163 

1164 @property 

1165 def cursor_position(self) -> Point: 

1166 """ 

1167 Return the cursor position coordinates, relative to the left/top corner 

1168 of the rendered screen. 

1169 """ 

1170 cpos = self.ui_content.cursor_position 

1171 try: 

1172 y, x = self._rowcol_to_yx[cpos.y, cpos.x] 

1173 except KeyError: 

1174 # For `DummyControl` for instance, the content can be empty, and so 

1175 # will `_rowcol_to_yx` be. Return 0/0 by default. 

1176 return Point(x=0, y=0) 

1177 else: 

1178 return Point(x=x - self._x_offset, y=y - self._y_offset) 

1179 

1180 @property 

1181 def applied_scroll_offsets(self) -> ScrollOffsets: 

1182 """ 

1183 Return a :class:`.ScrollOffsets` instance that indicates the actual 

1184 offset. This can be less than or equal to what's configured. E.g, when 

1185 the cursor is completely at the top, the top offset will be zero rather 

1186 than what's configured. 

1187 """ 

1188 if self.displayed_lines[0] == 0: 

1189 top = 0 

1190 else: 

1191 # Get row where the cursor is displayed. 

1192 y = self.input_line_to_visible_line[self.ui_content.cursor_position.y] 

1193 top = min(y, self.configured_scroll_offsets.top) 

1194 

1195 return ScrollOffsets( 

1196 top=top, 

1197 bottom=min( 

1198 self.ui_content.line_count - self.displayed_lines[-1] - 1, 

1199 self.configured_scroll_offsets.bottom, 

1200 ), 

1201 # For left/right, it probably doesn't make sense to return something. 

1202 # (We would have to calculate the widths of all the lines and keep 

1203 # double width characters in mind.) 

1204 left=0, 

1205 right=0, 

1206 ) 

1207 

1208 @property 

1209 def displayed_lines(self) -> list[int]: 

1210 """ 

1211 List of all the visible rows. (Line numbers of the input buffer.) 

1212 The last line may not be entirely visible. 

1213 """ 

1214 return sorted(row for row, col in self.visible_line_to_row_col.values()) 

1215 

1216 @property 

1217 def input_line_to_visible_line(self) -> dict[int, int]: 

1218 """ 

1219 Return the dictionary mapping the line numbers of the input buffer to 

1220 the lines of the screen. When a line spans several rows at the screen, 

1221 the first row appears in the dictionary. 

1222 """ 

1223 result: dict[int, int] = {} 

1224 for k, v in self.visible_line_to_input_line.items(): 

1225 if v in result: 

1226 result[v] = min(result[v], k) 

1227 else: 

1228 result[v] = k 

1229 return result 

1230 

1231 def first_visible_line(self, after_scroll_offset: bool = False) -> int: 

1232 """ 

1233 Return the line number (0 based) of the input document that corresponds 

1234 with the first visible line. 

1235 """ 

1236 if after_scroll_offset: 

1237 return self.displayed_lines[self.applied_scroll_offsets.top] 

1238 else: 

1239 return self.displayed_lines[0] 

1240 

1241 def last_visible_line(self, before_scroll_offset: bool = False) -> int: 

1242 """ 

1243 Like `first_visible_line`, but for the last visible line. 

1244 """ 

1245 if before_scroll_offset: 

1246 return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom] 

1247 else: 

1248 return self.displayed_lines[-1] 

1249 

1250 def center_visible_line( 

1251 self, before_scroll_offset: bool = False, after_scroll_offset: bool = False 

1252 ) -> int: 

1253 """ 

1254 Like `first_visible_line`, but for the center visible line. 

1255 """ 

1256 return ( 

1257 self.first_visible_line(after_scroll_offset) 

1258 + ( 

1259 self.last_visible_line(before_scroll_offset) 

1260 - self.first_visible_line(after_scroll_offset) 

1261 ) 

1262 // 2 

1263 ) 

1264 

1265 @property 

1266 def content_height(self) -> int: 

1267 """ 

1268 The full height of the user control. 

1269 """ 

1270 return self.ui_content.line_count 

1271 

1272 @property 

1273 def full_height_visible(self) -> bool: 

1274 """ 

1275 True when the full height is visible (There is no vertical scroll.) 

1276 """ 

1277 return ( 

1278 self.vertical_scroll == 0 

1279 and self.last_visible_line() == self.content_height 

1280 ) 

1281 

1282 @property 

1283 def top_visible(self) -> bool: 

1284 """ 

1285 True when the top of the buffer is visible. 

1286 """ 

1287 return self.vertical_scroll == 0 

1288 

1289 @property 

1290 def bottom_visible(self) -> bool: 

1291 """ 

1292 True when the bottom of the buffer is visible. 

1293 """ 

1294 return self.last_visible_line() == self.content_height - 1 

1295 

1296 @property 

1297 def vertical_scroll_percentage(self) -> int: 

1298 """ 

1299 Vertical scroll as a percentage. (0 means: the top is visible, 

1300 100 means: the bottom is visible.) 

1301 """ 

1302 if self.bottom_visible: 

1303 return 100 

1304 else: 

1305 return 100 * self.vertical_scroll // self.content_height 

1306 

1307 def get_height_for_line(self, lineno: int) -> int: 

1308 """ 

1309 Return the height of the given line. 

1310 (The height that it would take, if this line became visible.) 

1311 """ 

1312 if self.wrap_lines: 

1313 return self.ui_content.get_height_for_line( 

1314 lineno, self.window_width, self.window.get_line_prefix 

1315 ) 

1316 else: 

1317 return 1 

1318 

1319 

1320class ScrollOffsets: 

1321 """ 

1322 Scroll offsets for the :class:`.Window` class. 

1323 

1324 Note that left/right offsets only make sense if line wrapping is disabled. 

1325 """ 

1326 

1327 def __init__( 

1328 self, 

1329 top: int | Callable[[], int] = 0, 

1330 bottom: int | Callable[[], int] = 0, 

1331 left: int | Callable[[], int] = 0, 

1332 right: int | Callable[[], int] = 0, 

1333 ) -> None: 

1334 self._top = top 

1335 self._bottom = bottom 

1336 self._left = left 

1337 self._right = right 

1338 

1339 @property 

1340 def top(self) -> int: 

1341 return to_int(self._top) 

1342 

1343 @property 

1344 def bottom(self) -> int: 

1345 return to_int(self._bottom) 

1346 

1347 @property 

1348 def left(self) -> int: 

1349 return to_int(self._left) 

1350 

1351 @property 

1352 def right(self) -> int: 

1353 return to_int(self._right) 

1354 

1355 def __repr__(self) -> str: 

1356 return f"ScrollOffsets(top={self._top!r}, bottom={self._bottom!r}, left={self._left!r}, right={self._right!r})" 

1357 

1358 

1359class ColorColumn: 

1360 """ 

1361 Column for a :class:`.Window` to be colored. 

1362 """ 

1363 

1364 def __init__(self, position: int, style: str = "class:color-column") -> None: 

1365 self.position = position 

1366 self.style = style 

1367 

1368 

1369_in_insert_mode = vi_insert_mode | emacs_insert_mode 

1370 

1371 

1372class WindowAlign(Enum): 

1373 """ 

1374 Alignment of the Window content. 

1375 

1376 Note that this is different from `HorizontalAlign` and `VerticalAlign`, 

1377 which are used for the alignment of the child containers in respectively 

1378 `VSplit` and `HSplit`. 

1379 """ 

1380 

1381 LEFT = "LEFT" 

1382 RIGHT = "RIGHT" 

1383 CENTER = "CENTER" 

1384 

1385 

1386class Window(Container): 

1387 """ 

1388 Container that holds a control. 

1389 

1390 :param content: :class:`.UIControl` instance. 

1391 :param width: :class:`.Dimension` instance or callable. 

1392 :param height: :class:`.Dimension` instance or callable. 

1393 :param z_index: When specified, this can be used to bring element in front 

1394 of floating elements. 

1395 :param dont_extend_width: When `True`, don't take up more width then the 

1396 preferred width reported by the control. 

1397 :param dont_extend_height: When `True`, don't take up more width then the 

1398 preferred height reported by the control. 

1399 :param ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore 

1400 the :class:`.UIContent` width when calculating the dimensions. 

1401 :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore 

1402 the :class:`.UIContent` height when calculating the dimensions. 

1403 :param left_margins: A list of :class:`.Margin` instance to be displayed on 

1404 the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin` 

1405 can be one of them in order to show line numbers. 

1406 :param right_margins: Like `left_margins`, but on the other side. 

1407 :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the 

1408 preferred amount of lines/columns to be always visible before/after the 

1409 cursor. When both top and bottom are a very high number, the cursor 

1410 will be centered vertically most of the time. 

1411 :param allow_scroll_beyond_bottom: A `bool` or 

1412 :class:`.Filter` instance. When True, allow scrolling so far, that the 

1413 top part of the content is not visible anymore, while there is still 

1414 empty space available at the bottom of the window. In the Vi editor for 

1415 instance, this is possible. You will see tildes while the top part of 

1416 the body is hidden. 

1417 :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't 

1418 scroll horizontally, but wrap lines instead. 

1419 :param get_vertical_scroll: Callable that takes this window 

1420 instance as input and returns a preferred vertical scroll. 

1421 (When this is `None`, the scroll is only determined by the last and 

1422 current cursor position.) 

1423 :param get_horizontal_scroll: Callable that takes this window 

1424 instance as input and returns a preferred vertical scroll. 

1425 :param always_hide_cursor: A `bool` or 

1426 :class:`.Filter` instance. When True, never display the cursor, even 

1427 when the user control specifies a cursor position. 

1428 :param cursorline: A `bool` or :class:`.Filter` instance. When True, 

1429 display a cursorline. 

1430 :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True, 

1431 display a cursorcolumn. 

1432 :param colorcolumns: A list of :class:`.ColorColumn` instances that 

1433 describe the columns to be highlighted, or a callable that returns such 

1434 a list. 

1435 :param align: :class:`.WindowAlign` value or callable that returns an 

1436 :class:`.WindowAlign` value. alignment of content. 

1437 :param style: A style string. Style to be applied to all the cells in this 

1438 window. (This can be a callable that returns a string.) 

1439 :param char: (string) Character to be used for filling the background. This 

1440 can also be a callable that returns a character. 

1441 :param get_line_prefix: None or a callable that returns formatted text to 

1442 be inserted before a line. It takes a line number (int) and a 

1443 wrap_count and returns formatted text. This can be used for 

1444 implementation of line continuations, things like Vim "breakindent" and 

1445 so on. 

1446 """ 

1447 

1448 def __init__( 

1449 self, 

1450 content: UIControl | None = None, 

1451 width: AnyDimension = None, 

1452 height: AnyDimension = None, 

1453 z_index: int | None = None, 

1454 dont_extend_width: FilterOrBool = False, 

1455 dont_extend_height: FilterOrBool = False, 

1456 ignore_content_width: FilterOrBool = False, 

1457 ignore_content_height: FilterOrBool = False, 

1458 left_margins: Sequence[Margin] | None = None, 

1459 right_margins: Sequence[Margin] | None = None, 

1460 scroll_offsets: ScrollOffsets | None = None, 

1461 allow_scroll_beyond_bottom: FilterOrBool = False, 

1462 wrap_lines: FilterOrBool = False, 

1463 get_vertical_scroll: Callable[[Window], int] | None = None, 

1464 get_horizontal_scroll: Callable[[Window], int] | None = None, 

1465 always_hide_cursor: FilterOrBool = False, 

1466 cursorline: FilterOrBool = False, 

1467 cursorcolumn: FilterOrBool = False, 

1468 colorcolumns: ( 

1469 None | list[ColorColumn] | Callable[[], list[ColorColumn]] 

1470 ) = None, 

1471 align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT, 

1472 style: str | Callable[[], str] = "", 

1473 char: None | str | Callable[[], str] = None, 

1474 get_line_prefix: GetLinePrefixCallable | None = None, 

1475 ) -> None: 

1476 self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom) 

1477 self.always_hide_cursor = to_filter(always_hide_cursor) 

1478 self.wrap_lines = to_filter(wrap_lines) 

1479 self.cursorline = to_filter(cursorline) 

1480 self.cursorcolumn = to_filter(cursorcolumn) 

1481 

1482 self.content = content or DummyControl() 

1483 self.dont_extend_width = to_filter(dont_extend_width) 

1484 self.dont_extend_height = to_filter(dont_extend_height) 

1485 self.ignore_content_width = to_filter(ignore_content_width) 

1486 self.ignore_content_height = to_filter(ignore_content_height) 

1487 self.left_margins = left_margins or [] 

1488 self.right_margins = right_margins or [] 

1489 self.scroll_offsets = scroll_offsets or ScrollOffsets() 

1490 self.get_vertical_scroll = get_vertical_scroll 

1491 self.get_horizontal_scroll = get_horizontal_scroll 

1492 self.colorcolumns = colorcolumns or [] 

1493 self.align = align 

1494 self.style = style 

1495 self.char = char 

1496 self.get_line_prefix = get_line_prefix 

1497 

1498 self.width = width 

1499 self.height = height 

1500 self.z_index = z_index 

1501 

1502 # Cache for the screens generated by the margin. 

1503 self._ui_content_cache: SimpleCache[tuple[int, int, int], UIContent] = ( 

1504 SimpleCache(maxsize=8) 

1505 ) 

1506 self._margin_width_cache: SimpleCache[tuple[Margin, int], int] = SimpleCache( 

1507 maxsize=1 

1508 ) 

1509 

1510 self.reset() 

1511 

1512 def __repr__(self) -> str: 

1513 return f"Window(content={self.content!r})" 

1514 

1515 def reset(self) -> None: 

1516 self.content.reset() 

1517 

1518 #: Scrolling position of the main content. 

1519 self.vertical_scroll = 0 

1520 self.horizontal_scroll = 0 

1521 

1522 # Vertical scroll 2: this is the vertical offset that a line is 

1523 # scrolled if a single line (the one that contains the cursor) consumes 

1524 # all of the vertical space. 

1525 self.vertical_scroll_2 = 0 

1526 

1527 #: Keep render information (mappings between buffer input and render 

1528 #: output.) 

1529 self.render_info: WindowRenderInfo | None = None 

1530 

1531 def _get_margin_width(self, margin: Margin) -> int: 

1532 """ 

1533 Return the width for this margin. 

1534 (Calculate only once per render time.) 

1535 """ 

1536 

1537 # Margin.get_width, needs to have a UIContent instance. 

1538 def get_ui_content() -> UIContent: 

1539 return self._get_ui_content(width=0, height=0) 

1540 

1541 def get_width() -> int: 

1542 return margin.get_width(get_ui_content) 

1543 

1544 key = (margin, get_app().render_counter) 

1545 return self._margin_width_cache.get(key, get_width) 

1546 

1547 def _get_total_margin_width(self) -> int: 

1548 """ 

1549 Calculate and return the width of the margin (left + right). 

1550 """ 

1551 return sum(self._get_margin_width(m) for m in self.left_margins) + sum( 

1552 self._get_margin_width(m) for m in self.right_margins 

1553 ) 

1554 

1555 def preferred_width(self, max_available_width: int) -> Dimension: 

1556 """ 

1557 Calculate the preferred width for this window. 

1558 """ 

1559 

1560 def preferred_content_width() -> int | None: 

1561 """Content width: is only calculated if no exact width for the 

1562 window was given.""" 

1563 if self.ignore_content_width(): 

1564 return None 

1565 

1566 # Calculate the width of the margin. 

1567 total_margin_width = self._get_total_margin_width() 

1568 

1569 # Window of the content. (Can be `None`.) 

1570 preferred_width = self.content.preferred_width( 

1571 max_available_width - total_margin_width 

1572 ) 

1573 

1574 if preferred_width is not None: 

1575 # Include width of the margins. 

1576 preferred_width += total_margin_width 

1577 return preferred_width 

1578 

1579 # Merge. 

1580 return self._merge_dimensions( 

1581 dimension=to_dimension(self.width), 

1582 get_preferred=preferred_content_width, 

1583 dont_extend=self.dont_extend_width(), 

1584 ) 

1585 

1586 def preferred_height(self, width: int, max_available_height: int) -> Dimension: 

1587 """ 

1588 Calculate the preferred height for this window. 

1589 """ 

1590 

1591 def preferred_content_height() -> int | None: 

1592 """Content height: is only calculated if no exact height for the 

1593 window was given.""" 

1594 if self.ignore_content_height(): 

1595 return None 

1596 

1597 total_margin_width = self._get_total_margin_width() 

1598 wrap_lines = self.wrap_lines() 

1599 

1600 return self.content.preferred_height( 

1601 width - total_margin_width, 

1602 max_available_height, 

1603 wrap_lines, 

1604 self.get_line_prefix, 

1605 ) 

1606 

1607 return self._merge_dimensions( 

1608 dimension=to_dimension(self.height), 

1609 get_preferred=preferred_content_height, 

1610 dont_extend=self.dont_extend_height(), 

1611 ) 

1612 

1613 @staticmethod 

1614 def _merge_dimensions( 

1615 dimension: Dimension | None, 

1616 get_preferred: Callable[[], int | None], 

1617 dont_extend: bool = False, 

1618 ) -> Dimension: 

1619 """ 

1620 Take the Dimension from this `Window` class and the received preferred 

1621 size from the `UIControl` and return a `Dimension` to report to the 

1622 parent container. 

1623 """ 

1624 dimension = dimension or Dimension() 

1625 

1626 # When a preferred dimension was explicitly given to the Window, 

1627 # ignore the UIControl. 

1628 preferred: int | None 

1629 

1630 if dimension.preferred_specified: 

1631 preferred = dimension.preferred 

1632 else: 

1633 # Otherwise, calculate the preferred dimension from the UI control 

1634 # content. 

1635 preferred = get_preferred() 

1636 

1637 # When a 'preferred' dimension is given by the UIControl, make sure 

1638 # that it stays within the bounds of the Window. 

1639 if preferred is not None: 

1640 if dimension.max_specified: 

1641 preferred = min(preferred, dimension.max) 

1642 

1643 if dimension.min_specified: 

1644 preferred = max(preferred, dimension.min) 

1645 

1646 # When a `dont_extend` flag has been given, use the preferred dimension 

1647 # also as the max dimension. 

1648 max_: int | None 

1649 min_: int | None 

1650 

1651 if dont_extend and preferred is not None: 

1652 max_ = min(dimension.max, preferred) 

1653 else: 

1654 max_ = dimension.max if dimension.max_specified else None 

1655 

1656 min_ = dimension.min if dimension.min_specified else None 

1657 

1658 return Dimension( 

1659 min=min_, max=max_, preferred=preferred, weight=dimension.weight 

1660 ) 

1661 

1662 def _get_ui_content(self, width: int, height: int) -> UIContent: 

1663 """ 

1664 Create a `UIContent` instance. 

1665 """ 

1666 

1667 def get_content() -> UIContent: 

1668 return self.content.create_content(width=width, height=height) 

1669 

1670 key = (get_app().render_counter, width, height) 

1671 return self._ui_content_cache.get(key, get_content) 

1672 

1673 def _get_digraph_char(self) -> str | None: 

1674 "Return `False`, or the Digraph symbol to be used." 

1675 app = get_app() 

1676 if app.quoted_insert: 

1677 return "^" 

1678 if app.vi_state.waiting_for_digraph: 

1679 if app.vi_state.digraph_symbol1: 

1680 return app.vi_state.digraph_symbol1 

1681 return "?" 

1682 return None 

1683 

1684 def write_to_screen( 

1685 self, 

1686 screen: Screen, 

1687 mouse_handlers: MouseHandlers, 

1688 write_position: WritePosition, 

1689 parent_style: str, 

1690 erase_bg: bool, 

1691 z_index: int | None, 

1692 ) -> None: 

1693 """ 

1694 Write window to screen. This renders the user control, the margins and 

1695 copies everything over to the absolute position at the given screen. 

1696 """ 

1697 # If dont_extend_width/height was given. Then reduce width/height in 

1698 # WritePosition if the parent wanted us to paint in a bigger area. 

1699 # (This happens if this window is bundled with another window in a 

1700 # HSplit/VSplit, but with different size requirements.) 

1701 write_position = WritePosition( 

1702 xpos=write_position.xpos, 

1703 ypos=write_position.ypos, 

1704 width=write_position.width, 

1705 height=write_position.height, 

1706 ) 

1707 

1708 if self.dont_extend_width(): 

1709 write_position.width = min( 

1710 write_position.width, 

1711 self.preferred_width(write_position.width).preferred, 

1712 ) 

1713 

1714 if self.dont_extend_height(): 

1715 write_position.height = min( 

1716 write_position.height, 

1717 self.preferred_height( 

1718 write_position.width, write_position.height 

1719 ).preferred, 

1720 ) 

1721 

1722 # Draw 

1723 z_index = z_index if self.z_index is None else self.z_index 

1724 

1725 draw_func = partial( 

1726 self._write_to_screen_at_index, 

1727 screen, 

1728 mouse_handlers, 

1729 write_position, 

1730 parent_style, 

1731 erase_bg, 

1732 ) 

1733 

1734 if z_index is None or z_index <= 0: 

1735 # When no z_index is given, draw right away. 

1736 draw_func() 

1737 else: 

1738 # Otherwise, postpone. 

1739 screen.draw_with_z_index(z_index=z_index, draw_func=draw_func) 

1740 

1741 def _write_to_screen_at_index( 

1742 self, 

1743 screen: Screen, 

1744 mouse_handlers: MouseHandlers, 

1745 write_position: WritePosition, 

1746 parent_style: str, 

1747 erase_bg: bool, 

1748 ) -> None: 

1749 # Don't bother writing invisible windows. 

1750 # (We save some time, but also avoid applying last-line styling.) 

1751 if write_position.height <= 0 or write_position.width <= 0: 

1752 return 

1753 

1754 # Calculate margin sizes. 

1755 left_margin_widths = [self._get_margin_width(m) for m in self.left_margins] 

1756 right_margin_widths = [self._get_margin_width(m) for m in self.right_margins] 

1757 total_margin_width = sum(left_margin_widths + right_margin_widths) 

1758 

1759 # Render UserControl. 

1760 ui_content = self.content.create_content( 

1761 write_position.width - total_margin_width, write_position.height 

1762 ) 

1763 assert isinstance(ui_content, UIContent) 

1764 

1765 # Scroll content. 

1766 wrap_lines = self.wrap_lines() 

1767 self._scroll( 

1768 ui_content, write_position.width - total_margin_width, write_position.height 

1769 ) 

1770 

1771 # Erase background and fill with `char`. 

1772 self._fill_bg(screen, write_position, erase_bg) 

1773 

1774 # Resolve `align` attribute. 

1775 align = self.align() if callable(self.align) else self.align 

1776 

1777 # Write body 

1778 visible_line_to_row_col, rowcol_to_yx = self._copy_body( 

1779 ui_content, 

1780 screen, 

1781 write_position, 

1782 sum(left_margin_widths), 

1783 write_position.width - total_margin_width, 

1784 self.vertical_scroll, 

1785 self.horizontal_scroll, 

1786 wrap_lines=wrap_lines, 

1787 highlight_lines=True, 

1788 vertical_scroll_2=self.vertical_scroll_2, 

1789 always_hide_cursor=self.always_hide_cursor(), 

1790 has_focus=get_app().layout.current_control == self.content, 

1791 align=align, 

1792 get_line_prefix=self.get_line_prefix, 

1793 ) 

1794 

1795 # Remember render info. (Set before generating the margins. They need this.) 

1796 x_offset = write_position.xpos + sum(left_margin_widths) 

1797 y_offset = write_position.ypos 

1798 

1799 render_info = WindowRenderInfo( 

1800 window=self, 

1801 ui_content=ui_content, 

1802 horizontal_scroll=self.horizontal_scroll, 

1803 vertical_scroll=self.vertical_scroll, 

1804 window_width=write_position.width - total_margin_width, 

1805 window_height=write_position.height, 

1806 configured_scroll_offsets=self.scroll_offsets, 

1807 visible_line_to_row_col=visible_line_to_row_col, 

1808 rowcol_to_yx=rowcol_to_yx, 

1809 x_offset=x_offset, 

1810 y_offset=y_offset, 

1811 wrap_lines=wrap_lines, 

1812 ) 

1813 self.render_info = render_info 

1814 

1815 # Set mouse handlers. 

1816 def mouse_handler(mouse_event: MouseEvent) -> NotImplementedOrNone: 

1817 """ 

1818 Wrapper around the mouse_handler of the `UIControl` that turns 

1819 screen coordinates into line coordinates. 

1820 Returns `NotImplemented` if no UI invalidation should be done. 

1821 """ 

1822 # Don't handle mouse events outside of the current modal part of 

1823 # the UI. 

1824 if self not in get_app().layout.walk_through_modal_area(): 

1825 return NotImplemented 

1826 

1827 # Find row/col position first. 

1828 yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()} 

1829 y = mouse_event.position.y 

1830 x = mouse_event.position.x 

1831 

1832 # If clicked below the content area, look for a position in the 

1833 # last line instead. 

1834 max_y = write_position.ypos + len(visible_line_to_row_col) - 1 

1835 y = min(max_y, y) 

1836 result: NotImplementedOrNone 

1837 

1838 while x >= 0: 

1839 try: 

1840 row, col = yx_to_rowcol[y, x] 

1841 except KeyError: 

1842 # Try again. (When clicking on the right side of double 

1843 # width characters, or on the right side of the input.) 

1844 x -= 1 

1845 else: 

1846 # Found position, call handler of UIControl. 

1847 result = self.content.mouse_handler( 

1848 MouseEvent( 

1849 position=Point(x=col, y=row), 

1850 event_type=mouse_event.event_type, 

1851 button=mouse_event.button, 

1852 modifiers=mouse_event.modifiers, 

1853 ) 

1854 ) 

1855 break 

1856 else: 

1857 # nobreak. 

1858 # (No x/y coordinate found for the content. This happens in 

1859 # case of a DummyControl, that does not have any content. 

1860 # Report (0,0) instead.) 

1861 result = self.content.mouse_handler( 

1862 MouseEvent( 

1863 position=Point(x=0, y=0), 

1864 event_type=mouse_event.event_type, 

1865 button=mouse_event.button, 

1866 modifiers=mouse_event.modifiers, 

1867 ) 

1868 ) 

1869 

1870 # If it returns NotImplemented, handle it here. 

1871 if result == NotImplemented: 

1872 result = self._mouse_handler(mouse_event) 

1873 

1874 return result 

1875 

1876 mouse_handlers.set_mouse_handler_for_range( 

1877 x_min=write_position.xpos + sum(left_margin_widths), 

1878 x_max=write_position.xpos + write_position.width - total_margin_width, 

1879 y_min=write_position.ypos, 

1880 y_max=write_position.ypos + write_position.height, 

1881 handler=mouse_handler, 

1882 ) 

1883 

1884 # Render and copy margins. 

1885 move_x = 0 

1886 

1887 def render_margin(m: Margin, width: int) -> UIContent: 

1888 "Render margin. Return `Screen`." 

1889 # Retrieve margin fragments. 

1890 fragments = m.create_margin(render_info, width, write_position.height) 

1891 

1892 # Turn it into a UIContent object. 

1893 # already rendered those fragments using this size.) 

1894 return FormattedTextControl(fragments).create_content( 

1895 width + 1, write_position.height 

1896 ) 

1897 

1898 for m, width in zip(self.left_margins, left_margin_widths): 

1899 if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.) 

1900 # Create screen for margin. 

1901 margin_content = render_margin(m, width) 

1902 

1903 # Copy and shift X. 

1904 self._copy_margin(margin_content, screen, write_position, move_x, width) 

1905 move_x += width 

1906 

1907 move_x = write_position.width - sum(right_margin_widths) 

1908 

1909 for m, width in zip(self.right_margins, right_margin_widths): 

1910 # Create screen for margin. 

1911 margin_content = render_margin(m, width) 

1912 

1913 # Copy and shift X. 

1914 self._copy_margin(margin_content, screen, write_position, move_x, width) 

1915 move_x += width 

1916 

1917 # Apply 'self.style' 

1918 self._apply_style(screen, write_position, parent_style) 

1919 

1920 # Tell the screen that this user control has been painted at this 

1921 # position. 

1922 screen.visible_windows_to_write_positions[self] = write_position 

1923 

1924 def _copy_body( 

1925 self, 

1926 ui_content: UIContent, 

1927 new_screen: Screen, 

1928 write_position: WritePosition, 

1929 move_x: int, 

1930 width: int, 

1931 vertical_scroll: int = 0, 

1932 horizontal_scroll: int = 0, 

1933 wrap_lines: bool = False, 

1934 highlight_lines: bool = False, 

1935 vertical_scroll_2: int = 0, 

1936 always_hide_cursor: bool = False, 

1937 has_focus: bool = False, 

1938 align: WindowAlign = WindowAlign.LEFT, 

1939 get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None, 

1940 ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]: 

1941 """ 

1942 Copy the UIContent into the output screen. 

1943 Return (visible_line_to_row_col, rowcol_to_yx) tuple. 

1944 

1945 :param get_line_prefix: None or a callable that takes a line number 

1946 (int) and a wrap_count (int) and returns formatted text. 

1947 """ 

1948 xpos = write_position.xpos + move_x 

1949 ypos = write_position.ypos 

1950 line_count = ui_content.line_count 

1951 new_buffer = new_screen.data_buffer 

1952 empty_char = _CHAR_CACHE["", ""] 

1953 

1954 # Map visible line number to (row, col) of input. 

1955 # 'col' will always be zero if line wrapping is off. 

1956 visible_line_to_row_col: dict[int, tuple[int, int]] = {} 

1957 

1958 # Maps (row, col) from the input to (y, x) screen coordinates. 

1959 rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {} 

1960 

1961 def copy_line( 

1962 line: StyleAndTextTuples, 

1963 lineno: int, 

1964 x: int, 

1965 y: int, 

1966 is_input: bool = False, 

1967 ) -> tuple[int, int]: 

1968 """ 

1969 Copy over a single line to the output screen. This can wrap over 

1970 multiple lines in the output. It will call the prefix (prompt) 

1971 function before every line. 

1972 """ 

1973 if is_input: 

1974 current_rowcol_to_yx = rowcol_to_yx 

1975 else: 

1976 current_rowcol_to_yx = {} # Throwaway dictionary. 

1977 

1978 # Draw line prefix. 

1979 if is_input and get_line_prefix: 

1980 prompt = to_formatted_text(get_line_prefix(lineno, 0)) 

1981 x, y = copy_line(prompt, lineno, x, y, is_input=False) 

1982 

1983 # Scroll horizontally. 

1984 skipped = 0 # Characters skipped because of horizontal scrolling. 

1985 if horizontal_scroll and is_input: 

1986 h_scroll = horizontal_scroll 

1987 line = explode_text_fragments(line) 

1988 while h_scroll > 0 and line: 

1989 h_scroll -= get_cwidth(line[0][1]) 

1990 skipped += 1 

1991 del line[:1] # Remove first character. 

1992 

1993 x -= h_scroll # When scrolling over double width character, 

1994 # this can end up being negative. 

1995 

1996 # Align this line. (Note that this doesn't work well when we use 

1997 # get_line_prefix and that function returns variable width prefixes.) 

1998 if align == WindowAlign.CENTER: 

1999 line_width = fragment_list_width(line) 

2000 if line_width < width: 

2001 x += (width - line_width) // 2 

2002 elif align == WindowAlign.RIGHT: 

2003 line_width = fragment_list_width(line) 

2004 if line_width < width: 

2005 x += width - line_width 

2006 

2007 col = 0 

2008 wrap_count = 0 

2009 for style, text, *_ in line: 

2010 new_buffer_row = new_buffer[y + ypos] 

2011 

2012 # Remember raw VT escape sequences. (E.g. FinalTerm's 

2013 # escape sequences.) 

2014 if "[ZeroWidthEscape]" in style: 

2015 new_screen.zero_width_escapes[y + ypos][x + xpos] += text 

2016 continue 

2017 

2018 for c in text: 

2019 char = _CHAR_CACHE[c, style] 

2020 char_width = char.width 

2021 

2022 # Wrap when the line width is exceeded. 

2023 if wrap_lines and x + char_width > width: 

2024 visible_line_to_row_col[y + 1] = ( 

2025 lineno, 

2026 visible_line_to_row_col[y][1] + x, 

2027 ) 

2028 y += 1 

2029 wrap_count += 1 

2030 x = 0 

2031 

2032 # Insert line prefix (continuation prompt). 

2033 if is_input and get_line_prefix: 

2034 prompt = to_formatted_text( 

2035 get_line_prefix(lineno, wrap_count) 

2036 ) 

2037 x, y = copy_line(prompt, lineno, x, y, is_input=False) 

2038 

2039 new_buffer_row = new_buffer[y + ypos] 

2040 

2041 if y >= write_position.height: 

2042 return x, y # Break out of all for loops. 

2043 

2044 # Set character in screen and shift 'x'. 

2045 if x >= 0 and y >= 0 and x < width: 

2046 new_buffer_row[x + xpos] = char 

2047 

2048 # When we print a multi width character, make sure 

2049 # to erase the neighbors positions in the screen. 

2050 # (The empty string if different from everything, 

2051 # so next redraw this cell will repaint anyway.) 

2052 if char_width > 1: 

2053 for i in range(1, char_width): 

2054 new_buffer_row[x + xpos + i] = empty_char 

2055 

2056 # If this is a zero width characters, then it's 

2057 # probably part of a decomposed unicode character. 

2058 # See: https://en.wikipedia.org/wiki/Unicode_equivalence 

2059 # Merge it in the previous cell. 

2060 elif char_width == 0: 

2061 # Handle all character widths. If the previous 

2062 # character is a multiwidth character, then 

2063 # merge it two positions back. 

2064 for pw in [2, 1]: # Previous character width. 

2065 if ( 

2066 x - pw >= 0 

2067 and new_buffer_row[x + xpos - pw].width == pw 

2068 ): 

2069 prev_char = new_buffer_row[x + xpos - pw] 

2070 char2 = _CHAR_CACHE[ 

2071 prev_char.char + c, prev_char.style 

2072 ] 

2073 new_buffer_row[x + xpos - pw] = char2 

2074 

2075 # Keep track of write position for each character. 

2076 current_rowcol_to_yx[lineno, col + skipped] = ( 

2077 y + ypos, 

2078 x + xpos, 

2079 ) 

2080 

2081 col += 1 

2082 x += char_width 

2083 return x, y 

2084 

2085 # Copy content. 

2086 def copy() -> int: 

2087 y = -vertical_scroll_2 

2088 lineno = vertical_scroll 

2089 

2090 while y < write_position.height and lineno < line_count: 

2091 # Take the next line and copy it in the real screen. 

2092 line = ui_content.get_line(lineno) 

2093 

2094 visible_line_to_row_col[y] = (lineno, horizontal_scroll) 

2095 

2096 # Copy margin and actual line. 

2097 x = 0 

2098 x, y = copy_line(line, lineno, x, y, is_input=True) 

2099 

2100 lineno += 1 

2101 y += 1 

2102 return y 

2103 

2104 copy() 

2105 

2106 def cursor_pos_to_screen_pos(row: int, col: int) -> Point: 

2107 "Translate row/col from UIContent to real Screen coordinates." 

2108 try: 

2109 y, x = rowcol_to_yx[row, col] 

2110 except KeyError: 

2111 # Normally this should never happen. (It is a bug, if it happens.) 

2112 # But to be sure, return (0, 0) 

2113 return Point(x=0, y=0) 

2114 

2115 # raise ValueError( 

2116 # 'Invalid position. row=%r col=%r, vertical_scroll=%r, ' 

2117 # 'horizontal_scroll=%r, height=%r' % 

2118 # (row, col, vertical_scroll, horizontal_scroll, write_position.height)) 

2119 else: 

2120 return Point(x=x, y=y) 

2121 

2122 # Set cursor and menu positions. 

2123 if ui_content.cursor_position: 

2124 screen_cursor_position = cursor_pos_to_screen_pos( 

2125 ui_content.cursor_position.y, ui_content.cursor_position.x 

2126 ) 

2127 

2128 if has_focus: 

2129 new_screen.set_cursor_position(self, screen_cursor_position) 

2130 

2131 if always_hide_cursor: 

2132 new_screen.show_cursor = False 

2133 else: 

2134 new_screen.show_cursor = ui_content.show_cursor 

2135 

2136 self._highlight_digraph(new_screen) 

2137 

2138 if highlight_lines: 

2139 self._highlight_cursorlines( 

2140 new_screen, 

2141 screen_cursor_position, 

2142 xpos, 

2143 ypos, 

2144 width, 

2145 write_position.height, 

2146 ) 

2147 

2148 # Draw input characters from the input processor queue. 

2149 if has_focus and ui_content.cursor_position: 

2150 self._show_key_processor_key_buffer(new_screen) 

2151 

2152 # Set menu position. 

2153 if ui_content.menu_position: 

2154 new_screen.set_menu_position( 

2155 self, 

2156 cursor_pos_to_screen_pos( 

2157 ui_content.menu_position.y, ui_content.menu_position.x 

2158 ), 

2159 ) 

2160 

2161 # Update output screen height. 

2162 new_screen.height = max(new_screen.height, ypos + write_position.height) 

2163 

2164 return visible_line_to_row_col, rowcol_to_yx 

2165 

2166 def _fill_bg( 

2167 self, screen: Screen, write_position: WritePosition, erase_bg: bool 

2168 ) -> None: 

2169 """ 

2170 Erase/fill the background. 

2171 (Useful for floats and when a `char` has been given.) 

2172 """ 

2173 char: str | None 

2174 if callable(self.char): 

2175 char = self.char() 

2176 else: 

2177 char = self.char 

2178 

2179 if erase_bg or char: 

2180 wp = write_position 

2181 char_obj = _CHAR_CACHE[char or " ", ""] 

2182 

2183 for y in range(wp.ypos, wp.ypos + wp.height): 

2184 row = screen.data_buffer[y] 

2185 for x in range(wp.xpos, wp.xpos + wp.width): 

2186 row[x] = char_obj 

2187 

2188 def _apply_style( 

2189 self, new_screen: Screen, write_position: WritePosition, parent_style: str 

2190 ) -> None: 

2191 # Apply `self.style`. 

2192 style = parent_style + " " + to_str(self.style) 

2193 

2194 new_screen.fill_area(write_position, style=style, after=False) 

2195 

2196 # Apply the 'last-line' class to the last line of each Window. This can 

2197 # be used to apply an 'underline' to the user control. 

2198 wp = WritePosition( 

2199 write_position.xpos, 

2200 write_position.ypos + write_position.height - 1, 

2201 write_position.width, 

2202 1, 

2203 ) 

2204 new_screen.fill_area(wp, "class:last-line", after=True) 

2205 

2206 def _highlight_digraph(self, new_screen: Screen) -> None: 

2207 """ 

2208 When we are in Vi digraph mode, put a question mark underneath the 

2209 cursor. 

2210 """ 

2211 digraph_char = self._get_digraph_char() 

2212 if digraph_char: 

2213 cpos = new_screen.get_cursor_position(self) 

2214 new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ 

2215 digraph_char, "class:digraph" 

2216 ] 

2217 

2218 def _show_key_processor_key_buffer(self, new_screen: Screen) -> None: 

2219 """ 

2220 When the user is typing a key binding that consists of several keys, 

2221 display the last pressed key if the user is in insert mode and the key 

2222 is meaningful to be displayed. 

2223 E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the 

2224 first 'j' needs to be displayed in order to get some feedback. 

2225 """ 

2226 app = get_app() 

2227 key_buffer = app.key_processor.key_buffer 

2228 

2229 if key_buffer and _in_insert_mode() and not app.is_done: 

2230 # The textual data for the given key. (Can be a VT100 escape 

2231 # sequence.) 

2232 data = key_buffer[-1].data 

2233 

2234 # Display only if this is a 1 cell width character. 

2235 if get_cwidth(data) == 1: 

2236 cpos = new_screen.get_cursor_position(self) 

2237 new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[ 

2238 data, "class:partial-key-binding" 

2239 ] 

2240 

2241 def _highlight_cursorlines( 

2242 self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int 

2243 ) -> None: 

2244 """ 

2245 Highlight cursor row/column. 

2246 """ 

2247 cursor_line_style = " class:cursor-line " 

2248 cursor_column_style = " class:cursor-column " 

2249 

2250 data_buffer = new_screen.data_buffer 

2251 

2252 # Highlight cursor line. 

2253 if self.cursorline(): 

2254 row = data_buffer[cpos.y] 

2255 for x in range(x, x + width): 

2256 original_char = row[x] 

2257 row[x] = _CHAR_CACHE[ 

2258 original_char.char, original_char.style + cursor_line_style 

2259 ] 

2260 

2261 # Highlight cursor column. 

2262 if self.cursorcolumn(): 

2263 for y2 in range(y, y + height): 

2264 row = data_buffer[y2] 

2265 original_char = row[cpos.x] 

2266 row[cpos.x] = _CHAR_CACHE[ 

2267 original_char.char, original_char.style + cursor_column_style 

2268 ] 

2269 

2270 # Highlight color columns 

2271 colorcolumns = self.colorcolumns 

2272 if callable(colorcolumns): 

2273 colorcolumns = colorcolumns() 

2274 

2275 for cc in colorcolumns: 

2276 assert isinstance(cc, ColorColumn) 

2277 column = cc.position 

2278 

2279 if column < x + width: # Only draw when visible. 

2280 color_column_style = " " + cc.style 

2281 

2282 for y2 in range(y, y + height): 

2283 row = data_buffer[y2] 

2284 original_char = row[column + x] 

2285 row[column + x] = _CHAR_CACHE[ 

2286 original_char.char, original_char.style + color_column_style 

2287 ] 

2288 

2289 def _copy_margin( 

2290 self, 

2291 margin_content: UIContent, 

2292 new_screen: Screen, 

2293 write_position: WritePosition, 

2294 move_x: int, 

2295 width: int, 

2296 ) -> None: 

2297 """ 

2298 Copy characters from the margin screen to the real screen. 

2299 """ 

2300 xpos = write_position.xpos + move_x 

2301 ypos = write_position.ypos 

2302 

2303 margin_write_position = WritePosition(xpos, ypos, width, write_position.height) 

2304 self._copy_body(margin_content, new_screen, margin_write_position, 0, width) 

2305 

2306 def _scroll(self, ui_content: UIContent, width: int, height: int) -> None: 

2307 """ 

2308 Scroll body. Ensure that the cursor is visible. 

2309 """ 

2310 if self.wrap_lines(): 

2311 func = self._scroll_when_linewrapping 

2312 else: 

2313 func = self._scroll_without_linewrapping 

2314 

2315 func(ui_content, width, height) 

2316 

2317 def _scroll_when_linewrapping( 

2318 self, ui_content: UIContent, width: int, height: int 

2319 ) -> None: 

2320 """ 

2321 Scroll to make sure the cursor position is visible and that we maintain 

2322 the requested scroll offset. 

2323 

2324 Set `self.horizontal_scroll/vertical_scroll`. 

2325 """ 

2326 scroll_offsets_bottom = self.scroll_offsets.bottom 

2327 scroll_offsets_top = self.scroll_offsets.top 

2328 

2329 # We don't have horizontal scrolling. 

2330 self.horizontal_scroll = 0 

2331 

2332 def get_line_height(lineno: int) -> int: 

2333 return ui_content.get_height_for_line(lineno, width, self.get_line_prefix) 

2334 

2335 # When there is no space, reset `vertical_scroll_2` to zero and abort. 

2336 # This can happen if the margin is bigger than the window width. 

2337 # Otherwise the text height will become "infinite" (a big number) and 

2338 # the copy_line will spend a huge amount of iterations trying to render 

2339 # nothing. 

2340 if width <= 0: 

2341 self.vertical_scroll = ui_content.cursor_position.y 

2342 self.vertical_scroll_2 = 0 

2343 return 

2344 

2345 # If the current line consumes more than the whole window height, 

2346 # then we have to scroll vertically inside this line. (We don't take 

2347 # the scroll offsets into account for this.) 

2348 # Also, ignore the scroll offsets in this case. Just set the vertical 

2349 # scroll to this line. 

2350 line_height = get_line_height(ui_content.cursor_position.y) 

2351 if line_height > height - scroll_offsets_top: 

2352 # Calculate the height of the text before the cursor (including 

2353 # line prefixes). 

2354 text_before_height = ui_content.get_height_for_line( 

2355 ui_content.cursor_position.y, 

2356 width, 

2357 self.get_line_prefix, 

2358 slice_stop=ui_content.cursor_position.x, 

2359 ) 

2360 

2361 # Adjust scroll offset. 

2362 self.vertical_scroll = ui_content.cursor_position.y 

2363 self.vertical_scroll_2 = min( 

2364 text_before_height - 1, # Keep the cursor visible. 

2365 line_height 

2366 - height, # Avoid blank lines at the bottom when scrolling up again. 

2367 self.vertical_scroll_2, 

2368 ) 

2369 self.vertical_scroll_2 = max( 

2370 0, text_before_height - height, self.vertical_scroll_2 

2371 ) 

2372 return 

2373 else: 

2374 self.vertical_scroll_2 = 0 

2375 

2376 # Current line doesn't consume the whole height. Take scroll offsets into account. 

2377 def get_min_vertical_scroll() -> int: 

2378 # Make sure that the cursor line is not below the bottom. 

2379 # (Calculate how many lines can be shown between the cursor and the .) 

2380 used_height = 0 

2381 prev_lineno = ui_content.cursor_position.y 

2382 

2383 for lineno in range(ui_content.cursor_position.y, -1, -1): 

2384 used_height += get_line_height(lineno) 

2385 

2386 if used_height > height - scroll_offsets_bottom: 

2387 return prev_lineno 

2388 else: 

2389 prev_lineno = lineno 

2390 return 0 

2391 

2392 def get_max_vertical_scroll() -> int: 

2393 # Make sure that the cursor line is not above the top. 

2394 prev_lineno = ui_content.cursor_position.y 

2395 used_height = 0 

2396 

2397 for lineno in range(ui_content.cursor_position.y - 1, -1, -1): 

2398 used_height += get_line_height(lineno) 

2399 

2400 if used_height > scroll_offsets_top: 

2401 return prev_lineno 

2402 else: 

2403 prev_lineno = lineno 

2404 return prev_lineno 

2405 

2406 def get_topmost_visible() -> int: 

2407 """ 

2408 Calculate the upper most line that can be visible, while the bottom 

2409 is still visible. We should not allow scroll more than this if 

2410 `allow_scroll_beyond_bottom` is false. 

2411 """ 

2412 prev_lineno = ui_content.line_count - 1 

2413 used_height = 0 

2414 for lineno in range(ui_content.line_count - 1, -1, -1): 

2415 used_height += get_line_height(lineno) 

2416 if used_height > height: 

2417 return prev_lineno 

2418 else: 

2419 prev_lineno = lineno 

2420 return prev_lineno 

2421 

2422 # Scroll vertically. (Make sure that the whole line which contains the 

2423 # cursor is visible. 

2424 topmost_visible = get_topmost_visible() 

2425 

2426 # Note: the `min(topmost_visible, ...)` is to make sure that we 

2427 # don't require scrolling up because of the bottom scroll offset, 

2428 # when we are at the end of the document. 

2429 self.vertical_scroll = max( 

2430 self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll()) 

2431 ) 

2432 self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll()) 

2433 

2434 # Disallow scrolling beyond bottom? 

2435 if not self.allow_scroll_beyond_bottom(): 

2436 self.vertical_scroll = min(self.vertical_scroll, topmost_visible) 

2437 

2438 def _scroll_without_linewrapping( 

2439 self, ui_content: UIContent, width: int, height: int 

2440 ) -> None: 

2441 """ 

2442 Scroll to make sure the cursor position is visible and that we maintain 

2443 the requested scroll offset. 

2444 

2445 Set `self.horizontal_scroll/vertical_scroll`. 

2446 """ 

2447 cursor_position = ui_content.cursor_position or Point(x=0, y=0) 

2448 

2449 # Without line wrapping, we will never have to scroll vertically inside 

2450 # a single line. 

2451 self.vertical_scroll_2 = 0 

2452 

2453 if ui_content.line_count == 0: 

2454 self.vertical_scroll = 0 

2455 self.horizontal_scroll = 0 

2456 return 

2457 else: 

2458 current_line_text = fragment_list_to_text( 

2459 ui_content.get_line(cursor_position.y) 

2460 ) 

2461 

2462 def do_scroll( 

2463 current_scroll: int, 

2464 scroll_offset_start: int, 

2465 scroll_offset_end: int, 

2466 cursor_pos: int, 

2467 window_size: int, 

2468 content_size: int, 

2469 ) -> int: 

2470 "Scrolling algorithm. Used for both horizontal and vertical scrolling." 

2471 # Calculate the scroll offset to apply. 

2472 # This can obviously never be more than have the screen size. Also, when the 

2473 # cursor appears at the top or bottom, we don't apply the offset. 

2474 scroll_offset_start = int( 

2475 min(scroll_offset_start, window_size / 2, cursor_pos) 

2476 ) 

2477 scroll_offset_end = int( 

2478 min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos) 

2479 ) 

2480 

2481 # Prevent negative scroll offsets. 

2482 if current_scroll < 0: 

2483 current_scroll = 0 

2484 

2485 # Scroll back if we scrolled to much and there's still space to show more of the document. 

2486 if ( 

2487 not self.allow_scroll_beyond_bottom() 

2488 and current_scroll > content_size - window_size 

2489 ): 

2490 current_scroll = max(0, content_size - window_size) 

2491 

2492 # Scroll up if cursor is before visible part. 

2493 if current_scroll > cursor_pos - scroll_offset_start: 

2494 current_scroll = max(0, cursor_pos - scroll_offset_start) 

2495 

2496 # Scroll down if cursor is after visible part. 

2497 if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end: 

2498 current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end 

2499 

2500 return current_scroll 

2501 

2502 # When a preferred scroll is given, take that first into account. 

2503 if self.get_vertical_scroll: 

2504 self.vertical_scroll = self.get_vertical_scroll(self) 

2505 assert isinstance(self.vertical_scroll, int) 

2506 if self.get_horizontal_scroll: 

2507 self.horizontal_scroll = self.get_horizontal_scroll(self) 

2508 assert isinstance(self.horizontal_scroll, int) 

2509 

2510 # Update horizontal/vertical scroll to make sure that the cursor 

2511 # remains visible. 

2512 offsets = self.scroll_offsets 

2513 

2514 self.vertical_scroll = do_scroll( 

2515 current_scroll=self.vertical_scroll, 

2516 scroll_offset_start=offsets.top, 

2517 scroll_offset_end=offsets.bottom, 

2518 cursor_pos=ui_content.cursor_position.y, 

2519 window_size=height, 

2520 content_size=ui_content.line_count, 

2521 ) 

2522 

2523 if self.get_line_prefix: 

2524 current_line_prefix_width = fragment_list_width( 

2525 to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0)) 

2526 ) 

2527 else: 

2528 current_line_prefix_width = 0 

2529 

2530 self.horizontal_scroll = do_scroll( 

2531 current_scroll=self.horizontal_scroll, 

2532 scroll_offset_start=offsets.left, 

2533 scroll_offset_end=offsets.right, 

2534 cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]), 

2535 window_size=width - current_line_prefix_width, 

2536 # We can only analyze the current line. Calculating the width off 

2537 # all the lines is too expensive. 

2538 content_size=max( 

2539 get_cwidth(current_line_text), self.horizontal_scroll + width 

2540 ), 

2541 ) 

2542 

2543 def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone: 

2544 """ 

2545 Mouse handler. Called when the UI control doesn't handle this 

2546 particular event. 

2547 

2548 Return `NotImplemented` if nothing was done as a consequence of this 

2549 key binding (no UI invalidate required in that case). 

2550 """ 

2551 if mouse_event.event_type == MouseEventType.SCROLL_DOWN: 

2552 self._scroll_down() 

2553 return None 

2554 elif mouse_event.event_type == MouseEventType.SCROLL_UP: 

2555 self._scroll_up() 

2556 return None 

2557 

2558 return NotImplemented 

2559 

2560 def _scroll_down(self) -> None: 

2561 "Scroll window down." 

2562 info = self.render_info 

2563 

2564 if info is None: 

2565 return 

2566 

2567 if self.vertical_scroll < info.content_height - info.window_height: 

2568 if info.cursor_position.y <= info.configured_scroll_offsets.top: 

2569 self.content.move_cursor_down() 

2570 

2571 self.vertical_scroll += 1 

2572 

2573 def _scroll_up(self) -> None: 

2574 "Scroll window up." 

2575 info = self.render_info 

2576 

2577 if info is None: 

2578 return 

2579 

2580 if info.vertical_scroll > 0: 

2581 # TODO: not entirely correct yet in case of line wrapping and long lines. 

2582 if ( 

2583 info.cursor_position.y 

2584 >= info.window_height - 1 - info.configured_scroll_offsets.bottom 

2585 ): 

2586 self.content.move_cursor_up() 

2587 

2588 self.vertical_scroll -= 1 

2589 

2590 def get_key_bindings(self) -> KeyBindingsBase | None: 

2591 return self.content.get_key_bindings() 

2592 

2593 def get_children(self) -> list[Container]: 

2594 return [] 

2595 

2596 

2597class ConditionalContainer(Container): 

2598 """ 

2599 Wrapper around any other container that can change the visibility. The 

2600 received `filter` determines whether the given container should be 

2601 displayed or not. 

2602 

2603 :param content: :class:`.Container` instance. 

2604 :param filter: :class:`.Filter` instance. 

2605 """ 

2606 

2607 def __init__( 

2608 self, 

2609 content: AnyContainer, 

2610 filter: FilterOrBool, 

2611 alternative_content: AnyContainer | None = None, 

2612 ) -> None: 

2613 self.content = to_container(content) 

2614 self.alternative_content = ( 

2615 to_container(alternative_content) 

2616 if alternative_content is not None 

2617 else None 

2618 ) 

2619 self.filter = to_filter(filter) 

2620 

2621 def __repr__(self) -> str: 

2622 return f"ConditionalContainer({self.content!r}, filter={self.filter!r})" 

2623 

2624 def reset(self) -> None: 

2625 self.content.reset() 

2626 

2627 def preferred_width(self, max_available_width: int) -> Dimension: 

2628 if self.filter(): 

2629 return self.content.preferred_width(max_available_width) 

2630 elif self.alternative_content is not None: 

2631 return self.alternative_content.preferred_width(max_available_width) 

2632 else: 

2633 return Dimension.zero() 

2634 

2635 def preferred_height(self, width: int, max_available_height: int) -> Dimension: 

2636 if self.filter(): 

2637 return self.content.preferred_height(width, max_available_height) 

2638 elif self.alternative_content is not None: 

2639 return self.alternative_content.preferred_height( 

2640 width, max_available_height 

2641 ) 

2642 else: 

2643 return Dimension.zero() 

2644 

2645 def write_to_screen( 

2646 self, 

2647 screen: Screen, 

2648 mouse_handlers: MouseHandlers, 

2649 write_position: WritePosition, 

2650 parent_style: str, 

2651 erase_bg: bool, 

2652 z_index: int | None, 

2653 ) -> None: 

2654 if self.filter(): 

2655 return self.content.write_to_screen( 

2656 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index 

2657 ) 

2658 elif self.alternative_content is not None: 

2659 return self.alternative_content.write_to_screen( 

2660 screen, 

2661 mouse_handlers, 

2662 write_position, 

2663 parent_style, 

2664 erase_bg, 

2665 z_index, 

2666 ) 

2667 

2668 def get_children(self) -> list[Container]: 

2669 result = [self.content] 

2670 if self.alternative_content is not None: 

2671 result.append(self.alternative_content) 

2672 return result 

2673 

2674 

2675class DynamicContainer(Container): 

2676 """ 

2677 Container class that dynamically returns any Container. 

2678 

2679 :param get_container: Callable that returns a :class:`.Container` instance 

2680 or any widget with a ``__pt_container__`` method. 

2681 """ 

2682 

2683 def __init__(self, get_container: Callable[[], AnyContainer]) -> None: 

2684 self.get_container = get_container 

2685 

2686 def _get_container(self) -> Container: 

2687 """ 

2688 Return the current container object. 

2689 

2690 We call `to_container`, because `get_container` can also return a 

2691 widget with a ``__pt_container__`` method. 

2692 """ 

2693 obj = self.get_container() 

2694 return to_container(obj) 

2695 

2696 def reset(self) -> None: 

2697 self._get_container().reset() 

2698 

2699 def preferred_width(self, max_available_width: int) -> Dimension: 

2700 return self._get_container().preferred_width(max_available_width) 

2701 

2702 def preferred_height(self, width: int, max_available_height: int) -> Dimension: 

2703 return self._get_container().preferred_height(width, max_available_height) 

2704 

2705 def write_to_screen( 

2706 self, 

2707 screen: Screen, 

2708 mouse_handlers: MouseHandlers, 

2709 write_position: WritePosition, 

2710 parent_style: str, 

2711 erase_bg: bool, 

2712 z_index: int | None, 

2713 ) -> None: 

2714 self._get_container().write_to_screen( 

2715 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index 

2716 ) 

2717 

2718 def is_modal(self) -> bool: 

2719 return False 

2720 

2721 def get_key_bindings(self) -> KeyBindingsBase | None: 

2722 # Key bindings will be collected when `layout.walk()` finds the child 

2723 # container. 

2724 return None 

2725 

2726 def get_children(self) -> list[Container]: 

2727 # Here we have to return the current active container itself, not its 

2728 # children. Otherwise, we run into issues where `layout.walk()` will 

2729 # never see an object of type `Window` if this contains a window. We 

2730 # can't/shouldn't proxy the "isinstance" check. 

2731 return [self._get_container()] 

2732 

2733 

2734def to_container(container: AnyContainer) -> Container: 

2735 """ 

2736 Make sure that the given object is a :class:`.Container`. 

2737 """ 

2738 if isinstance(container, Container): 

2739 return container 

2740 elif hasattr(container, "__pt_container__"): 

2741 return to_container(container.__pt_container__()) 

2742 else: 

2743 raise ValueError(f"Not a container object: {container!r}") 

2744 

2745 

2746def to_window(container: AnyContainer) -> Window: 

2747 """ 

2748 Make sure that the given argument is a :class:`.Window`. 

2749 """ 

2750 if isinstance(container, Window): 

2751 return container 

2752 elif hasattr(container, "__pt_container__"): 

2753 return to_window(cast("MagicContainer", container).__pt_container__()) 

2754 else: 

2755 raise ValueError(f"Not a Window object: {container!r}.") 

2756 

2757 

2758def is_container(value: object) -> TypeGuard[AnyContainer]: 

2759 """ 

2760 Checks whether the given value is a container object 

2761 (for use in assert statements). 

2762 """ 

2763 if isinstance(value, Container): 

2764 return True 

2765 if hasattr(value, "__pt_container__"): 

2766 return is_container(cast("MagicContainer", value).__pt_container__()) 

2767 return False