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

968 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-25 06:05 +0000

1""" 

2Container for the layout. 

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

4""" 

5from __future__ import annotations 

6 

7from abc import ABCMeta, abstractmethod 

8from enum import Enum 

9from functools import partial 

10from typing import TYPE_CHECKING, Callable, Sequence, Union, cast 

11 

12from prompt_toolkit.application.current import get_app 

13from prompt_toolkit.cache import SimpleCache 

14from prompt_toolkit.data_structures import Point 

15from prompt_toolkit.filters import ( 

16 FilterOrBool, 

17 emacs_insert_mode, 

18 to_filter, 

19 vi_insert_mode, 

20) 

21from prompt_toolkit.formatted_text import ( 

22 AnyFormattedText, 

23 StyleAndTextTuples, 

24 to_formatted_text, 

25) 

26from prompt_toolkit.formatted_text.utils import ( 

27 fragment_list_to_text, 

28 fragment_list_width, 

29) 

30from prompt_toolkit.key_binding import KeyBindingsBase 

31from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 

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

33 

34from .controls import ( 

35 DummyControl, 

36 FormattedTextControl, 

37 GetLinePrefixCallable, 

38 UIContent, 

39 UIControl, 

40) 

41from .dimension import ( 

42 AnyDimension, 

43 Dimension, 

44 max_layout_dimensions, 

45 sum_layout_dimensions, 

46 to_dimension, 

47) 

48from .margins import Margin 

49from .mouse_handlers import MouseHandlers 

50from .screen import _CHAR_CACHE, Screen, WritePosition 

51from .utils import explode_text_fragments 

52 

53if TYPE_CHECKING: 

54 from typing_extensions import Protocol, TypeGuard 

55 

56 from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone 

57 

58 

59__all__ = [ 

60 "AnyContainer", 

61 "Container", 

62 "HorizontalAlign", 

63 "VerticalAlign", 

64 "HSplit", 

65 "VSplit", 

66 "FloatContainer", 

67 "Float", 

68 "WindowAlign", 

69 "Window", 

70 "WindowRenderInfo", 

71 "ConditionalContainer", 

72 "ScrollOffsets", 

73 "ColorColumn", 

74 "to_container", 

75 "to_window", 

76 "is_container", 

77 "DynamicContainer", 

78] 

79 

80 

81class Container(metaclass=ABCMeta): 

82 """ 

83 Base class for user interface layout. 

84 """ 

85 

86 @abstractmethod 

87 def reset(self) -> None: 

88 """ 

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

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

91 """ 

92 

93 @abstractmethod 

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

95 """ 

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

97 desired width for this container. 

98 """ 

99 

100 @abstractmethod 

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

102 """ 

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

104 desired height for this container. 

105 """ 

106 

107 @abstractmethod 

108 def write_to_screen( 

109 self, 

110 screen: Screen, 

111 mouse_handlers: MouseHandlers, 

112 write_position: WritePosition, 

113 parent_style: str, 

114 erase_bg: bool, 

115 z_index: int | None, 

116 ) -> None: 

117 """ 

118 Write the actual content to the screen. 

119 

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

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

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

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

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

125 style down to the windows that they contain. 

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

127 """ 

128 

129 def is_modal(self) -> bool: 

130 """ 

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

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

133 """ 

134 return False 

135 

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

137 """ 

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

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

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

141 """ 

142 return None 

143 

144 @abstractmethod 

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

146 """ 

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

148 """ 

149 return [] 

150 

151 

152if TYPE_CHECKING: 

153 

154 class MagicContainer(Protocol): 

155 """ 

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

157 """ 

158 

159 def __pt_container__(self) -> AnyContainer: 

160 ... 

161 

162 

163AnyContainer = Union[Container, "MagicContainer"] 

164 

165 

166def _window_too_small() -> Window: 

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

168 return Window( 

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

170 ) 

171 

172 

173class VerticalAlign(Enum): 

174 "Alignment for `HSplit`." 

175 TOP = "TOP" 

176 CENTER = "CENTER" 

177 BOTTOM = "BOTTOM" 

178 JUSTIFY = "JUSTIFY" 

179 

180 

181class HorizontalAlign(Enum): 

182 "Alignment for `VSplit`." 

183 LEFT = "LEFT" 

184 CENTER = "CENTER" 

185 RIGHT = "RIGHT" 

186 JUSTIFY = "JUSTIFY" 

187 

188 

189class _Split(Container): 

190 """ 

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

192 """ 

193 

194 def __init__( 

195 self, 

196 children: Sequence[AnyContainer], 

197 window_too_small: Container | None = None, 

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

199 padding_char: str | None = None, 

200 padding_style: str = "", 

201 width: AnyDimension = None, 

202 height: AnyDimension = None, 

203 z_index: int | None = None, 

204 modal: bool = False, 

205 key_bindings: KeyBindingsBase | None = None, 

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

207 ) -> None: 

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

209 self.window_too_small = window_too_small or _window_too_small() 

210 self.padding = padding 

211 self.padding_char = padding_char 

212 self.padding_style = padding_style 

213 

214 self.width = width 

215 self.height = height 

216 self.z_index = z_index 

217 

218 self.modal = modal 

219 self.key_bindings = key_bindings 

220 self.style = style 

221 

222 def is_modal(self) -> bool: 

223 return self.modal 

224 

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

226 return self.key_bindings 

227 

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

229 return self.children 

230 

231 

232class HSplit(_Split): 

233 """ 

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

235 

236 +--------------------+ 

237 | | 

238 +--------------------+ 

239 | | 

240 +--------------------+ 

241 

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

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

244 

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

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

247 

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

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

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

251 "Window too small" message. 

252 :param align: `VerticalAlign` value. 

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

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

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

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

257 :param style: A style string. 

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

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

260 

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

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

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

264 """ 

265 

266 def __init__( 

267 self, 

268 children: Sequence[AnyContainer], 

269 window_too_small: Container | None = None, 

270 align: VerticalAlign = VerticalAlign.JUSTIFY, 

271 padding: AnyDimension = 0, 

272 padding_char: str | None = None, 

273 padding_style: str = "", 

274 width: AnyDimension = None, 

275 height: AnyDimension = None, 

276 z_index: int | None = None, 

277 modal: bool = False, 

278 key_bindings: KeyBindingsBase | None = None, 

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

280 ) -> None: 

281 super().__init__( 

282 children=children, 

283 window_too_small=window_too_small, 

284 padding=padding, 

285 padding_char=padding_char, 

286 padding_style=padding_style, 

287 width=width, 

288 height=height, 

289 z_index=z_index, 

290 modal=modal, 

291 key_bindings=key_bindings, 

292 style=style, 

293 ) 

294 

295 self.align = align 

296 

297 self._children_cache: SimpleCache[ 

298 tuple[Container, ...], list[Container] 

299 ] = SimpleCache(maxsize=1) 

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

301 

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

303 if self.width is not None: 

304 return to_dimension(self.width) 

305 

306 if self.children: 

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

308 return max_layout_dimensions(dimensions) 

309 else: 

310 return Dimension() 

311 

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

313 if self.height is not None: 

314 return to_dimension(self.height) 

315 

316 dimensions = [ 

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

318 ] 

319 return sum_layout_dimensions(dimensions) 

320 

321 def reset(self) -> None: 

322 for c in self.children: 

323 c.reset() 

324 

325 @property 

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

327 """ 

328 List of child objects, including padding. 

329 """ 

330 

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

332 result: list[Container] = [] 

333 

334 # Padding Top. 

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

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

337 

338 # The children with padding. 

339 for child in self.children: 

340 result.append(child) 

341 result.append( 

342 Window( 

343 height=self.padding, 

344 char=self.padding_char, 

345 style=self.padding_style, 

346 ) 

347 ) 

348 if result: 

349 result.pop() 

350 

351 # Padding right. 

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

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

354 

355 return result 

356 

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

358 

359 def write_to_screen( 

360 self, 

361 screen: Screen, 

362 mouse_handlers: MouseHandlers, 

363 write_position: WritePosition, 

364 parent_style: str, 

365 erase_bg: bool, 

366 z_index: int | None, 

367 ) -> None: 

368 """ 

369 Render the prompt to a `Screen` instance. 

370 

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

372 to which the output has to be written. 

373 """ 

374 sizes = self._divide_heights(write_position) 

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

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

377 

378 if sizes is None: 

379 self.window_too_small.write_to_screen( 

380 screen, mouse_handlers, write_position, style, erase_bg, z_index 

381 ) 

382 else: 

383 # 

384 ypos = write_position.ypos 

385 xpos = write_position.xpos 

386 width = write_position.width 

387 

388 # Draw child panes. 

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

390 c.write_to_screen( 

391 screen, 

392 mouse_handlers, 

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

394 style, 

395 erase_bg, 

396 z_index, 

397 ) 

398 ypos += s 

399 

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

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

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

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

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

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

406 if remaining_height > 0: 

407 self._remaining_space_window.write_to_screen( 

408 screen, 

409 mouse_handlers, 

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

411 style, 

412 erase_bg, 

413 z_index, 

414 ) 

415 

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

417 """ 

418 Return the heights for all rows. 

419 Or None when there is not enough space. 

420 """ 

421 if not self.children: 

422 return [] 

423 

424 width = write_position.width 

425 height = write_position.height 

426 

427 # Calculate heights. 

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

429 

430 # Sum dimensions 

431 sum_dimensions = sum_layout_dimensions(dimensions) 

432 

433 # If there is not enough space for both. 

434 # Don't do anything. 

435 if sum_dimensions.min > height: 

436 return None 

437 

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

439 # the whole height.) 

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

441 

442 child_generator = take_using_weights( 

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

444 ) 

445 

446 i = next(child_generator) 

447 

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

449 preferred_stop = min(height, sum_dimensions.preferred) 

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

451 

452 while sum(sizes) < preferred_stop: 

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

454 sizes[i] += 1 

455 i = next(child_generator) 

456 

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

458 if not get_app().is_done: 

459 max_stop = min(height, sum_dimensions.max) 

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

461 

462 while sum(sizes) < max_stop: 

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

464 sizes[i] += 1 

465 i = next(child_generator) 

466 

467 return sizes 

468 

469 

470class VSplit(_Split): 

471 """ 

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

473 

474 +---------+----------+ 

475 | | | 

476 | | | 

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

478 

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

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

481 

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

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

484 

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

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

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

488 "Window too small" message. 

489 :param align: `HorizontalAlign` value. 

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

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

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

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

494 :param style: A style string. 

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

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

497 

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

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

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

501 """ 

502 

503 def __init__( 

504 self, 

505 children: Sequence[AnyContainer], 

506 window_too_small: Container | None = None, 

507 align: HorizontalAlign = HorizontalAlign.JUSTIFY, 

508 padding: AnyDimension = 0, 

509 padding_char: str | None = None, 

510 padding_style: str = "", 

511 width: AnyDimension = None, 

512 height: AnyDimension = None, 

513 z_index: int | None = None, 

514 modal: bool = False, 

515 key_bindings: KeyBindingsBase | None = None, 

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

517 ) -> None: 

518 super().__init__( 

519 children=children, 

520 window_too_small=window_too_small, 

521 padding=padding, 

522 padding_char=padding_char, 

523 padding_style=padding_style, 

524 width=width, 

525 height=height, 

526 z_index=z_index, 

527 modal=modal, 

528 key_bindings=key_bindings, 

529 style=style, 

530 ) 

531 

532 self.align = align 

533 

534 self._children_cache: SimpleCache[ 

535 tuple[Container, ...], list[Container] 

536 ] = SimpleCache(maxsize=1) 

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

538 

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

540 if self.width is not None: 

541 return to_dimension(self.width) 

542 

543 dimensions = [ 

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

545 ] 

546 

547 return sum_layout_dimensions(dimensions) 

548 

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

550 if self.height is not None: 

551 return to_dimension(self.height) 

552 

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

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

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

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

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

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

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

560 

561 sizes = self._divide_widths(width) 

562 children = self._all_children 

563 

564 if sizes is None: 

565 return Dimension() 

566 else: 

567 dimensions = [ 

568 c.preferred_height(s, max_available_height) 

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

570 ] 

571 return max_layout_dimensions(dimensions) 

572 

573 def reset(self) -> None: 

574 for c in self.children: 

575 c.reset() 

576 

577 @property 

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

579 """ 

580 List of child objects, including padding. 

581 """ 

582 

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

584 result: list[Container] = [] 

585 

586 # Padding left. 

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

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

589 

590 # The children with padding. 

591 for child in self.children: 

592 result.append(child) 

593 result.append( 

594 Window( 

595 width=self.padding, 

596 char=self.padding_char, 

597 style=self.padding_style, 

598 ) 

599 ) 

600 if result: 

601 result.pop() 

602 

603 # Padding right. 

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

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

606 

607 return result 

608 

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

610 

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

612 """ 

613 Return the widths for all columns. 

614 Or None when there is not enough space. 

615 """ 

616 children = self._all_children 

617 

618 if not children: 

619 return [] 

620 

621 # Calculate widths. 

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

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

624 

625 # Sum dimensions 

626 sum_dimensions = sum_layout_dimensions(dimensions) 

627 

628 # If there is not enough space for both. 

629 # Don't do anything. 

630 if sum_dimensions.min > width: 

631 return None 

632 

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

634 # the whole width.) 

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

636 

637 child_generator = take_using_weights( 

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

639 ) 

640 

641 i = next(child_generator) 

642 

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

644 preferred_stop = min(width, sum_dimensions.preferred) 

645 

646 while sum(sizes) < preferred_stop: 

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

648 sizes[i] += 1 

649 i = next(child_generator) 

650 

651 # Increase until we use all the available space. 

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

653 max_stop = min(width, sum_dimensions.max) 

654 

655 while sum(sizes) < max_stop: 

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

657 sizes[i] += 1 

658 i = next(child_generator) 

659 

660 return sizes 

661 

662 def write_to_screen( 

663 self, 

664 screen: Screen, 

665 mouse_handlers: MouseHandlers, 

666 write_position: WritePosition, 

667 parent_style: str, 

668 erase_bg: bool, 

669 z_index: int | None, 

670 ) -> None: 

671 """ 

672 Render the prompt to a `Screen` instance. 

673 

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

675 to which the output has to be written. 

676 """ 

677 if not self.children: 

678 return 

679 

680 children = self._all_children 

681 sizes = self._divide_widths(write_position.width) 

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

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

684 

685 # If there is not enough space. 

686 if sizes is None: 

687 self.window_too_small.write_to_screen( 

688 screen, mouse_handlers, write_position, style, erase_bg, z_index 

689 ) 

690 return 

691 

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

693 # write_position.height. 

694 heights = [ 

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

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

697 ] 

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

699 

700 # 

701 ypos = write_position.ypos 

702 xpos = write_position.xpos 

703 

704 # Draw all child panes. 

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

706 c.write_to_screen( 

707 screen, 

708 mouse_handlers, 

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

710 style, 

711 erase_bg, 

712 z_index, 

713 ) 

714 xpos += s 

715 

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

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

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

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

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

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

722 if remaining_width > 0: 

723 self._remaining_space_window.write_to_screen( 

724 screen, 

725 mouse_handlers, 

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

727 style, 

728 erase_bg, 

729 z_index, 

730 ) 

731 

732 

733class FloatContainer(Container): 

734 """ 

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

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

737 

738 Example Usage:: 

739 

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

741 floats=[ 

742 Float(xcursor=True, 

743 ycursor=True, 

744 content=CompletionsMenu(...)) 

745 ]) 

746 

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

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

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

750 """ 

751 

752 def __init__( 

753 self, 

754 content: AnyContainer, 

755 floats: list[Float], 

756 modal: bool = False, 

757 key_bindings: KeyBindingsBase | None = None, 

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

759 z_index: int | None = None, 

760 ) -> None: 

761 self.content = to_container(content) 

762 self.floats = floats 

763 

764 self.modal = modal 

765 self.key_bindings = key_bindings 

766 self.style = style 

767 self.z_index = z_index 

768 

769 def reset(self) -> None: 

770 self.content.reset() 

771 

772 for f in self.floats: 

773 f.content.reset() 

774 

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

776 return self.content.preferred_width(max_available_width) 

777 

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

779 """ 

780 Return the preferred height of the float container. 

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

782 into the dimensions provided by the container.) 

783 """ 

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

785 

786 def write_to_screen( 

787 self, 

788 screen: Screen, 

789 mouse_handlers: MouseHandlers, 

790 write_position: WritePosition, 

791 parent_style: str, 

792 erase_bg: bool, 

793 z_index: int | None, 

794 ) -> None: 

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

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

797 

798 self.content.write_to_screen( 

799 screen, mouse_handlers, write_position, style, erase_bg, z_index 

800 ) 

801 

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

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

804 # container and the `Float`. 

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

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

807 

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

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

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

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

812 # enough for now.) 

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

814 

815 if postpone: 

816 new_z_index = ( 

817 number + 10**8 

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

819 screen.draw_with_z_index( 

820 z_index=new_z_index, 

821 draw_func=partial( 

822 self._draw_float, 

823 fl, 

824 screen, 

825 mouse_handlers, 

826 write_position, 

827 style, 

828 erase_bg, 

829 new_z_index, 

830 ), 

831 ) 

832 else: 

833 self._draw_float( 

834 fl, 

835 screen, 

836 mouse_handlers, 

837 write_position, 

838 style, 

839 erase_bg, 

840 new_z_index, 

841 ) 

842 

843 def _draw_float( 

844 self, 

845 fl: Float, 

846 screen: Screen, 

847 mouse_handlers: MouseHandlers, 

848 write_position: WritePosition, 

849 style: str, 

850 erase_bg: bool, 

851 z_index: int | None, 

852 ) -> None: 

853 "Draw a single Float." 

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

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

856 # relative to the write_position.) 

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

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

859 cpos = screen.get_menu_position( 

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

861 ) 

862 cursor_position = Point( 

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

864 ) 

865 

866 fl_width = fl.get_width() 

867 fl_height = fl.get_height() 

868 width: int 

869 height: int 

870 xpos: int 

871 ypos: int 

872 

873 # Left & width given. 

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

875 xpos = fl.left 

876 width = fl_width 

877 # Left & right given -> calculate width. 

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

879 xpos = fl.left 

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

881 # Width & right given -> calculate left. 

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

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

884 width = fl_width 

885 # Near x position of cursor. 

886 elif fl.xcursor: 

887 if fl_width is None: 

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

889 width = min(write_position.width, width) 

890 else: 

891 width = fl_width 

892 

893 xpos = cursor_position.x 

894 if xpos + width > write_position.width: 

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

896 # Only width given -> center horizontally. 

897 elif fl_width: 

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

899 width = fl_width 

900 # Otherwise, take preferred width from float content. 

901 else: 

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

903 

904 if fl.left is not None: 

905 xpos = fl.left 

906 elif fl.right is not None: 

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

908 else: # Center horizontally. 

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

910 

911 # Trim. 

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

913 

914 # Top & height given. 

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

916 ypos = fl.top 

917 height = fl_height 

918 # Top & bottom given -> calculate height. 

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

920 ypos = fl.top 

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

922 # Height & bottom given -> calculate top. 

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

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

925 height = fl_height 

926 # Near cursor. 

927 elif fl.ycursor: 

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

929 

930 if fl_height is None: 

931 height = fl.content.preferred_height( 

932 width, write_position.height 

933 ).preferred 

934 else: 

935 height = fl_height 

936 

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

938 # when the content requires it.) 

939 if height > write_position.height - ypos: 

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

941 # When the space below the cursor is more than 

942 # the space above, just reduce the height. 

943 height = write_position.height - ypos 

944 else: 

945 # Otherwise, fit the float above the cursor. 

946 height = min(height, cursor_position.y) 

947 ypos = cursor_position.y - height 

948 

949 # Only height given -> center vertically. 

950 elif fl_height: 

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

952 height = fl_height 

953 # Otherwise, take preferred height from content. 

954 else: 

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

956 

957 if fl.top is not None: 

958 ypos = fl.top 

959 elif fl.bottom is not None: 

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

961 else: # Center vertically. 

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

963 

964 # Trim. 

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

966 

967 # Write float. 

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

969 if height > 0 and width > 0: 

970 wp = WritePosition( 

971 xpos=xpos + write_position.xpos, 

972 ypos=ypos + write_position.ypos, 

973 width=width, 

974 height=height, 

975 ) 

976 

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

978 fl.content.write_to_screen( 

979 screen, 

980 mouse_handlers, 

981 wp, 

982 style, 

983 erase_bg=not fl.transparent(), 

984 z_index=z_index, 

985 ) 

986 

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

988 """ 

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

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

991 """ 

992 wp = write_position 

993 

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

995 if y in screen.data_buffer: 

996 row = screen.data_buffer[y] 

997 

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

999 c = row[x] 

1000 if c.char != " ": 

1001 return False 

1002 

1003 return True 

1004 

1005 def is_modal(self) -> bool: 

1006 return self.modal 

1007 

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

1009 return self.key_bindings 

1010 

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

1012 children = [self.content] 

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

1014 return children 

1015 

1016 

1017class Float: 

1018 """ 

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

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

1021 

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

1023 

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

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

1026 

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

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

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

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

1031 

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

1033 the current window. 

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

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

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

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

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

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

1040 drawn transparently. 

1041 """ 

1042 

1043 def __init__( 

1044 self, 

1045 content: AnyContainer, 

1046 top: int | None = None, 

1047 right: int | None = None, 

1048 bottom: int | None = None, 

1049 left: int | None = None, 

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

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

1052 xcursor: bool = False, 

1053 ycursor: bool = False, 

1054 attach_to_window: AnyContainer | None = None, 

1055 hide_when_covering_content: bool = False, 

1056 allow_cover_cursor: bool = False, 

1057 z_index: int = 1, 

1058 transparent: bool = False, 

1059 ) -> None: 

1060 assert z_index >= 1 

1061 

1062 self.left = left 

1063 self.right = right 

1064 self.top = top 

1065 self.bottom = bottom 

1066 

1067 self.width = width 

1068 self.height = height 

1069 

1070 self.xcursor = xcursor 

1071 self.ycursor = ycursor 

1072 

1073 self.attach_to_window = ( 

1074 to_window(attach_to_window) if attach_to_window else None 

1075 ) 

1076 

1077 self.content = to_container(content) 

1078 self.hide_when_covering_content = hide_when_covering_content 

1079 self.allow_cover_cursor = allow_cover_cursor 

1080 self.z_index = z_index 

1081 self.transparent = to_filter(transparent) 

1082 

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

1084 if callable(self.width): 

1085 return self.width() 

1086 return self.width 

1087 

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

1089 if callable(self.height): 

1090 return self.height() 

1091 return self.height 

1092 

1093 def __repr__(self) -> str: 

1094 return "Float(content=%r)" % self.content 

1095 

1096 

1097class WindowRenderInfo: 

1098 """ 

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

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

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

1102 render position on the output screen. 

1103 

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

1105 well as implementing mouse support.) 

1106 

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

1108 the whole input, without clipping. (ui_content) 

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

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

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

1112 without the margins. 

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

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

1115 :class:`Window` instance. 

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

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

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

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

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

1121 the rendered screen. 

1122 """ 

1123 

1124 def __init__( 

1125 self, 

1126 window: Window, 

1127 ui_content: UIContent, 

1128 horizontal_scroll: int, 

1129 vertical_scroll: int, 

1130 window_width: int, 

1131 window_height: int, 

1132 configured_scroll_offsets: ScrollOffsets, 

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

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

1135 x_offset: int, 

1136 y_offset: int, 

1137 wrap_lines: bool, 

1138 ) -> None: 

1139 self.window = window 

1140 self.ui_content = ui_content 

1141 self.vertical_scroll = vertical_scroll 

1142 self.window_width = window_width # Width without margins. 

1143 self.window_height = window_height 

1144 

1145 self.configured_scroll_offsets = configured_scroll_offsets 

1146 self.visible_line_to_row_col = visible_line_to_row_col 

1147 self.wrap_lines = wrap_lines 

1148 

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

1150 # screen coordinates. 

1151 self._x_offset = x_offset 

1152 self._y_offset = y_offset 

1153 

1154 @property 

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

1156 return { 

1157 visible_line: rowcol[0] 

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

1159 } 

1160 

1161 @property 

1162 def cursor_position(self) -> Point: 

1163 """ 

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

1165 of the rendered screen. 

1166 """ 

1167 cpos = self.ui_content.cursor_position 

1168 try: 

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

1170 except KeyError: 

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

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

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

1174 else: 

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

1176 

1177 @property 

1178 def applied_scroll_offsets(self) -> ScrollOffsets: 

1179 """ 

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

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

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

1183 than what's configured. 

1184 """ 

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

1186 top = 0 

1187 else: 

1188 # Get row where the cursor is displayed. 

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

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

1191 

1192 return ScrollOffsets( 

1193 top=top, 

1194 bottom=min( 

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

1196 self.configured_scroll_offsets.bottom, 

1197 ), 

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

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

1200 # double width characters in mind.) 

1201 left=0, 

1202 right=0, 

1203 ) 

1204 

1205 @property 

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

1207 """ 

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

1209 The last line may not be entirely visible. 

1210 """ 

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

1212 

1213 @property 

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

1215 """ 

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

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

1218 the first row appears in the dictionary. 

1219 """ 

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

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

1222 if v in result: 

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

1224 else: 

1225 result[v] = k 

1226 return result 

1227 

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

1229 """ 

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

1231 with the first visible line. 

1232 """ 

1233 if after_scroll_offset: 

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

1235 else: 

1236 return self.displayed_lines[0] 

1237 

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

1239 """ 

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

1241 """ 

1242 if before_scroll_offset: 

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

1244 else: 

1245 return self.displayed_lines[-1] 

1246 

1247 def center_visible_line( 

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

1249 ) -> int: 

1250 """ 

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

1252 """ 

1253 return ( 

1254 self.first_visible_line(after_scroll_offset) 

1255 + ( 

1256 self.last_visible_line(before_scroll_offset) 

1257 - self.first_visible_line(after_scroll_offset) 

1258 ) 

1259 // 2 

1260 ) 

1261 

1262 @property 

1263 def content_height(self) -> int: 

1264 """ 

1265 The full height of the user control. 

1266 """ 

1267 return self.ui_content.line_count 

1268 

1269 @property 

1270 def full_height_visible(self) -> bool: 

1271 """ 

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

1273 """ 

1274 return ( 

1275 self.vertical_scroll == 0 

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

1277 ) 

1278 

1279 @property 

1280 def top_visible(self) -> bool: 

1281 """ 

1282 True when the top of the buffer is visible. 

1283 """ 

1284 return self.vertical_scroll == 0 

1285 

1286 @property 

1287 def bottom_visible(self) -> bool: 

1288 """ 

1289 True when the bottom of the buffer is visible. 

1290 """ 

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

1292 

1293 @property 

1294 def vertical_scroll_percentage(self) -> int: 

1295 """ 

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

1297 100 means: the bottom is visible.) 

1298 """ 

1299 if self.bottom_visible: 

1300 return 100 

1301 else: 

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

1303 

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

1305 """ 

1306 Return the height of the given line. 

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

1308 """ 

1309 if self.wrap_lines: 

1310 return self.ui_content.get_height_for_line( 

1311 lineno, self.window_width, self.window.get_line_prefix 

1312 ) 

1313 else: 

1314 return 1 

1315 

1316 

1317class ScrollOffsets: 

1318 """ 

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

1320 

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

1322 """ 

1323 

1324 def __init__( 

1325 self, 

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

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

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

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

1330 ) -> None: 

1331 self._top = top 

1332 self._bottom = bottom 

1333 self._left = left 

1334 self._right = right 

1335 

1336 @property 

1337 def top(self) -> int: 

1338 return to_int(self._top) 

1339 

1340 @property 

1341 def bottom(self) -> int: 

1342 return to_int(self._bottom) 

1343 

1344 @property 

1345 def left(self) -> int: 

1346 return to_int(self._left) 

1347 

1348 @property 

1349 def right(self) -> int: 

1350 return to_int(self._right) 

1351 

1352 def __repr__(self) -> str: 

1353 return "ScrollOffsets(top={!r}, bottom={!r}, left={!r}, right={!r})".format( 

1354 self._top, 

1355 self._bottom, 

1356 self._left, 

1357 self._right, 

1358 ) 

1359 

1360 

1361class ColorColumn: 

1362 """ 

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

1364 """ 

1365 

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

1367 self.position = position 

1368 self.style = style 

1369 

1370 

1371_in_insert_mode = vi_insert_mode | emacs_insert_mode 

1372 

1373 

1374class WindowAlign(Enum): 

1375 """ 

1376 Alignment of the Window content. 

1377 

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

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

1380 `VSplit` and `HSplit`. 

1381 """ 

1382 

1383 LEFT = "LEFT" 

1384 RIGHT = "RIGHT" 

1385 CENTER = "CENTER" 

1386 

1387 

1388class Window(Container): 

1389 """ 

1390 Container that holds a control. 

1391 

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

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

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

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

1396 of floating elements. 

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

1398 preferred width reported by the control. 

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

1400 preferred height reported by the control. 

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

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

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

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

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

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

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

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

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

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

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

1412 will be centered vertically most of the time. 

1413 :param allow_scroll_beyond_bottom: A `bool` or 

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

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

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

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

1418 the body is hidden. 

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

1420 scroll horizontally, but wrap lines instead. 

1421 :param get_vertical_scroll: Callable that takes this window 

1422 instance as input and returns a preferred vertical scroll. 

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

1424 current cursor position.) 

1425 :param get_horizontal_scroll: Callable that takes this window 

1426 instance as input and returns a preferred vertical scroll. 

1427 :param always_hide_cursor: A `bool` or 

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

1429 when the user control specifies a cursor position. 

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

1431 display a cursorline. 

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

1433 display a cursorcolumn. 

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

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

1436 a list. 

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

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

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

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

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

1442 can also be a callable that returns a character. 

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

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

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

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

1447 so on. 

1448 """ 

1449 

1450 def __init__( 

1451 self, 

1452 content: UIControl | None = None, 

1453 width: AnyDimension = None, 

1454 height: AnyDimension = None, 

1455 z_index: int | None = None, 

1456 dont_extend_width: FilterOrBool = False, 

1457 dont_extend_height: FilterOrBool = False, 

1458 ignore_content_width: FilterOrBool = False, 

1459 ignore_content_height: FilterOrBool = False, 

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

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

1462 scroll_offsets: ScrollOffsets | None = None, 

1463 allow_scroll_beyond_bottom: FilterOrBool = False, 

1464 wrap_lines: FilterOrBool = False, 

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

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

1467 always_hide_cursor: FilterOrBool = False, 

1468 cursorline: FilterOrBool = False, 

1469 cursorcolumn: FilterOrBool = False, 

1470 colorcolumns: ( 

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

1472 ) = None, 

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

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

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

1476 get_line_prefix: GetLinePrefixCallable | None = None, 

1477 ) -> None: 

1478 self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom) 

1479 self.always_hide_cursor = to_filter(always_hide_cursor) 

1480 self.wrap_lines = to_filter(wrap_lines) 

1481 self.cursorline = to_filter(cursorline) 

1482 self.cursorcolumn = to_filter(cursorcolumn) 

1483 

1484 self.content = content or DummyControl() 

1485 self.dont_extend_width = to_filter(dont_extend_width) 

1486 self.dont_extend_height = to_filter(dont_extend_height) 

1487 self.ignore_content_width = to_filter(ignore_content_width) 

1488 self.ignore_content_height = to_filter(ignore_content_height) 

1489 self.left_margins = left_margins or [] 

1490 self.right_margins = right_margins or [] 

1491 self.scroll_offsets = scroll_offsets or ScrollOffsets() 

1492 self.get_vertical_scroll = get_vertical_scroll 

1493 self.get_horizontal_scroll = get_horizontal_scroll 

1494 self.colorcolumns = colorcolumns or [] 

1495 self.align = align 

1496 self.style = style 

1497 self.char = char 

1498 self.get_line_prefix = get_line_prefix 

1499 

1500 self.width = width 

1501 self.height = height 

1502 self.z_index = z_index 

1503 

1504 # Cache for the screens generated by the margin. 

1505 self._ui_content_cache: SimpleCache[ 

1506 tuple[int, int, int], UIContent 

1507 ] = SimpleCache(maxsize=8) 

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

1509 maxsize=1 

1510 ) 

1511 

1512 self.reset() 

1513 

1514 def __repr__(self) -> str: 

1515 return "Window(content=%r)" % self.content 

1516 

1517 def reset(self) -> None: 

1518 self.content.reset() 

1519 

1520 #: Scrolling position of the main content. 

1521 self.vertical_scroll = 0 

1522 self.horizontal_scroll = 0 

1523 

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

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

1526 # all of the vertical space. 

1527 self.vertical_scroll_2 = 0 

1528 

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

1530 #: output.) 

1531 self.render_info: WindowRenderInfo | None = None 

1532 

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

1534 """ 

1535 Return the width for this margin. 

1536 (Calculate only once per render time.) 

1537 """ 

1538 

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

1540 def get_ui_content() -> UIContent: 

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

1542 

1543 def get_width() -> int: 

1544 return margin.get_width(get_ui_content) 

1545 

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

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

1548 

1549 def _get_total_margin_width(self) -> int: 

1550 """ 

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

1552 """ 

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

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

1555 ) 

1556 

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

1558 """ 

1559 Calculate the preferred width for this window. 

1560 """ 

1561 

1562 def preferred_content_width() -> int | None: 

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

1564 window was given.""" 

1565 if self.ignore_content_width(): 

1566 return None 

1567 

1568 # Calculate the width of the margin. 

1569 total_margin_width = self._get_total_margin_width() 

1570 

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

1572 preferred_width = self.content.preferred_width( 

1573 max_available_width - total_margin_width 

1574 ) 

1575 

1576 if preferred_width is not None: 

1577 # Include width of the margins. 

1578 preferred_width += total_margin_width 

1579 return preferred_width 

1580 

1581 # Merge. 

1582 return self._merge_dimensions( 

1583 dimension=to_dimension(self.width), 

1584 get_preferred=preferred_content_width, 

1585 dont_extend=self.dont_extend_width(), 

1586 ) 

1587 

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

1589 """ 

1590 Calculate the preferred height for this window. 

1591 """ 

1592 

1593 def preferred_content_height() -> int | None: 

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

1595 window was given.""" 

1596 if self.ignore_content_height(): 

1597 return None 

1598 

1599 total_margin_width = self._get_total_margin_width() 

1600 wrap_lines = self.wrap_lines() 

1601 

1602 return self.content.preferred_height( 

1603 width - total_margin_width, 

1604 max_available_height, 

1605 wrap_lines, 

1606 self.get_line_prefix, 

1607 ) 

1608 

1609 return self._merge_dimensions( 

1610 dimension=to_dimension(self.height), 

1611 get_preferred=preferred_content_height, 

1612 dont_extend=self.dont_extend_height(), 

1613 ) 

1614 

1615 @staticmethod 

1616 def _merge_dimensions( 

1617 dimension: Dimension | None, 

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

1619 dont_extend: bool = False, 

1620 ) -> Dimension: 

1621 """ 

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

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

1624 parent container. 

1625 """ 

1626 dimension = dimension or Dimension() 

1627 

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

1629 # ignore the UIControl. 

1630 preferred: int | None 

1631 

1632 if dimension.preferred_specified: 

1633 preferred = dimension.preferred 

1634 else: 

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

1636 # content. 

1637 preferred = get_preferred() 

1638 

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

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

1641 if preferred is not None: 

1642 if dimension.max_specified: 

1643 preferred = min(preferred, dimension.max) 

1644 

1645 if dimension.min_specified: 

1646 preferred = max(preferred, dimension.min) 

1647 

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

1649 # also as the max dimension. 

1650 max_: int | None 

1651 min_: int | None 

1652 

1653 if dont_extend and preferred is not None: 

1654 max_ = min(dimension.max, preferred) 

1655 else: 

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

1657 

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

1659 

1660 return Dimension( 

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

1662 ) 

1663 

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

1665 """ 

1666 Create a `UIContent` instance. 

1667 """ 

1668 

1669 def get_content() -> UIContent: 

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

1671 

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

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

1674 

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

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

1677 app = get_app() 

1678 if app.quoted_insert: 

1679 return "^" 

1680 if app.vi_state.waiting_for_digraph: 

1681 if app.vi_state.digraph_symbol1: 

1682 return app.vi_state.digraph_symbol1 

1683 return "?" 

1684 return None 

1685 

1686 def write_to_screen( 

1687 self, 

1688 screen: Screen, 

1689 mouse_handlers: MouseHandlers, 

1690 write_position: WritePosition, 

1691 parent_style: str, 

1692 erase_bg: bool, 

1693 z_index: int | None, 

1694 ) -> None: 

1695 """ 

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

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

1698 """ 

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

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

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

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

1703 write_position = WritePosition( 

1704 xpos=write_position.xpos, 

1705 ypos=write_position.ypos, 

1706 width=write_position.width, 

1707 height=write_position.height, 

1708 ) 

1709 

1710 if self.dont_extend_width(): 

1711 write_position.width = min( 

1712 write_position.width, 

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

1714 ) 

1715 

1716 if self.dont_extend_height(): 

1717 write_position.height = min( 

1718 write_position.height, 

1719 self.preferred_height( 

1720 write_position.width, write_position.height 

1721 ).preferred, 

1722 ) 

1723 

1724 # Draw 

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

1726 

1727 draw_func = partial( 

1728 self._write_to_screen_at_index, 

1729 screen, 

1730 mouse_handlers, 

1731 write_position, 

1732 parent_style, 

1733 erase_bg, 

1734 ) 

1735 

1736 if z_index is None or z_index <= 0: 

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

1738 draw_func() 

1739 else: 

1740 # Otherwise, postpone. 

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

1742 

1743 def _write_to_screen_at_index( 

1744 self, 

1745 screen: Screen, 

1746 mouse_handlers: MouseHandlers, 

1747 write_position: WritePosition, 

1748 parent_style: str, 

1749 erase_bg: bool, 

1750 ) -> None: 

1751 # Don't bother writing invisible windows. 

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

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

1754 return 

1755 

1756 # Calculate margin sizes. 

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

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

1759 total_margin_width = sum(left_margin_widths + right_margin_widths) 

1760 

1761 # Render UserControl. 

1762 ui_content = self.content.create_content( 

1763 write_position.width - total_margin_width, write_position.height 

1764 ) 

1765 assert isinstance(ui_content, UIContent) 

1766 

1767 # Scroll content. 

1768 wrap_lines = self.wrap_lines() 

1769 self._scroll( 

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

1771 ) 

1772 

1773 # Erase background and fill with `char`. 

1774 self._fill_bg(screen, write_position, erase_bg) 

1775 

1776 # Resolve `align` attribute. 

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

1778 

1779 # Write body 

1780 visible_line_to_row_col, rowcol_to_yx = self._copy_body( 

1781 ui_content, 

1782 screen, 

1783 write_position, 

1784 sum(left_margin_widths), 

1785 write_position.width - total_margin_width, 

1786 self.vertical_scroll, 

1787 self.horizontal_scroll, 

1788 wrap_lines=wrap_lines, 

1789 highlight_lines=True, 

1790 vertical_scroll_2=self.vertical_scroll_2, 

1791 always_hide_cursor=self.always_hide_cursor(), 

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

1793 align=align, 

1794 get_line_prefix=self.get_line_prefix, 

1795 ) 

1796 

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

1798 x_offset = write_position.xpos + sum(left_margin_widths) 

1799 y_offset = write_position.ypos 

1800 

1801 render_info = WindowRenderInfo( 

1802 window=self, 

1803 ui_content=ui_content, 

1804 horizontal_scroll=self.horizontal_scroll, 

1805 vertical_scroll=self.vertical_scroll, 

1806 window_width=write_position.width - total_margin_width, 

1807 window_height=write_position.height, 

1808 configured_scroll_offsets=self.scroll_offsets, 

1809 visible_line_to_row_col=visible_line_to_row_col, 

1810 rowcol_to_yx=rowcol_to_yx, 

1811 x_offset=x_offset, 

1812 y_offset=y_offset, 

1813 wrap_lines=wrap_lines, 

1814 ) 

1815 self.render_info = render_info 

1816 

1817 # Set mouse handlers. 

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

1819 """ 

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

1821 screen coordinates into line coordinates. 

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

1823 """ 

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

1825 # the UI. 

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

1827 return NotImplemented 

1828 

1829 # Find row/col position first. 

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

1831 y = mouse_event.position.y 

1832 x = mouse_event.position.x 

1833 

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

1835 # last line instead. 

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

1837 y = min(max_y, y) 

1838 result: NotImplementedOrNone 

1839 

1840 while x >= 0: 

1841 try: 

1842 row, col = yx_to_rowcol[y, x] 

1843 except KeyError: 

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

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

1846 x -= 1 

1847 else: 

1848 # Found position, call handler of UIControl. 

1849 result = self.content.mouse_handler( 

1850 MouseEvent( 

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

1852 event_type=mouse_event.event_type, 

1853 button=mouse_event.button, 

1854 modifiers=mouse_event.modifiers, 

1855 ) 

1856 ) 

1857 break 

1858 else: 

1859 # nobreak. 

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

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

1862 # Report (0,0) instead.) 

1863 result = self.content.mouse_handler( 

1864 MouseEvent( 

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

1866 event_type=mouse_event.event_type, 

1867 button=mouse_event.button, 

1868 modifiers=mouse_event.modifiers, 

1869 ) 

1870 ) 

1871 

1872 # If it returns NotImplemented, handle it here. 

1873 if result == NotImplemented: 

1874 result = self._mouse_handler(mouse_event) 

1875 

1876 return result 

1877 

1878 mouse_handlers.set_mouse_handler_for_range( 

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

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

1881 y_min=write_position.ypos, 

1882 y_max=write_position.ypos + write_position.height, 

1883 handler=mouse_handler, 

1884 ) 

1885 

1886 # Render and copy margins. 

1887 move_x = 0 

1888 

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

1890 "Render margin. Return `Screen`." 

1891 # Retrieve margin fragments. 

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

1893 

1894 # Turn it into a UIContent object. 

1895 # already rendered those fragments using this size.) 

1896 return FormattedTextControl(fragments).create_content( 

1897 width + 1, write_position.height 

1898 ) 

1899 

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

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

1902 # Create screen for margin. 

1903 margin_content = render_margin(m, width) 

1904 

1905 # Copy and shift X. 

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

1907 move_x += width 

1908 

1909 move_x = write_position.width - sum(right_margin_widths) 

1910 

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

1912 # Create screen for margin. 

1913 margin_content = render_margin(m, width) 

1914 

1915 # Copy and shift X. 

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

1917 move_x += width 

1918 

1919 # Apply 'self.style' 

1920 self._apply_style(screen, write_position, parent_style) 

1921 

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

1923 # position. 

1924 screen.visible_windows_to_write_positions[self] = write_position 

1925 

1926 def _copy_body( 

1927 self, 

1928 ui_content: UIContent, 

1929 new_screen: Screen, 

1930 write_position: WritePosition, 

1931 move_x: int, 

1932 width: int, 

1933 vertical_scroll: int = 0, 

1934 horizontal_scroll: int = 0, 

1935 wrap_lines: bool = False, 

1936 highlight_lines: bool = False, 

1937 vertical_scroll_2: int = 0, 

1938 always_hide_cursor: bool = False, 

1939 has_focus: bool = False, 

1940 align: WindowAlign = WindowAlign.LEFT, 

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

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

1943 """ 

1944 Copy the UIContent into the output screen. 

1945 Return (visible_line_to_row_col, rowcol_to_yx) tuple. 

1946 

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

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

1949 """ 

1950 xpos = write_position.xpos + move_x 

1951 ypos = write_position.ypos 

1952 line_count = ui_content.line_count 

1953 new_buffer = new_screen.data_buffer 

1954 empty_char = _CHAR_CACHE["", ""] 

1955 

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

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

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

1959 

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

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

1962 

1963 def copy_line( 

1964 line: StyleAndTextTuples, 

1965 lineno: int, 

1966 x: int, 

1967 y: int, 

1968 is_input: bool = False, 

1969 ) -> tuple[int, int]: 

1970 """ 

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

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

1973 function before every line. 

1974 """ 

1975 if is_input: 

1976 current_rowcol_to_yx = rowcol_to_yx 

1977 else: 

1978 current_rowcol_to_yx = {} # Throwaway dictionary. 

1979 

1980 # Draw line prefix. 

1981 if is_input and get_line_prefix: 

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

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

1984 

1985 # Scroll horizontally. 

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

1987 if horizontal_scroll and is_input: 

1988 h_scroll = horizontal_scroll 

1989 line = explode_text_fragments(line) 

1990 while h_scroll > 0 and line: 

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

1992 skipped += 1 

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

1994 

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

1996 # this can end up being negative. 

1997 

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

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

2000 if align == WindowAlign.CENTER: 

2001 line_width = fragment_list_width(line) 

2002 if line_width < width: 

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

2004 elif align == WindowAlign.RIGHT: 

2005 line_width = fragment_list_width(line) 

2006 if line_width < width: 

2007 x += width - line_width 

2008 

2009 col = 0 

2010 wrap_count = 0 

2011 for style, text, *_ in line: 

2012 new_buffer_row = new_buffer[y + ypos] 

2013 

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

2015 # escape sequences.) 

2016 if "[ZeroWidthEscape]" in style: 

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

2018 continue 

2019 

2020 for c in text: 

2021 char = _CHAR_CACHE[c, style] 

2022 char_width = char.width 

2023 

2024 # Wrap when the line width is exceeded. 

2025 if wrap_lines and x + char_width > width: 

2026 visible_line_to_row_col[y + 1] = ( 

2027 lineno, 

2028 visible_line_to_row_col[y][1] + x, 

2029 ) 

2030 y += 1 

2031 wrap_count += 1 

2032 x = 0 

2033 

2034 # Insert line prefix (continuation prompt). 

2035 if is_input and get_line_prefix: 

2036 prompt = to_formatted_text( 

2037 get_line_prefix(lineno, wrap_count) 

2038 ) 

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

2040 

2041 new_buffer_row = new_buffer[y + ypos] 

2042 

2043 if y >= write_position.height: 

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

2045 

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

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

2048 new_buffer_row[x + xpos] = char 

2049 

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

2051 # to erase the neighbours positions in the screen. 

2052 # (The empty string if different from everything, 

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

2054 if char_width > 1: 

2055 for i in range(1, char_width): 

2056 new_buffer_row[x + xpos + i] = empty_char 

2057 

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

2059 # probably part of a decomposed unicode character. 

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

2061 # Merge it in the previous cell. 

2062 elif char_width == 0: 

2063 # Handle all character widths. If the previous 

2064 # character is a multiwidth character, then 

2065 # merge it two positions back. 

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

2067 if ( 

2068 x - pw >= 0 

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

2070 ): 

2071 prev_char = new_buffer_row[x + xpos - pw] 

2072 char2 = _CHAR_CACHE[ 

2073 prev_char.char + c, prev_char.style 

2074 ] 

2075 new_buffer_row[x + xpos - pw] = char2 

2076 

2077 # Keep track of write position for each character. 

2078 current_rowcol_to_yx[lineno, col + skipped] = ( 

2079 y + ypos, 

2080 x + xpos, 

2081 ) 

2082 

2083 col += 1 

2084 x += char_width 

2085 return x, y 

2086 

2087 # Copy content. 

2088 def copy() -> int: 

2089 y = -vertical_scroll_2 

2090 lineno = vertical_scroll 

2091 

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

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

2094 line = ui_content.get_line(lineno) 

2095 

2096 visible_line_to_row_col[y] = (lineno, horizontal_scroll) 

2097 

2098 # Copy margin and actual line. 

2099 x = 0 

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

2101 

2102 lineno += 1 

2103 y += 1 

2104 return y 

2105 

2106 copy() 

2107 

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

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

2110 try: 

2111 y, x = rowcol_to_yx[row, col] 

2112 except KeyError: 

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

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

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

2116 

2117 # raise ValueError( 

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

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

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

2121 else: 

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

2123 

2124 # Set cursor and menu positions. 

2125 if ui_content.cursor_position: 

2126 screen_cursor_position = cursor_pos_to_screen_pos( 

2127 ui_content.cursor_position.y, ui_content.cursor_position.x 

2128 ) 

2129 

2130 if has_focus: 

2131 new_screen.set_cursor_position(self, screen_cursor_position) 

2132 

2133 if always_hide_cursor: 

2134 new_screen.show_cursor = False 

2135 else: 

2136 new_screen.show_cursor = ui_content.show_cursor 

2137 

2138 self._highlight_digraph(new_screen) 

2139 

2140 if highlight_lines: 

2141 self._highlight_cursorlines( 

2142 new_screen, 

2143 screen_cursor_position, 

2144 xpos, 

2145 ypos, 

2146 width, 

2147 write_position.height, 

2148 ) 

2149 

2150 # Draw input characters from the input processor queue. 

2151 if has_focus and ui_content.cursor_position: 

2152 self._show_key_processor_key_buffer(new_screen) 

2153 

2154 # Set menu position. 

2155 if ui_content.menu_position: 

2156 new_screen.set_menu_position( 

2157 self, 

2158 cursor_pos_to_screen_pos( 

2159 ui_content.menu_position.y, ui_content.menu_position.x 

2160 ), 

2161 ) 

2162 

2163 # Update output screen height. 

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

2165 

2166 return visible_line_to_row_col, rowcol_to_yx 

2167 

2168 def _fill_bg( 

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

2170 ) -> None: 

2171 """ 

2172 Erase/fill the background. 

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

2174 """ 

2175 char: str | None 

2176 if callable(self.char): 

2177 char = self.char() 

2178 else: 

2179 char = self.char 

2180 

2181 if erase_bg or char: 

2182 wp = write_position 

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

2184 

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

2186 row = screen.data_buffer[y] 

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

2188 row[x] = char_obj 

2189 

2190 def _apply_style( 

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

2192 ) -> None: 

2193 # Apply `self.style`. 

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

2195 

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

2197 

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

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

2200 wp = WritePosition( 

2201 write_position.xpos, 

2202 write_position.ypos + write_position.height - 1, 

2203 write_position.width, 

2204 1, 

2205 ) 

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

2207 

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

2209 """ 

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

2211 cursor. 

2212 """ 

2213 digraph_char = self._get_digraph_char() 

2214 if digraph_char: 

2215 cpos = new_screen.get_cursor_position(self) 

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

2217 digraph_char, "class:digraph" 

2218 ] 

2219 

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

2221 """ 

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

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

2224 is meaningful to be displayed. 

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

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

2227 """ 

2228 app = get_app() 

2229 key_buffer = app.key_processor.key_buffer 

2230 

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

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

2233 # sequence.) 

2234 data = key_buffer[-1].data 

2235 

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

2237 if get_cwidth(data) == 1: 

2238 cpos = new_screen.get_cursor_position(self) 

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

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

2241 ] 

2242 

2243 def _highlight_cursorlines( 

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

2245 ) -> None: 

2246 """ 

2247 Highlight cursor row/column. 

2248 """ 

2249 cursor_line_style = " class:cursor-line " 

2250 cursor_column_style = " class:cursor-column " 

2251 

2252 data_buffer = new_screen.data_buffer 

2253 

2254 # Highlight cursor line. 

2255 if self.cursorline(): 

2256 row = data_buffer[cpos.y] 

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

2258 original_char = row[x] 

2259 row[x] = _CHAR_CACHE[ 

2260 original_char.char, original_char.style + cursor_line_style 

2261 ] 

2262 

2263 # Highlight cursor column. 

2264 if self.cursorcolumn(): 

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

2266 row = data_buffer[y2] 

2267 original_char = row[cpos.x] 

2268 row[cpos.x] = _CHAR_CACHE[ 

2269 original_char.char, original_char.style + cursor_column_style 

2270 ] 

2271 

2272 # Highlight color columns 

2273 colorcolumns = self.colorcolumns 

2274 if callable(colorcolumns): 

2275 colorcolumns = colorcolumns() 

2276 

2277 for cc in colorcolumns: 

2278 assert isinstance(cc, ColorColumn) 

2279 column = cc.position 

2280 

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

2282 color_column_style = " " + cc.style 

2283 

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

2285 row = data_buffer[y2] 

2286 original_char = row[column + x] 

2287 row[column + x] = _CHAR_CACHE[ 

2288 original_char.char, original_char.style + color_column_style 

2289 ] 

2290 

2291 def _copy_margin( 

2292 self, 

2293 margin_content: UIContent, 

2294 new_screen: Screen, 

2295 write_position: WritePosition, 

2296 move_x: int, 

2297 width: int, 

2298 ) -> None: 

2299 """ 

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

2301 """ 

2302 xpos = write_position.xpos + move_x 

2303 ypos = write_position.ypos 

2304 

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

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

2307 

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

2309 """ 

2310 Scroll body. Ensure that the cursor is visible. 

2311 """ 

2312 if self.wrap_lines(): 

2313 func = self._scroll_when_linewrapping 

2314 else: 

2315 func = self._scroll_without_linewrapping 

2316 

2317 func(ui_content, width, height) 

2318 

2319 def _scroll_when_linewrapping( 

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

2321 ) -> None: 

2322 """ 

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

2324 the requested scroll offset. 

2325 

2326 Set `self.horizontal_scroll/vertical_scroll`. 

2327 """ 

2328 scroll_offsets_bottom = self.scroll_offsets.bottom 

2329 scroll_offsets_top = self.scroll_offsets.top 

2330 

2331 # We don't have horizontal scrolling. 

2332 self.horizontal_scroll = 0 

2333 

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

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

2336 

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

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

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

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

2341 # nothing. 

2342 if width <= 0: 

2343 self.vertical_scroll = ui_content.cursor_position.y 

2344 self.vertical_scroll_2 = 0 

2345 return 

2346 

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

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

2349 # the scroll offsets into account for this.) 

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

2351 # scroll to this line. 

2352 line_height = get_line_height(ui_content.cursor_position.y) 

2353 if line_height > height - scroll_offsets_top: 

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

2355 # line prefixes). 

2356 text_before_height = ui_content.get_height_for_line( 

2357 ui_content.cursor_position.y, 

2358 width, 

2359 self.get_line_prefix, 

2360 slice_stop=ui_content.cursor_position.x, 

2361 ) 

2362 

2363 # Adjust scroll offset. 

2364 self.vertical_scroll = ui_content.cursor_position.y 

2365 self.vertical_scroll_2 = min( 

2366 text_before_height - 1, # Keep the cursor visible. 

2367 line_height 

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

2369 self.vertical_scroll_2, 

2370 ) 

2371 self.vertical_scroll_2 = max( 

2372 0, text_before_height - height, self.vertical_scroll_2 

2373 ) 

2374 return 

2375 else: 

2376 self.vertical_scroll_2 = 0 

2377 

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

2379 def get_min_vertical_scroll() -> int: 

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

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

2382 used_height = 0 

2383 prev_lineno = ui_content.cursor_position.y 

2384 

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

2386 used_height += get_line_height(lineno) 

2387 

2388 if used_height > height - scroll_offsets_bottom: 

2389 return prev_lineno 

2390 else: 

2391 prev_lineno = lineno 

2392 return 0 

2393 

2394 def get_max_vertical_scroll() -> int: 

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

2396 prev_lineno = ui_content.cursor_position.y 

2397 used_height = 0 

2398 

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

2400 used_height += get_line_height(lineno) 

2401 

2402 if used_height > scroll_offsets_top: 

2403 return prev_lineno 

2404 else: 

2405 prev_lineno = lineno 

2406 return prev_lineno 

2407 

2408 def get_topmost_visible() -> int: 

2409 """ 

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

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

2412 `allow_scroll_beyond_bottom` is false. 

2413 """ 

2414 prev_lineno = ui_content.line_count - 1 

2415 used_height = 0 

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

2417 used_height += get_line_height(lineno) 

2418 if used_height > height: 

2419 return prev_lineno 

2420 else: 

2421 prev_lineno = lineno 

2422 return prev_lineno 

2423 

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

2425 # cursor is visible. 

2426 topmost_visible = get_topmost_visible() 

2427 

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

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

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

2431 self.vertical_scroll = max( 

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

2433 ) 

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

2435 

2436 # Disallow scrolling beyond bottom? 

2437 if not self.allow_scroll_beyond_bottom(): 

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

2439 

2440 def _scroll_without_linewrapping( 

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

2442 ) -> None: 

2443 """ 

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

2445 the requested scroll offset. 

2446 

2447 Set `self.horizontal_scroll/vertical_scroll`. 

2448 """ 

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

2450 

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

2452 # a single line. 

2453 self.vertical_scroll_2 = 0 

2454 

2455 if ui_content.line_count == 0: 

2456 self.vertical_scroll = 0 

2457 self.horizontal_scroll = 0 

2458 return 

2459 else: 

2460 current_line_text = fragment_list_to_text( 

2461 ui_content.get_line(cursor_position.y) 

2462 ) 

2463 

2464 def do_scroll( 

2465 current_scroll: int, 

2466 scroll_offset_start: int, 

2467 scroll_offset_end: int, 

2468 cursor_pos: int, 

2469 window_size: int, 

2470 content_size: int, 

2471 ) -> int: 

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

2473 # Calculate the scroll offset to apply. 

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

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

2476 scroll_offset_start = int( 

2477 min(scroll_offset_start, window_size / 2, cursor_pos) 

2478 ) 

2479 scroll_offset_end = int( 

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

2481 ) 

2482 

2483 # Prevent negative scroll offsets. 

2484 if current_scroll < 0: 

2485 current_scroll = 0 

2486 

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

2488 if ( 

2489 not self.allow_scroll_beyond_bottom() 

2490 and current_scroll > content_size - window_size 

2491 ): 

2492 current_scroll = max(0, content_size - window_size) 

2493 

2494 # Scroll up if cursor is before visible part. 

2495 if current_scroll > cursor_pos - scroll_offset_start: 

2496 current_scroll = max(0, cursor_pos - scroll_offset_start) 

2497 

2498 # Scroll down if cursor is after visible part. 

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

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

2501 

2502 return current_scroll 

2503 

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

2505 if self.get_vertical_scroll: 

2506 self.vertical_scroll = self.get_vertical_scroll(self) 

2507 assert isinstance(self.vertical_scroll, int) 

2508 if self.get_horizontal_scroll: 

2509 self.horizontal_scroll = self.get_horizontal_scroll(self) 

2510 assert isinstance(self.horizontal_scroll, int) 

2511 

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

2513 # remains visible. 

2514 offsets = self.scroll_offsets 

2515 

2516 self.vertical_scroll = do_scroll( 

2517 current_scroll=self.vertical_scroll, 

2518 scroll_offset_start=offsets.top, 

2519 scroll_offset_end=offsets.bottom, 

2520 cursor_pos=ui_content.cursor_position.y, 

2521 window_size=height, 

2522 content_size=ui_content.line_count, 

2523 ) 

2524 

2525 if self.get_line_prefix: 

2526 current_line_prefix_width = fragment_list_width( 

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

2528 ) 

2529 else: 

2530 current_line_prefix_width = 0 

2531 

2532 self.horizontal_scroll = do_scroll( 

2533 current_scroll=self.horizontal_scroll, 

2534 scroll_offset_start=offsets.left, 

2535 scroll_offset_end=offsets.right, 

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

2537 window_size=width - current_line_prefix_width, 

2538 # We can only analyse the current line. Calculating the width off 

2539 # all the lines is too expensive. 

2540 content_size=max( 

2541 get_cwidth(current_line_text), self.horizontal_scroll + width 

2542 ), 

2543 ) 

2544 

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

2546 """ 

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

2548 particular event. 

2549 

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

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

2552 """ 

2553 if mouse_event.event_type == MouseEventType.SCROLL_DOWN: 

2554 self._scroll_down() 

2555 return None 

2556 elif mouse_event.event_type == MouseEventType.SCROLL_UP: 

2557 self._scroll_up() 

2558 return None 

2559 

2560 return NotImplemented 

2561 

2562 def _scroll_down(self) -> None: 

2563 "Scroll window down." 

2564 info = self.render_info 

2565 

2566 if info is None: 

2567 return 

2568 

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

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

2571 self.content.move_cursor_down() 

2572 

2573 self.vertical_scroll += 1 

2574 

2575 def _scroll_up(self) -> None: 

2576 "Scroll window up." 

2577 info = self.render_info 

2578 

2579 if info is None: 

2580 return 

2581 

2582 if info.vertical_scroll > 0: 

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

2584 if ( 

2585 info.cursor_position.y 

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

2587 ): 

2588 self.content.move_cursor_up() 

2589 

2590 self.vertical_scroll -= 1 

2591 

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

2593 return self.content.get_key_bindings() 

2594 

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

2596 return [] 

2597 

2598 

2599class ConditionalContainer(Container): 

2600 """ 

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

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

2603 displayed or not. 

2604 

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

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

2607 """ 

2608 

2609 def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None: 

2610 self.content = to_container(content) 

2611 self.filter = to_filter(filter) 

2612 

2613 def __repr__(self) -> str: 

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

2615 

2616 def reset(self) -> None: 

2617 self.content.reset() 

2618 

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

2620 if self.filter(): 

2621 return self.content.preferred_width(max_available_width) 

2622 else: 

2623 return Dimension.zero() 

2624 

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

2626 if self.filter(): 

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

2628 else: 

2629 return Dimension.zero() 

2630 

2631 def write_to_screen( 

2632 self, 

2633 screen: Screen, 

2634 mouse_handlers: MouseHandlers, 

2635 write_position: WritePosition, 

2636 parent_style: str, 

2637 erase_bg: bool, 

2638 z_index: int | None, 

2639 ) -> None: 

2640 if self.filter(): 

2641 return self.content.write_to_screen( 

2642 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index 

2643 ) 

2644 

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

2646 return [self.content] 

2647 

2648 

2649class DynamicContainer(Container): 

2650 """ 

2651 Container class that dynamically returns any Container. 

2652 

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

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

2655 """ 

2656 

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

2658 self.get_container = get_container 

2659 

2660 def _get_container(self) -> Container: 

2661 """ 

2662 Return the current container object. 

2663 

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

2665 widget with a ``__pt_container__`` method. 

2666 """ 

2667 obj = self.get_container() 

2668 return to_container(obj) 

2669 

2670 def reset(self) -> None: 

2671 self._get_container().reset() 

2672 

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

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

2675 

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

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

2678 

2679 def write_to_screen( 

2680 self, 

2681 screen: Screen, 

2682 mouse_handlers: MouseHandlers, 

2683 write_position: WritePosition, 

2684 parent_style: str, 

2685 erase_bg: bool, 

2686 z_index: int | None, 

2687 ) -> None: 

2688 self._get_container().write_to_screen( 

2689 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index 

2690 ) 

2691 

2692 def is_modal(self) -> bool: 

2693 return False 

2694 

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

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

2697 # container. 

2698 return None 

2699 

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

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

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

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

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

2705 return [self._get_container()] 

2706 

2707 

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

2709 """ 

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

2711 """ 

2712 if isinstance(container, Container): 

2713 return container 

2714 elif hasattr(container, "__pt_container__"): 

2715 return to_container(container.__pt_container__()) 

2716 else: 

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

2718 

2719 

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

2721 """ 

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

2723 """ 

2724 if isinstance(container, Window): 

2725 return container 

2726 elif hasattr(container, "__pt_container__"): 

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

2728 else: 

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

2730 

2731 

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

2733 """ 

2734 Checks whether the given value is a container object 

2735 (for use in assert statements). 

2736 """ 

2737 if isinstance(value, Container): 

2738 return True 

2739 if hasattr(value, "__pt_container__"): 

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

2741 return False