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

968 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-03-26 06:07 +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 ( 

11 TYPE_CHECKING, 

12 Callable, 

13 Dict, 

14 List, 

15 Optional, 

16 Sequence, 

17 Tuple, 

18 Union, 

19 cast, 

20) 

21 

22from prompt_toolkit.application.current import get_app 

23from prompt_toolkit.cache import SimpleCache 

24from prompt_toolkit.data_structures import Point 

25from prompt_toolkit.filters import ( 

26 FilterOrBool, 

27 emacs_insert_mode, 

28 to_filter, 

29 vi_insert_mode, 

30) 

31from prompt_toolkit.formatted_text import ( 

32 AnyFormattedText, 

33 StyleAndTextTuples, 

34 to_formatted_text, 

35) 

36from prompt_toolkit.formatted_text.utils import ( 

37 fragment_list_to_text, 

38 fragment_list_width, 

39) 

40from prompt_toolkit.key_binding import KeyBindingsBase 

41from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 

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

43 

44from .controls import ( 

45 DummyControl, 

46 FormattedTextControl, 

47 GetLinePrefixCallable, 

48 UIContent, 

49 UIControl, 

50) 

51from .dimension import ( 

52 AnyDimension, 

53 Dimension, 

54 max_layout_dimensions, 

55 sum_layout_dimensions, 

56 to_dimension, 

57) 

58from .margins import Margin 

59from .mouse_handlers import MouseHandlers 

60from .screen import _CHAR_CACHE, Screen, WritePosition 

61from .utils import explode_text_fragments 

62 

63if TYPE_CHECKING: 

64 from typing_extensions import Protocol, TypeGuard 

65 

66 from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone 

67 

68 

69__all__ = [ 

70 "AnyContainer", 

71 "Container", 

72 "HorizontalAlign", 

73 "VerticalAlign", 

74 "HSplit", 

75 "VSplit", 

76 "FloatContainer", 

77 "Float", 

78 "WindowAlign", 

79 "Window", 

80 "WindowRenderInfo", 

81 "ConditionalContainer", 

82 "ScrollOffsets", 

83 "ColorColumn", 

84 "to_container", 

85 "to_window", 

86 "is_container", 

87 "DynamicContainer", 

88] 

89 

90 

91class Container(metaclass=ABCMeta): 

92 """ 

93 Base class for user interface layout. 

94 """ 

95 

96 @abstractmethod 

97 def reset(self) -> None: 

98 """ 

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

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

101 """ 

102 

103 @abstractmethod 

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

105 """ 

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

107 desired width for this container. 

108 """ 

109 

110 @abstractmethod 

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

112 """ 

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

114 desired height for this container. 

115 """ 

116 

117 @abstractmethod 

118 def write_to_screen( 

119 self, 

120 screen: Screen, 

121 mouse_handlers: MouseHandlers, 

122 write_position: WritePosition, 

123 parent_style: str, 

124 erase_bg: bool, 

125 z_index: int | None, 

126 ) -> None: 

127 """ 

128 Write the actual content to the screen. 

129 

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

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

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

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

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

135 style down to the windows that they contain. 

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

137 """ 

138 

139 def is_modal(self) -> bool: 

140 """ 

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

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

143 """ 

144 return False 

145 

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

147 """ 

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

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

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

151 """ 

152 return None 

153 

154 @abstractmethod 

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

156 """ 

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

158 """ 

159 return [] 

160 

161 

162if TYPE_CHECKING: 

163 

164 class MagicContainer(Protocol): 

165 """ 

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

167 """ 

168 

169 def __pt_container__(self) -> AnyContainer: 

170 ... 

171 

172 

173AnyContainer = Union[Container, "MagicContainer"] 

174 

175 

176def _window_too_small() -> Window: 

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

178 return Window( 

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

180 ) 

181 

182 

183class VerticalAlign(Enum): 

184 "Alignment for `HSplit`." 

185 TOP = "TOP" 

186 CENTER = "CENTER" 

187 BOTTOM = "BOTTOM" 

188 JUSTIFY = "JUSTIFY" 

189 

190 

191class HorizontalAlign(Enum): 

192 "Alignment for `VSplit`." 

193 LEFT = "LEFT" 

194 CENTER = "CENTER" 

195 RIGHT = "RIGHT" 

196 JUSTIFY = "JUSTIFY" 

197 

198 

199class _Split(Container): 

200 """ 

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

202 """ 

203 

204 def __init__( 

205 self, 

206 children: Sequence[AnyContainer], 

207 window_too_small: Container | None = None, 

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

209 padding_char: str | None = None, 

210 padding_style: str = "", 

211 width: AnyDimension = None, 

212 height: AnyDimension = None, 

213 z_index: int | None = None, 

214 modal: bool = False, 

215 key_bindings: KeyBindingsBase | None = None, 

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

217 ) -> None: 

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

219 self.window_too_small = window_too_small or _window_too_small() 

220 self.padding = padding 

221 self.padding_char = padding_char 

222 self.padding_style = padding_style 

223 

224 self.width = width 

225 self.height = height 

226 self.z_index = z_index 

227 

228 self.modal = modal 

229 self.key_bindings = key_bindings 

230 self.style = style 

231 

232 def is_modal(self) -> bool: 

233 return self.modal 

234 

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

236 return self.key_bindings 

237 

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

239 return self.children 

240 

241 

242class HSplit(_Split): 

243 """ 

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

245 

246 +--------------------+ 

247 | | 

248 +--------------------+ 

249 | | 

250 +--------------------+ 

251 

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

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

254 

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

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

257 

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

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

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

261 "Window too small" message. 

262 :param align: `VerticalAlign` value. 

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

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

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

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

267 :param style: A style string. 

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

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

270 

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

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

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

274 """ 

275 

276 def __init__( 

277 self, 

278 children: Sequence[AnyContainer], 

279 window_too_small: Container | None = None, 

280 align: VerticalAlign = VerticalAlign.JUSTIFY, 

281 padding: AnyDimension = 0, 

282 padding_char: str | None = None, 

283 padding_style: str = "", 

284 width: AnyDimension = None, 

285 height: AnyDimension = None, 

286 z_index: int | None = None, 

287 modal: bool = False, 

288 key_bindings: KeyBindingsBase | None = None, 

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

290 ) -> None: 

291 super().__init__( 

292 children=children, 

293 window_too_small=window_too_small, 

294 padding=padding, 

295 padding_char=padding_char, 

296 padding_style=padding_style, 

297 width=width, 

298 height=height, 

299 z_index=z_index, 

300 modal=modal, 

301 key_bindings=key_bindings, 

302 style=style, 

303 ) 

304 

305 self.align = align 

306 

307 self._children_cache: SimpleCache[ 

308 tuple[Container, ...], list[Container] 

309 ] = SimpleCache(maxsize=1) 

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

311 

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

313 if self.width is not None: 

314 return to_dimension(self.width) 

315 

316 if self.children: 

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

318 return max_layout_dimensions(dimensions) 

319 else: 

320 return Dimension() 

321 

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

323 if self.height is not None: 

324 return to_dimension(self.height) 

325 

326 dimensions = [ 

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

328 ] 

329 return sum_layout_dimensions(dimensions) 

330 

331 def reset(self) -> None: 

332 for c in self.children: 

333 c.reset() 

334 

335 @property 

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

337 """ 

338 List of child objects, including padding. 

339 """ 

340 

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

342 result: list[Container] = [] 

343 

344 # Padding Top. 

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

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

347 

348 # The children with padding. 

349 for child in self.children: 

350 result.append(child) 

351 result.append( 

352 Window( 

353 height=self.padding, 

354 char=self.padding_char, 

355 style=self.padding_style, 

356 ) 

357 ) 

358 if result: 

359 result.pop() 

360 

361 # Padding right. 

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

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

364 

365 return result 

366 

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

368 

369 def write_to_screen( 

370 self, 

371 screen: Screen, 

372 mouse_handlers: MouseHandlers, 

373 write_position: WritePosition, 

374 parent_style: str, 

375 erase_bg: bool, 

376 z_index: int | None, 

377 ) -> None: 

378 """ 

379 Render the prompt to a `Screen` instance. 

380 

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

382 to which the output has to be written. 

383 """ 

384 sizes = self._divide_heights(write_position) 

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

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

387 

388 if sizes is None: 

389 self.window_too_small.write_to_screen( 

390 screen, mouse_handlers, write_position, style, erase_bg, z_index 

391 ) 

392 else: 

393 # 

394 ypos = write_position.ypos 

395 xpos = write_position.xpos 

396 width = write_position.width 

397 

398 # Draw child panes. 

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

400 c.write_to_screen( 

401 screen, 

402 mouse_handlers, 

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

404 style, 

405 erase_bg, 

406 z_index, 

407 ) 

408 ypos += s 

409 

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

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

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

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

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

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

416 if remaining_height > 0: 

417 self._remaining_space_window.write_to_screen( 

418 screen, 

419 mouse_handlers, 

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

421 style, 

422 erase_bg, 

423 z_index, 

424 ) 

425 

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

427 """ 

428 Return the heights for all rows. 

429 Or None when there is not enough space. 

430 """ 

431 if not self.children: 

432 return [] 

433 

434 width = write_position.width 

435 height = write_position.height 

436 

437 # Calculate heights. 

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

439 

440 # Sum dimensions 

441 sum_dimensions = sum_layout_dimensions(dimensions) 

442 

443 # If there is not enough space for both. 

444 # Don't do anything. 

445 if sum_dimensions.min > height: 

446 return None 

447 

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

449 # the whole height.) 

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

451 

452 child_generator = take_using_weights( 

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

454 ) 

455 

456 i = next(child_generator) 

457 

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

459 preferred_stop = min(height, sum_dimensions.preferred) 

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

461 

462 while sum(sizes) < preferred_stop: 

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

464 sizes[i] += 1 

465 i = next(child_generator) 

466 

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

468 if not get_app().is_done: 

469 max_stop = min(height, sum_dimensions.max) 

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

471 

472 while sum(sizes) < max_stop: 

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

474 sizes[i] += 1 

475 i = next(child_generator) 

476 

477 return sizes 

478 

479 

480class VSplit(_Split): 

481 """ 

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

483 

484 +---------+----------+ 

485 | | | 

486 | | | 

487 +---------+----------+ 

488 

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

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

491 

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

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

494 

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

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

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

498 "Window too small" message. 

499 :param align: `HorizontalAlign` value. 

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

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

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

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

504 :param style: A style string. 

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

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

507 

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

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

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

511 """ 

512 

513 def __init__( 

514 self, 

515 children: Sequence[AnyContainer], 

516 window_too_small: Container | None = None, 

517 align: HorizontalAlign = HorizontalAlign.JUSTIFY, 

518 padding: AnyDimension = 0, 

519 padding_char: str | None = None, 

520 padding_style: str = "", 

521 width: AnyDimension = None, 

522 height: AnyDimension = None, 

523 z_index: int | None = None, 

524 modal: bool = False, 

525 key_bindings: KeyBindingsBase | None = None, 

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

527 ) -> None: 

528 super().__init__( 

529 children=children, 

530 window_too_small=window_too_small, 

531 padding=padding, 

532 padding_char=padding_char, 

533 padding_style=padding_style, 

534 width=width, 

535 height=height, 

536 z_index=z_index, 

537 modal=modal, 

538 key_bindings=key_bindings, 

539 style=style, 

540 ) 

541 

542 self.align = align 

543 

544 self._children_cache: SimpleCache[ 

545 tuple[Container, ...], list[Container] 

546 ] = SimpleCache(maxsize=1) 

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

548 

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

550 if self.width is not None: 

551 return to_dimension(self.width) 

552 

553 dimensions = [ 

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

555 ] 

556 

557 return sum_layout_dimensions(dimensions) 

558 

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

560 if self.height is not None: 

561 return to_dimension(self.height) 

562 

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

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

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

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

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

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

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

570 

571 sizes = self._divide_widths(width) 

572 children = self._all_children 

573 

574 if sizes is None: 

575 return Dimension() 

576 else: 

577 dimensions = [ 

578 c.preferred_height(s, max_available_height) 

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

580 ] 

581 return max_layout_dimensions(dimensions) 

582 

583 def reset(self) -> None: 

584 for c in self.children: 

585 c.reset() 

586 

587 @property 

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

589 """ 

590 List of child objects, including padding. 

591 """ 

592 

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

594 result: list[Container] = [] 

595 

596 # Padding left. 

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

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

599 

600 # The children with padding. 

601 for child in self.children: 

602 result.append(child) 

603 result.append( 

604 Window( 

605 width=self.padding, 

606 char=self.padding_char, 

607 style=self.padding_style, 

608 ) 

609 ) 

610 if result: 

611 result.pop() 

612 

613 # Padding right. 

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

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

616 

617 return result 

618 

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

620 

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

622 """ 

623 Return the widths for all columns. 

624 Or None when there is not enough space. 

625 """ 

626 children = self._all_children 

627 

628 if not children: 

629 return [] 

630 

631 # Calculate widths. 

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

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

634 

635 # Sum dimensions 

636 sum_dimensions = sum_layout_dimensions(dimensions) 

637 

638 # If there is not enough space for both. 

639 # Don't do anything. 

640 if sum_dimensions.min > width: 

641 return None 

642 

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

644 # the whole width.) 

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

646 

647 child_generator = take_using_weights( 

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

649 ) 

650 

651 i = next(child_generator) 

652 

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

654 preferred_stop = min(width, sum_dimensions.preferred) 

655 

656 while sum(sizes) < preferred_stop: 

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

658 sizes[i] += 1 

659 i = next(child_generator) 

660 

661 # Increase until we use all the available space. 

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

663 max_stop = min(width, sum_dimensions.max) 

664 

665 while sum(sizes) < max_stop: 

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

667 sizes[i] += 1 

668 i = next(child_generator) 

669 

670 return sizes 

671 

672 def write_to_screen( 

673 self, 

674 screen: Screen, 

675 mouse_handlers: MouseHandlers, 

676 write_position: WritePosition, 

677 parent_style: str, 

678 erase_bg: bool, 

679 z_index: int | None, 

680 ) -> None: 

681 """ 

682 Render the prompt to a `Screen` instance. 

683 

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

685 to which the output has to be written. 

686 """ 

687 if not self.children: 

688 return 

689 

690 children = self._all_children 

691 sizes = self._divide_widths(write_position.width) 

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

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

694 

695 # If there is not enough space. 

696 if sizes is None: 

697 self.window_too_small.write_to_screen( 

698 screen, mouse_handlers, write_position, style, erase_bg, z_index 

699 ) 

700 return 

701 

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

703 # write_position.height. 

704 heights = [ 

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

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

707 ] 

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

709 

710 # 

711 ypos = write_position.ypos 

712 xpos = write_position.xpos 

713 

714 # Draw all child panes. 

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

716 c.write_to_screen( 

717 screen, 

718 mouse_handlers, 

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

720 style, 

721 erase_bg, 

722 z_index, 

723 ) 

724 xpos += s 

725 

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

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

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

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

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

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

732 if remaining_width > 0: 

733 self._remaining_space_window.write_to_screen( 

734 screen, 

735 mouse_handlers, 

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

737 style, 

738 erase_bg, 

739 z_index, 

740 ) 

741 

742 

743class FloatContainer(Container): 

744 """ 

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

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

747 

748 Example Usage:: 

749 

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

751 floats=[ 

752 Float(xcursor=True, 

753 ycursor=True, 

754 content=CompletionsMenu(...)) 

755 ]) 

756 

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

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

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

760 """ 

761 

762 def __init__( 

763 self, 

764 content: AnyContainer, 

765 floats: list[Float], 

766 modal: bool = False, 

767 key_bindings: KeyBindingsBase | None = None, 

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

769 z_index: int | None = None, 

770 ) -> None: 

771 self.content = to_container(content) 

772 self.floats = floats 

773 

774 self.modal = modal 

775 self.key_bindings = key_bindings 

776 self.style = style 

777 self.z_index = z_index 

778 

779 def reset(self) -> None: 

780 self.content.reset() 

781 

782 for f in self.floats: 

783 f.content.reset() 

784 

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

786 return self.content.preferred_width(max_available_width) 

787 

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

789 """ 

790 Return the preferred height of the float container. 

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

792 into the dimensions provided by the container.) 

793 """ 

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

795 

796 def write_to_screen( 

797 self, 

798 screen: Screen, 

799 mouse_handlers: MouseHandlers, 

800 write_position: WritePosition, 

801 parent_style: str, 

802 erase_bg: bool, 

803 z_index: int | None, 

804 ) -> None: 

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

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

807 

808 self.content.write_to_screen( 

809 screen, mouse_handlers, write_position, style, erase_bg, z_index 

810 ) 

811 

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

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

814 # container and the `Float`. 

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

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

817 

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

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

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

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

822 # enough for now.) 

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

824 

825 if postpone: 

826 new_z_index = ( 

827 number + 10**8 

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

829 screen.draw_with_z_index( 

830 z_index=new_z_index, 

831 draw_func=partial( 

832 self._draw_float, 

833 fl, 

834 screen, 

835 mouse_handlers, 

836 write_position, 

837 style, 

838 erase_bg, 

839 new_z_index, 

840 ), 

841 ) 

842 else: 

843 self._draw_float( 

844 fl, 

845 screen, 

846 mouse_handlers, 

847 write_position, 

848 style, 

849 erase_bg, 

850 new_z_index, 

851 ) 

852 

853 def _draw_float( 

854 self, 

855 fl: Float, 

856 screen: Screen, 

857 mouse_handlers: MouseHandlers, 

858 write_position: WritePosition, 

859 style: str, 

860 erase_bg: bool, 

861 z_index: int | None, 

862 ) -> None: 

863 "Draw a single Float." 

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

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

866 # relative to the write_position.) 

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

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

869 cpos = screen.get_menu_position( 

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

871 ) 

872 cursor_position = Point( 

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

874 ) 

875 

876 fl_width = fl.get_width() 

877 fl_height = fl.get_height() 

878 width: int 

879 height: int 

880 xpos: int 

881 ypos: int 

882 

883 # Left & width given. 

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

885 xpos = fl.left 

886 width = fl_width 

887 # Left & right given -> calculate width. 

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

889 xpos = fl.left 

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

891 # Width & right given -> calculate left. 

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

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

894 width = fl_width 

895 # Near x position of cursor. 

896 elif fl.xcursor: 

897 if fl_width is None: 

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

899 width = min(write_position.width, width) 

900 else: 

901 width = fl_width 

902 

903 xpos = cursor_position.x 

904 if xpos + width > write_position.width: 

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

906 # Only width given -> center horizontally. 

907 elif fl_width: 

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

909 width = fl_width 

910 # Otherwise, take preferred width from float content. 

911 else: 

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

913 

914 if fl.left is not None: 

915 xpos = fl.left 

916 elif fl.right is not None: 

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

918 else: # Center horizontally. 

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

920 

921 # Trim. 

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

923 

924 # Top & height given. 

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

926 ypos = fl.top 

927 height = fl_height 

928 # Top & bottom given -> calculate height. 

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

930 ypos = fl.top 

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

932 # Height & bottom given -> calculate top. 

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

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

935 height = fl_height 

936 # Near cursor. 

937 elif fl.ycursor: 

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

939 

940 if fl_height is None: 

941 height = fl.content.preferred_height( 

942 width, write_position.height 

943 ).preferred 

944 else: 

945 height = fl_height 

946 

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

948 # when the content requires it.) 

949 if height > write_position.height - ypos: 

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

951 # When the space below the cursor is more than 

952 # the space above, just reduce the height. 

953 height = write_position.height - ypos 

954 else: 

955 # Otherwise, fit the float above the cursor. 

956 height = min(height, cursor_position.y) 

957 ypos = cursor_position.y - height 

958 

959 # Only height given -> center vertically. 

960 elif fl_height: 

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

962 height = fl_height 

963 # Otherwise, take preferred height from content. 

964 else: 

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

966 

967 if fl.top is not None: 

968 ypos = fl.top 

969 elif fl.bottom is not None: 

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

971 else: # Center vertically. 

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

973 

974 # Trim. 

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

976 

977 # Write float. 

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

979 if height > 0 and width > 0: 

980 wp = WritePosition( 

981 xpos=xpos + write_position.xpos, 

982 ypos=ypos + write_position.ypos, 

983 width=width, 

984 height=height, 

985 ) 

986 

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

988 fl.content.write_to_screen( 

989 screen, 

990 mouse_handlers, 

991 wp, 

992 style, 

993 erase_bg=not fl.transparent(), 

994 z_index=z_index, 

995 ) 

996 

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

998 """ 

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

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

1001 """ 

1002 wp = write_position 

1003 

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

1005 if y in screen.data_buffer: 

1006 row = screen.data_buffer[y] 

1007 

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

1009 c = row[x] 

1010 if c.char != " ": 

1011 return False 

1012 

1013 return True 

1014 

1015 def is_modal(self) -> bool: 

1016 return self.modal 

1017 

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

1019 return self.key_bindings 

1020 

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

1022 children = [self.content] 

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

1024 return children 

1025 

1026 

1027class Float: 

1028 """ 

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

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

1031 

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

1033 

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

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

1036 

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

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

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

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

1041 

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

1043 the current window. 

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

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

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

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

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

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

1050 drawn transparently. 

1051 """ 

1052 

1053 def __init__( 

1054 self, 

1055 content: AnyContainer, 

1056 top: int | None = None, 

1057 right: int | None = None, 

1058 bottom: int | None = None, 

1059 left: int | None = None, 

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

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

1062 xcursor: bool = False, 

1063 ycursor: bool = False, 

1064 attach_to_window: AnyContainer | None = None, 

1065 hide_when_covering_content: bool = False, 

1066 allow_cover_cursor: bool = False, 

1067 z_index: int = 1, 

1068 transparent: bool = False, 

1069 ) -> None: 

1070 assert z_index >= 1 

1071 

1072 self.left = left 

1073 self.right = right 

1074 self.top = top 

1075 self.bottom = bottom 

1076 

1077 self.width = width 

1078 self.height = height 

1079 

1080 self.xcursor = xcursor 

1081 self.ycursor = ycursor 

1082 

1083 self.attach_to_window = ( 

1084 to_window(attach_to_window) if attach_to_window else None 

1085 ) 

1086 

1087 self.content = to_container(content) 

1088 self.hide_when_covering_content = hide_when_covering_content 

1089 self.allow_cover_cursor = allow_cover_cursor 

1090 self.z_index = z_index 

1091 self.transparent = to_filter(transparent) 

1092 

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

1094 if callable(self.width): 

1095 return self.width() 

1096 return self.width 

1097 

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

1099 if callable(self.height): 

1100 return self.height() 

1101 return self.height 

1102 

1103 def __repr__(self) -> str: 

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

1105 

1106 

1107class WindowRenderInfo: 

1108 """ 

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

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

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

1112 render position on the output screen. 

1113 

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

1115 well as implementing mouse support.) 

1116 

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

1118 the whole input, without clipping. (ui_content) 

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

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

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

1122 without the margins. 

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

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

1125 :class:`Window` instance. 

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

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

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

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

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

1131 the rendered screen. 

1132 """ 

1133 

1134 def __init__( 

1135 self, 

1136 window: Window, 

1137 ui_content: UIContent, 

1138 horizontal_scroll: int, 

1139 vertical_scroll: int, 

1140 window_width: int, 

1141 window_height: int, 

1142 configured_scroll_offsets: ScrollOffsets, 

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

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

1145 x_offset: int, 

1146 y_offset: int, 

1147 wrap_lines: bool, 

1148 ) -> None: 

1149 self.window = window 

1150 self.ui_content = ui_content 

1151 self.vertical_scroll = vertical_scroll 

1152 self.window_width = window_width # Width without margins. 

1153 self.window_height = window_height 

1154 

1155 self.configured_scroll_offsets = configured_scroll_offsets 

1156 self.visible_line_to_row_col = visible_line_to_row_col 

1157 self.wrap_lines = wrap_lines 

1158 

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

1160 # screen coordinates. 

1161 self._x_offset = x_offset 

1162 self._y_offset = y_offset 

1163 

1164 @property 

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

1166 return { 

1167 visible_line: rowcol[0] 

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

1169 } 

1170 

1171 @property 

1172 def cursor_position(self) -> Point: 

1173 """ 

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

1175 of the rendered screen. 

1176 """ 

1177 cpos = self.ui_content.cursor_position 

1178 try: 

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

1180 except KeyError: 

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

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

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

1184 else: 

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

1186 

1187 @property 

1188 def applied_scroll_offsets(self) -> ScrollOffsets: 

1189 """ 

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

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

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

1193 than what's configured. 

1194 """ 

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

1196 top = 0 

1197 else: 

1198 # Get row where the cursor is displayed. 

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

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

1201 

1202 return ScrollOffsets( 

1203 top=top, 

1204 bottom=min( 

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

1206 self.configured_scroll_offsets.bottom, 

1207 ), 

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

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

1210 # double width characters in mind.) 

1211 left=0, 

1212 right=0, 

1213 ) 

1214 

1215 @property 

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

1217 """ 

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

1219 The last line may not be entirely visible. 

1220 """ 

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

1222 

1223 @property 

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

1225 """ 

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

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

1228 the first row appears in the dictionary. 

1229 """ 

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

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

1232 if v in result: 

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

1234 else: 

1235 result[v] = k 

1236 return result 

1237 

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

1239 """ 

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

1241 with the first visible line. 

1242 """ 

1243 if after_scroll_offset: 

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

1245 else: 

1246 return self.displayed_lines[0] 

1247 

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

1249 """ 

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

1251 """ 

1252 if before_scroll_offset: 

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

1254 else: 

1255 return self.displayed_lines[-1] 

1256 

1257 def center_visible_line( 

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

1259 ) -> int: 

1260 """ 

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

1262 """ 

1263 return ( 

1264 self.first_visible_line(after_scroll_offset) 

1265 + ( 

1266 self.last_visible_line(before_scroll_offset) 

1267 - self.first_visible_line(after_scroll_offset) 

1268 ) 

1269 // 2 

1270 ) 

1271 

1272 @property 

1273 def content_height(self) -> int: 

1274 """ 

1275 The full height of the user control. 

1276 """ 

1277 return self.ui_content.line_count 

1278 

1279 @property 

1280 def full_height_visible(self) -> bool: 

1281 """ 

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

1283 """ 

1284 return ( 

1285 self.vertical_scroll == 0 

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

1287 ) 

1288 

1289 @property 

1290 def top_visible(self) -> bool: 

1291 """ 

1292 True when the top of the buffer is visible. 

1293 """ 

1294 return self.vertical_scroll == 0 

1295 

1296 @property 

1297 def bottom_visible(self) -> bool: 

1298 """ 

1299 True when the bottom of the buffer is visible. 

1300 """ 

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

1302 

1303 @property 

1304 def vertical_scroll_percentage(self) -> int: 

1305 """ 

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

1307 100 means: the bottom is visible.) 

1308 """ 

1309 if self.bottom_visible: 

1310 return 100 

1311 else: 

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

1313 

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

1315 """ 

1316 Return the height of the given line. 

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

1318 """ 

1319 if self.wrap_lines: 

1320 return self.ui_content.get_height_for_line( 

1321 lineno, self.window_width, self.window.get_line_prefix 

1322 ) 

1323 else: 

1324 return 1 

1325 

1326 

1327class ScrollOffsets: 

1328 """ 

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

1330 

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

1332 """ 

1333 

1334 def __init__( 

1335 self, 

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

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

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

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

1340 ) -> None: 

1341 self._top = top 

1342 self._bottom = bottom 

1343 self._left = left 

1344 self._right = right 

1345 

1346 @property 

1347 def top(self) -> int: 

1348 return to_int(self._top) 

1349 

1350 @property 

1351 def bottom(self) -> int: 

1352 return to_int(self._bottom) 

1353 

1354 @property 

1355 def left(self) -> int: 

1356 return to_int(self._left) 

1357 

1358 @property 

1359 def right(self) -> int: 

1360 return to_int(self._right) 

1361 

1362 def __repr__(self) -> str: 

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

1364 self._top, 

1365 self._bottom, 

1366 self._left, 

1367 self._right, 

1368 ) 

1369 

1370 

1371class ColorColumn: 

1372 """ 

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

1374 """ 

1375 

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

1377 self.position = position 

1378 self.style = style 

1379 

1380 

1381_in_insert_mode = vi_insert_mode | emacs_insert_mode 

1382 

1383 

1384class WindowAlign(Enum): 

1385 """ 

1386 Alignment of the Window content. 

1387 

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

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

1390 `VSplit` and `HSplit`. 

1391 """ 

1392 

1393 LEFT = "LEFT" 

1394 RIGHT = "RIGHT" 

1395 CENTER = "CENTER" 

1396 

1397 

1398class Window(Container): 

1399 """ 

1400 Container that holds a control. 

1401 

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

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

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

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

1406 of floating elements. 

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

1408 preferred width reported by the control. 

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

1410 preferred height reported by the control. 

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

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

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

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

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

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

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

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

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

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

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

1422 will be centered vertically most of the time. 

1423 :param allow_scroll_beyond_bottom: A `bool` or 

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

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

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

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

1428 the body is hidden. 

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

1430 scroll horizontally, but wrap lines instead. 

1431 :param get_vertical_scroll: Callable that takes this window 

1432 instance as input and returns a preferred vertical scroll. 

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

1434 current cursor position.) 

1435 :param get_horizontal_scroll: Callable that takes this window 

1436 instance as input and returns a preferred vertical scroll. 

1437 :param always_hide_cursor: A `bool` or 

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

1439 when the user control specifies a cursor position. 

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

1441 display a cursorline. 

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

1443 display a cursorcolumn. 

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

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

1446 a list. 

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

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

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

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

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

1452 can also be a callable that returns a character. 

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

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

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

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

1457 so on. 

1458 """ 

1459 

1460 def __init__( 

1461 self, 

1462 content: UIControl | None = None, 

1463 width: AnyDimension = None, 

1464 height: AnyDimension = None, 

1465 z_index: int | None = None, 

1466 dont_extend_width: FilterOrBool = False, 

1467 dont_extend_height: FilterOrBool = False, 

1468 ignore_content_width: FilterOrBool = False, 

1469 ignore_content_height: FilterOrBool = False, 

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

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

1472 scroll_offsets: ScrollOffsets | None = None, 

1473 allow_scroll_beyond_bottom: FilterOrBool = False, 

1474 wrap_lines: FilterOrBool = False, 

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

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

1477 always_hide_cursor: FilterOrBool = False, 

1478 cursorline: FilterOrBool = False, 

1479 cursorcolumn: FilterOrBool = False, 

1480 colorcolumns: ( 

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

1482 ) = None, 

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

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

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

1486 get_line_prefix: GetLinePrefixCallable | None = None, 

1487 ) -> None: 

1488 self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom) 

1489 self.always_hide_cursor = to_filter(always_hide_cursor) 

1490 self.wrap_lines = to_filter(wrap_lines) 

1491 self.cursorline = to_filter(cursorline) 

1492 self.cursorcolumn = to_filter(cursorcolumn) 

1493 

1494 self.content = content or DummyControl() 

1495 self.dont_extend_width = to_filter(dont_extend_width) 

1496 self.dont_extend_height = to_filter(dont_extend_height) 

1497 self.ignore_content_width = to_filter(ignore_content_width) 

1498 self.ignore_content_height = to_filter(ignore_content_height) 

1499 self.left_margins = left_margins or [] 

1500 self.right_margins = right_margins or [] 

1501 self.scroll_offsets = scroll_offsets or ScrollOffsets() 

1502 self.get_vertical_scroll = get_vertical_scroll 

1503 self.get_horizontal_scroll = get_horizontal_scroll 

1504 self.colorcolumns = colorcolumns or [] 

1505 self.align = align 

1506 self.style = style 

1507 self.char = char 

1508 self.get_line_prefix = get_line_prefix 

1509 

1510 self.width = width 

1511 self.height = height 

1512 self.z_index = z_index 

1513 

1514 # Cache for the screens generated by the margin. 

1515 self._ui_content_cache: SimpleCache[ 

1516 tuple[int, int, int], UIContent 

1517 ] = SimpleCache(maxsize=8) 

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

1519 maxsize=1 

1520 ) 

1521 

1522 self.reset() 

1523 

1524 def __repr__(self) -> str: 

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

1526 

1527 def reset(self) -> None: 

1528 self.content.reset() 

1529 

1530 #: Scrolling position of the main content. 

1531 self.vertical_scroll = 0 

1532 self.horizontal_scroll = 0 

1533 

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

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

1536 # all of the vertical space. 

1537 self.vertical_scroll_2 = 0 

1538 

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

1540 #: output.) 

1541 self.render_info: WindowRenderInfo | None = None 

1542 

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

1544 """ 

1545 Return the width for this margin. 

1546 (Calculate only once per render time.) 

1547 """ 

1548 

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

1550 def get_ui_content() -> UIContent: 

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

1552 

1553 def get_width() -> int: 

1554 return margin.get_width(get_ui_content) 

1555 

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

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

1558 

1559 def _get_total_margin_width(self) -> int: 

1560 """ 

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

1562 """ 

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

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

1565 ) 

1566 

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

1568 """ 

1569 Calculate the preferred width for this window. 

1570 """ 

1571 

1572 def preferred_content_width() -> int | None: 

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

1574 window was given.""" 

1575 if self.ignore_content_width(): 

1576 return None 

1577 

1578 # Calculate the width of the margin. 

1579 total_margin_width = self._get_total_margin_width() 

1580 

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

1582 preferred_width = self.content.preferred_width( 

1583 max_available_width - total_margin_width 

1584 ) 

1585 

1586 if preferred_width is not None: 

1587 # Include width of the margins. 

1588 preferred_width += total_margin_width 

1589 return preferred_width 

1590 

1591 # Merge. 

1592 return self._merge_dimensions( 

1593 dimension=to_dimension(self.width), 

1594 get_preferred=preferred_content_width, 

1595 dont_extend=self.dont_extend_width(), 

1596 ) 

1597 

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

1599 """ 

1600 Calculate the preferred height for this window. 

1601 """ 

1602 

1603 def preferred_content_height() -> int | None: 

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

1605 window was given.""" 

1606 if self.ignore_content_height(): 

1607 return None 

1608 

1609 total_margin_width = self._get_total_margin_width() 

1610 wrap_lines = self.wrap_lines() 

1611 

1612 return self.content.preferred_height( 

1613 width - total_margin_width, 

1614 max_available_height, 

1615 wrap_lines, 

1616 self.get_line_prefix, 

1617 ) 

1618 

1619 return self._merge_dimensions( 

1620 dimension=to_dimension(self.height), 

1621 get_preferred=preferred_content_height, 

1622 dont_extend=self.dont_extend_height(), 

1623 ) 

1624 

1625 @staticmethod 

1626 def _merge_dimensions( 

1627 dimension: Dimension | None, 

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

1629 dont_extend: bool = False, 

1630 ) -> Dimension: 

1631 """ 

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

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

1634 parent container. 

1635 """ 

1636 dimension = dimension or Dimension() 

1637 

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

1639 # ignore the UIControl. 

1640 preferred: int | None 

1641 

1642 if dimension.preferred_specified: 

1643 preferred = dimension.preferred 

1644 else: 

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

1646 # content. 

1647 preferred = get_preferred() 

1648 

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

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

1651 if preferred is not None: 

1652 if dimension.max_specified: 

1653 preferred = min(preferred, dimension.max) 

1654 

1655 if dimension.min_specified: 

1656 preferred = max(preferred, dimension.min) 

1657 

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

1659 # also as the max dimension. 

1660 max_: int | None 

1661 min_: int | None 

1662 

1663 if dont_extend and preferred is not None: 

1664 max_ = min(dimension.max, preferred) 

1665 else: 

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

1667 

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

1669 

1670 return Dimension( 

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

1672 ) 

1673 

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

1675 """ 

1676 Create a `UIContent` instance. 

1677 """ 

1678 

1679 def get_content() -> UIContent: 

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

1681 

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

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

1684 

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

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

1687 app = get_app() 

1688 if app.quoted_insert: 

1689 return "^" 

1690 if app.vi_state.waiting_for_digraph: 

1691 if app.vi_state.digraph_symbol1: 

1692 return app.vi_state.digraph_symbol1 

1693 return "?" 

1694 return None 

1695 

1696 def write_to_screen( 

1697 self, 

1698 screen: Screen, 

1699 mouse_handlers: MouseHandlers, 

1700 write_position: WritePosition, 

1701 parent_style: str, 

1702 erase_bg: bool, 

1703 z_index: int | None, 

1704 ) -> None: 

1705 """ 

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

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

1708 """ 

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

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

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

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

1713 write_position = WritePosition( 

1714 xpos=write_position.xpos, 

1715 ypos=write_position.ypos, 

1716 width=write_position.width, 

1717 height=write_position.height, 

1718 ) 

1719 

1720 if self.dont_extend_width(): 

1721 write_position.width = min( 

1722 write_position.width, 

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

1724 ) 

1725 

1726 if self.dont_extend_height(): 

1727 write_position.height = min( 

1728 write_position.height, 

1729 self.preferred_height( 

1730 write_position.width, write_position.height 

1731 ).preferred, 

1732 ) 

1733 

1734 # Draw 

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

1736 

1737 draw_func = partial( 

1738 self._write_to_screen_at_index, 

1739 screen, 

1740 mouse_handlers, 

1741 write_position, 

1742 parent_style, 

1743 erase_bg, 

1744 ) 

1745 

1746 if z_index is None or z_index <= 0: 

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

1748 draw_func() 

1749 else: 

1750 # Otherwise, postpone. 

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

1752 

1753 def _write_to_screen_at_index( 

1754 self, 

1755 screen: Screen, 

1756 mouse_handlers: MouseHandlers, 

1757 write_position: WritePosition, 

1758 parent_style: str, 

1759 erase_bg: bool, 

1760 ) -> None: 

1761 # Don't bother writing invisible windows. 

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

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

1764 return 

1765 

1766 # Calculate margin sizes. 

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

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

1769 total_margin_width = sum(left_margin_widths + right_margin_widths) 

1770 

1771 # Render UserControl. 

1772 ui_content = self.content.create_content( 

1773 write_position.width - total_margin_width, write_position.height 

1774 ) 

1775 assert isinstance(ui_content, UIContent) 

1776 

1777 # Scroll content. 

1778 wrap_lines = self.wrap_lines() 

1779 self._scroll( 

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

1781 ) 

1782 

1783 # Erase background and fill with `char`. 

1784 self._fill_bg(screen, write_position, erase_bg) 

1785 

1786 # Resolve `align` attribute. 

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

1788 

1789 # Write body 

1790 visible_line_to_row_col, rowcol_to_yx = self._copy_body( 

1791 ui_content, 

1792 screen, 

1793 write_position, 

1794 sum(left_margin_widths), 

1795 write_position.width - total_margin_width, 

1796 self.vertical_scroll, 

1797 self.horizontal_scroll, 

1798 wrap_lines=wrap_lines, 

1799 highlight_lines=True, 

1800 vertical_scroll_2=self.vertical_scroll_2, 

1801 always_hide_cursor=self.always_hide_cursor(), 

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

1803 align=align, 

1804 get_line_prefix=self.get_line_prefix, 

1805 ) 

1806 

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

1808 x_offset = write_position.xpos + sum(left_margin_widths) 

1809 y_offset = write_position.ypos 

1810 

1811 render_info = WindowRenderInfo( 

1812 window=self, 

1813 ui_content=ui_content, 

1814 horizontal_scroll=self.horizontal_scroll, 

1815 vertical_scroll=self.vertical_scroll, 

1816 window_width=write_position.width - total_margin_width, 

1817 window_height=write_position.height, 

1818 configured_scroll_offsets=self.scroll_offsets, 

1819 visible_line_to_row_col=visible_line_to_row_col, 

1820 rowcol_to_yx=rowcol_to_yx, 

1821 x_offset=x_offset, 

1822 y_offset=y_offset, 

1823 wrap_lines=wrap_lines, 

1824 ) 

1825 self.render_info = render_info 

1826 

1827 # Set mouse handlers. 

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

1829 """ 

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

1831 screen coordinates into line coordinates. 

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

1833 """ 

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

1835 # the UI. 

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

1837 return NotImplemented 

1838 

1839 # Find row/col position first. 

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

1841 y = mouse_event.position.y 

1842 x = mouse_event.position.x 

1843 

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

1845 # last line instead. 

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

1847 y = min(max_y, y) 

1848 result: NotImplementedOrNone 

1849 

1850 while x >= 0: 

1851 try: 

1852 row, col = yx_to_rowcol[y, x] 

1853 except KeyError: 

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

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

1856 x -= 1 

1857 else: 

1858 # Found position, call handler of UIControl. 

1859 result = self.content.mouse_handler( 

1860 MouseEvent( 

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

1862 event_type=mouse_event.event_type, 

1863 button=mouse_event.button, 

1864 modifiers=mouse_event.modifiers, 

1865 ) 

1866 ) 

1867 break 

1868 else: 

1869 # nobreak. 

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

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

1872 # Report (0,0) instead.) 

1873 result = self.content.mouse_handler( 

1874 MouseEvent( 

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

1876 event_type=mouse_event.event_type, 

1877 button=mouse_event.button, 

1878 modifiers=mouse_event.modifiers, 

1879 ) 

1880 ) 

1881 

1882 # If it returns NotImplemented, handle it here. 

1883 if result == NotImplemented: 

1884 result = self._mouse_handler(mouse_event) 

1885 

1886 return result 

1887 

1888 mouse_handlers.set_mouse_handler_for_range( 

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

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

1891 y_min=write_position.ypos, 

1892 y_max=write_position.ypos + write_position.height, 

1893 handler=mouse_handler, 

1894 ) 

1895 

1896 # Render and copy margins. 

1897 move_x = 0 

1898 

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

1900 "Render margin. Return `Screen`." 

1901 # Retrieve margin fragments. 

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

1903 

1904 # Turn it into a UIContent object. 

1905 # already rendered those fragments using this size.) 

1906 return FormattedTextControl(fragments).create_content( 

1907 width + 1, write_position.height 

1908 ) 

1909 

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

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

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 move_x = write_position.width - sum(right_margin_widths) 

1920 

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

1922 # Create screen for margin. 

1923 margin_content = render_margin(m, width) 

1924 

1925 # Copy and shift X. 

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

1927 move_x += width 

1928 

1929 # Apply 'self.style' 

1930 self._apply_style(screen, write_position, parent_style) 

1931 

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

1933 # position. 

1934 screen.visible_windows_to_write_positions[self] = write_position 

1935 

1936 def _copy_body( 

1937 self, 

1938 ui_content: UIContent, 

1939 new_screen: Screen, 

1940 write_position: WritePosition, 

1941 move_x: int, 

1942 width: int, 

1943 vertical_scroll: int = 0, 

1944 horizontal_scroll: int = 0, 

1945 wrap_lines: bool = False, 

1946 highlight_lines: bool = False, 

1947 vertical_scroll_2: int = 0, 

1948 always_hide_cursor: bool = False, 

1949 has_focus: bool = False, 

1950 align: WindowAlign = WindowAlign.LEFT, 

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

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

1953 """ 

1954 Copy the UIContent into the output screen. 

1955 Return (visible_line_to_row_col, rowcol_to_yx) tuple. 

1956 

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

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

1959 """ 

1960 xpos = write_position.xpos + move_x 

1961 ypos = write_position.ypos 

1962 line_count = ui_content.line_count 

1963 new_buffer = new_screen.data_buffer 

1964 empty_char = _CHAR_CACHE["", ""] 

1965 

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

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

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

1969 

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

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

1972 

1973 def copy_line( 

1974 line: StyleAndTextTuples, 

1975 lineno: int, 

1976 x: int, 

1977 y: int, 

1978 is_input: bool = False, 

1979 ) -> tuple[int, int]: 

1980 """ 

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

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

1983 function before every line. 

1984 """ 

1985 if is_input: 

1986 current_rowcol_to_yx = rowcol_to_yx 

1987 else: 

1988 current_rowcol_to_yx = {} # Throwaway dictionary. 

1989 

1990 # Draw line prefix. 

1991 if is_input and get_line_prefix: 

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

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

1994 

1995 # Scroll horizontally. 

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

1997 if horizontal_scroll and is_input: 

1998 h_scroll = horizontal_scroll 

1999 line = explode_text_fragments(line) 

2000 while h_scroll > 0 and line: 

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

2002 skipped += 1 

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

2004 

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

2006 # this can end up being negative. 

2007 

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

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

2010 if align == WindowAlign.CENTER: 

2011 line_width = fragment_list_width(line) 

2012 if line_width < width: 

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

2014 elif align == WindowAlign.RIGHT: 

2015 line_width = fragment_list_width(line) 

2016 if line_width < width: 

2017 x += width - line_width 

2018 

2019 col = 0 

2020 wrap_count = 0 

2021 for style, text, *_ in line: 

2022 new_buffer_row = new_buffer[y + ypos] 

2023 

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

2025 # escape sequences.) 

2026 if "[ZeroWidthEscape]" in style: 

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

2028 continue 

2029 

2030 for c in text: 

2031 char = _CHAR_CACHE[c, style] 

2032 char_width = char.width 

2033 

2034 # Wrap when the line width is exceeded. 

2035 if wrap_lines and x + char_width > width: 

2036 visible_line_to_row_col[y + 1] = ( 

2037 lineno, 

2038 visible_line_to_row_col[y][1] + x, 

2039 ) 

2040 y += 1 

2041 wrap_count += 1 

2042 x = 0 

2043 

2044 # Insert line prefix (continuation prompt). 

2045 if is_input and get_line_prefix: 

2046 prompt = to_formatted_text( 

2047 get_line_prefix(lineno, wrap_count) 

2048 ) 

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

2050 

2051 new_buffer_row = new_buffer[y + ypos] 

2052 

2053 if y >= write_position.height: 

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

2055 

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

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

2058 new_buffer_row[x + xpos] = char 

2059 

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

2061 # to erase the neighbours positions in the screen. 

2062 # (The empty string if different from everything, 

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

2064 if char_width > 1: 

2065 for i in range(1, char_width): 

2066 new_buffer_row[x + xpos + i] = empty_char 

2067 

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

2069 # probably part of a decomposed unicode character. 

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

2071 # Merge it in the previous cell. 

2072 elif char_width == 0: 

2073 # Handle all character widths. If the previous 

2074 # character is a multiwidth character, then 

2075 # merge it two positions back. 

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

2077 if ( 

2078 x - pw >= 0 

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

2080 ): 

2081 prev_char = new_buffer_row[x + xpos - pw] 

2082 char2 = _CHAR_CACHE[ 

2083 prev_char.char + c, prev_char.style 

2084 ] 

2085 new_buffer_row[x + xpos - pw] = char2 

2086 

2087 # Keep track of write position for each character. 

2088 current_rowcol_to_yx[lineno, col + skipped] = ( 

2089 y + ypos, 

2090 x + xpos, 

2091 ) 

2092 

2093 col += 1 

2094 x += char_width 

2095 return x, y 

2096 

2097 # Copy content. 

2098 def copy() -> int: 

2099 y = -vertical_scroll_2 

2100 lineno = vertical_scroll 

2101 

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

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

2104 line = ui_content.get_line(lineno) 

2105 

2106 visible_line_to_row_col[y] = (lineno, horizontal_scroll) 

2107 

2108 # Copy margin and actual line. 

2109 x = 0 

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

2111 

2112 lineno += 1 

2113 y += 1 

2114 return y 

2115 

2116 copy() 

2117 

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

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

2120 try: 

2121 y, x = rowcol_to_yx[row, col] 

2122 except KeyError: 

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

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

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

2126 

2127 # raise ValueError( 

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

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

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

2131 else: 

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

2133 

2134 # Set cursor and menu positions. 

2135 if ui_content.cursor_position: 

2136 screen_cursor_position = cursor_pos_to_screen_pos( 

2137 ui_content.cursor_position.y, ui_content.cursor_position.x 

2138 ) 

2139 

2140 if has_focus: 

2141 new_screen.set_cursor_position(self, screen_cursor_position) 

2142 

2143 if always_hide_cursor: 

2144 new_screen.show_cursor = False 

2145 else: 

2146 new_screen.show_cursor = ui_content.show_cursor 

2147 

2148 self._highlight_digraph(new_screen) 

2149 

2150 if highlight_lines: 

2151 self._highlight_cursorlines( 

2152 new_screen, 

2153 screen_cursor_position, 

2154 xpos, 

2155 ypos, 

2156 width, 

2157 write_position.height, 

2158 ) 

2159 

2160 # Draw input characters from the input processor queue. 

2161 if has_focus and ui_content.cursor_position: 

2162 self._show_key_processor_key_buffer(new_screen) 

2163 

2164 # Set menu position. 

2165 if ui_content.menu_position: 

2166 new_screen.set_menu_position( 

2167 self, 

2168 cursor_pos_to_screen_pos( 

2169 ui_content.menu_position.y, ui_content.menu_position.x 

2170 ), 

2171 ) 

2172 

2173 # Update output screen height. 

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

2175 

2176 return visible_line_to_row_col, rowcol_to_yx 

2177 

2178 def _fill_bg( 

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

2180 ) -> None: 

2181 """ 

2182 Erase/fill the background. 

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

2184 """ 

2185 char: str | None 

2186 if callable(self.char): 

2187 char = self.char() 

2188 else: 

2189 char = self.char 

2190 

2191 if erase_bg or char: 

2192 wp = write_position 

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

2194 

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

2196 row = screen.data_buffer[y] 

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

2198 row[x] = char_obj 

2199 

2200 def _apply_style( 

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

2202 ) -> None: 

2203 # Apply `self.style`. 

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

2205 

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

2207 

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

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

2210 wp = WritePosition( 

2211 write_position.xpos, 

2212 write_position.ypos + write_position.height - 1, 

2213 write_position.width, 

2214 1, 

2215 ) 

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

2217 

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

2219 """ 

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

2221 cursor. 

2222 """ 

2223 digraph_char = self._get_digraph_char() 

2224 if digraph_char: 

2225 cpos = new_screen.get_cursor_position(self) 

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

2227 digraph_char, "class:digraph" 

2228 ] 

2229 

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

2231 """ 

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

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

2234 is meaningful to be displayed. 

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

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

2237 """ 

2238 app = get_app() 

2239 key_buffer = app.key_processor.key_buffer 

2240 

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

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

2243 # sequence.) 

2244 data = key_buffer[-1].data 

2245 

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

2247 if get_cwidth(data) == 1: 

2248 cpos = new_screen.get_cursor_position(self) 

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

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

2251 ] 

2252 

2253 def _highlight_cursorlines( 

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

2255 ) -> None: 

2256 """ 

2257 Highlight cursor row/column. 

2258 """ 

2259 cursor_line_style = " class:cursor-line " 

2260 cursor_column_style = " class:cursor-column " 

2261 

2262 data_buffer = new_screen.data_buffer 

2263 

2264 # Highlight cursor line. 

2265 if self.cursorline(): 

2266 row = data_buffer[cpos.y] 

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

2268 original_char = row[x] 

2269 row[x] = _CHAR_CACHE[ 

2270 original_char.char, original_char.style + cursor_line_style 

2271 ] 

2272 

2273 # Highlight cursor column. 

2274 if self.cursorcolumn(): 

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

2276 row = data_buffer[y2] 

2277 original_char = row[cpos.x] 

2278 row[cpos.x] = _CHAR_CACHE[ 

2279 original_char.char, original_char.style + cursor_column_style 

2280 ] 

2281 

2282 # Highlight color columns 

2283 colorcolumns = self.colorcolumns 

2284 if callable(colorcolumns): 

2285 colorcolumns = colorcolumns() 

2286 

2287 for cc in colorcolumns: 

2288 assert isinstance(cc, ColorColumn) 

2289 column = cc.position 

2290 

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

2292 color_column_style = " " + cc.style 

2293 

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

2295 row = data_buffer[y2] 

2296 original_char = row[column + x] 

2297 row[column + x] = _CHAR_CACHE[ 

2298 original_char.char, original_char.style + color_column_style 

2299 ] 

2300 

2301 def _copy_margin( 

2302 self, 

2303 margin_content: UIContent, 

2304 new_screen: Screen, 

2305 write_position: WritePosition, 

2306 move_x: int, 

2307 width: int, 

2308 ) -> None: 

2309 """ 

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

2311 """ 

2312 xpos = write_position.xpos + move_x 

2313 ypos = write_position.ypos 

2314 

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

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

2317 

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

2319 """ 

2320 Scroll body. Ensure that the cursor is visible. 

2321 """ 

2322 if self.wrap_lines(): 

2323 func = self._scroll_when_linewrapping 

2324 else: 

2325 func = self._scroll_without_linewrapping 

2326 

2327 func(ui_content, width, height) 

2328 

2329 def _scroll_when_linewrapping( 

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

2331 ) -> None: 

2332 """ 

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

2334 the requested scroll offset. 

2335 

2336 Set `self.horizontal_scroll/vertical_scroll`. 

2337 """ 

2338 scroll_offsets_bottom = self.scroll_offsets.bottom 

2339 scroll_offsets_top = self.scroll_offsets.top 

2340 

2341 # We don't have horizontal scrolling. 

2342 self.horizontal_scroll = 0 

2343 

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

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

2346 

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

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

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

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

2351 # nothing. 

2352 if width <= 0: 

2353 self.vertical_scroll = ui_content.cursor_position.y 

2354 self.vertical_scroll_2 = 0 

2355 return 

2356 

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

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

2359 # the scroll offsets into account for this.) 

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

2361 # scroll to this line. 

2362 line_height = get_line_height(ui_content.cursor_position.y) 

2363 if line_height > height - scroll_offsets_top: 

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

2365 # line prefixes). 

2366 text_before_height = ui_content.get_height_for_line( 

2367 ui_content.cursor_position.y, 

2368 width, 

2369 self.get_line_prefix, 

2370 slice_stop=ui_content.cursor_position.x, 

2371 ) 

2372 

2373 # Adjust scroll offset. 

2374 self.vertical_scroll = ui_content.cursor_position.y 

2375 self.vertical_scroll_2 = min( 

2376 text_before_height - 1, # Keep the cursor visible. 

2377 line_height 

2378 - height, # Avoid blank lines at the bottom when scolling up again. 

2379 self.vertical_scroll_2, 

2380 ) 

2381 self.vertical_scroll_2 = max( 

2382 0, text_before_height - height, self.vertical_scroll_2 

2383 ) 

2384 return 

2385 else: 

2386 self.vertical_scroll_2 = 0 

2387 

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

2389 def get_min_vertical_scroll() -> int: 

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

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

2392 used_height = 0 

2393 prev_lineno = ui_content.cursor_position.y 

2394 

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

2396 used_height += get_line_height(lineno) 

2397 

2398 if used_height > height - scroll_offsets_bottom: 

2399 return prev_lineno 

2400 else: 

2401 prev_lineno = lineno 

2402 return 0 

2403 

2404 def get_max_vertical_scroll() -> int: 

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

2406 prev_lineno = ui_content.cursor_position.y 

2407 used_height = 0 

2408 

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

2410 used_height += get_line_height(lineno) 

2411 

2412 if used_height > scroll_offsets_top: 

2413 return prev_lineno 

2414 else: 

2415 prev_lineno = lineno 

2416 return prev_lineno 

2417 

2418 def get_topmost_visible() -> int: 

2419 """ 

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

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

2422 `allow_scroll_beyond_bottom` is false. 

2423 """ 

2424 prev_lineno = ui_content.line_count - 1 

2425 used_height = 0 

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

2427 used_height += get_line_height(lineno) 

2428 if used_height > height: 

2429 return prev_lineno 

2430 else: 

2431 prev_lineno = lineno 

2432 return prev_lineno 

2433 

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

2435 # cursor is visible. 

2436 topmost_visible = get_topmost_visible() 

2437 

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

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

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

2441 self.vertical_scroll = max( 

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

2443 ) 

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

2445 

2446 # Disallow scrolling beyond bottom? 

2447 if not self.allow_scroll_beyond_bottom(): 

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

2449 

2450 def _scroll_without_linewrapping( 

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

2452 ) -> None: 

2453 """ 

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

2455 the requested scroll offset. 

2456 

2457 Set `self.horizontal_scroll/vertical_scroll`. 

2458 """ 

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

2460 

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

2462 # a single line. 

2463 self.vertical_scroll_2 = 0 

2464 

2465 if ui_content.line_count == 0: 

2466 self.vertical_scroll = 0 

2467 self.horizontal_scroll = 0 

2468 return 

2469 else: 

2470 current_line_text = fragment_list_to_text( 

2471 ui_content.get_line(cursor_position.y) 

2472 ) 

2473 

2474 def do_scroll( 

2475 current_scroll: int, 

2476 scroll_offset_start: int, 

2477 scroll_offset_end: int, 

2478 cursor_pos: int, 

2479 window_size: int, 

2480 content_size: int, 

2481 ) -> int: 

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

2483 # Calculate the scroll offset to apply. 

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

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

2486 scroll_offset_start = int( 

2487 min(scroll_offset_start, window_size / 2, cursor_pos) 

2488 ) 

2489 scroll_offset_end = int( 

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

2491 ) 

2492 

2493 # Prevent negative scroll offsets. 

2494 if current_scroll < 0: 

2495 current_scroll = 0 

2496 

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

2498 if ( 

2499 not self.allow_scroll_beyond_bottom() 

2500 and current_scroll > content_size - window_size 

2501 ): 

2502 current_scroll = max(0, content_size - window_size) 

2503 

2504 # Scroll up if cursor is before visible part. 

2505 if current_scroll > cursor_pos - scroll_offset_start: 

2506 current_scroll = max(0, cursor_pos - scroll_offset_start) 

2507 

2508 # Scroll down if cursor is after visible part. 

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

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

2511 

2512 return current_scroll 

2513 

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

2515 if self.get_vertical_scroll: 

2516 self.vertical_scroll = self.get_vertical_scroll(self) 

2517 assert isinstance(self.vertical_scroll, int) 

2518 if self.get_horizontal_scroll: 

2519 self.horizontal_scroll = self.get_horizontal_scroll(self) 

2520 assert isinstance(self.horizontal_scroll, int) 

2521 

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

2523 # remains visible. 

2524 offsets = self.scroll_offsets 

2525 

2526 self.vertical_scroll = do_scroll( 

2527 current_scroll=self.vertical_scroll, 

2528 scroll_offset_start=offsets.top, 

2529 scroll_offset_end=offsets.bottom, 

2530 cursor_pos=ui_content.cursor_position.y, 

2531 window_size=height, 

2532 content_size=ui_content.line_count, 

2533 ) 

2534 

2535 if self.get_line_prefix: 

2536 current_line_prefix_width = fragment_list_width( 

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

2538 ) 

2539 else: 

2540 current_line_prefix_width = 0 

2541 

2542 self.horizontal_scroll = do_scroll( 

2543 current_scroll=self.horizontal_scroll, 

2544 scroll_offset_start=offsets.left, 

2545 scroll_offset_end=offsets.right, 

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

2547 window_size=width - current_line_prefix_width, 

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

2549 # all the lines is too expensive. 

2550 content_size=max( 

2551 get_cwidth(current_line_text), self.horizontal_scroll + width 

2552 ), 

2553 ) 

2554 

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

2556 """ 

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

2558 particular event. 

2559 

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

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

2562 """ 

2563 if mouse_event.event_type == MouseEventType.SCROLL_DOWN: 

2564 self._scroll_down() 

2565 return None 

2566 elif mouse_event.event_type == MouseEventType.SCROLL_UP: 

2567 self._scroll_up() 

2568 return None 

2569 

2570 return NotImplemented 

2571 

2572 def _scroll_down(self) -> None: 

2573 "Scroll window down." 

2574 info = self.render_info 

2575 

2576 if info is None: 

2577 return 

2578 

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

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

2581 self.content.move_cursor_down() 

2582 

2583 self.vertical_scroll += 1 

2584 

2585 def _scroll_up(self) -> None: 

2586 "Scroll window up." 

2587 info = self.render_info 

2588 

2589 if info is None: 

2590 return 

2591 

2592 if info.vertical_scroll > 0: 

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

2594 if ( 

2595 info.cursor_position.y 

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

2597 ): 

2598 self.content.move_cursor_up() 

2599 

2600 self.vertical_scroll -= 1 

2601 

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

2603 return self.content.get_key_bindings() 

2604 

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

2606 return [] 

2607 

2608 

2609class ConditionalContainer(Container): 

2610 """ 

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

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

2613 displayed or not. 

2614 

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

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

2617 """ 

2618 

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

2620 self.content = to_container(content) 

2621 self.filter = to_filter(filter) 

2622 

2623 def __repr__(self) -> str: 

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

2625 

2626 def reset(self) -> None: 

2627 self.content.reset() 

2628 

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

2630 if self.filter(): 

2631 return self.content.preferred_width(max_available_width) 

2632 else: 

2633 return Dimension.zero() 

2634 

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

2636 if self.filter(): 

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

2638 else: 

2639 return Dimension.zero() 

2640 

2641 def write_to_screen( 

2642 self, 

2643 screen: Screen, 

2644 mouse_handlers: MouseHandlers, 

2645 write_position: WritePosition, 

2646 parent_style: str, 

2647 erase_bg: bool, 

2648 z_index: int | None, 

2649 ) -> None: 

2650 if self.filter(): 

2651 return self.content.write_to_screen( 

2652 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index 

2653 ) 

2654 

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

2656 return [self.content] 

2657 

2658 

2659class DynamicContainer(Container): 

2660 """ 

2661 Container class that dynamically returns any Container. 

2662 

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

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

2665 """ 

2666 

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

2668 self.get_container = get_container 

2669 

2670 def _get_container(self) -> Container: 

2671 """ 

2672 Return the current container object. 

2673 

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

2675 widget with a ``__pt_container__`` method. 

2676 """ 

2677 obj = self.get_container() 

2678 return to_container(obj) 

2679 

2680 def reset(self) -> None: 

2681 self._get_container().reset() 

2682 

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

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

2685 

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

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

2688 

2689 def write_to_screen( 

2690 self, 

2691 screen: Screen, 

2692 mouse_handlers: MouseHandlers, 

2693 write_position: WritePosition, 

2694 parent_style: str, 

2695 erase_bg: bool, 

2696 z_index: int | None, 

2697 ) -> None: 

2698 self._get_container().write_to_screen( 

2699 screen, mouse_handlers, write_position, parent_style, erase_bg, z_index 

2700 ) 

2701 

2702 def is_modal(self) -> bool: 

2703 return False 

2704 

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

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

2707 # container. 

2708 return None 

2709 

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

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

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

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

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

2715 return [self._get_container()] 

2716 

2717 

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

2719 """ 

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

2721 """ 

2722 if isinstance(container, Container): 

2723 return container 

2724 elif hasattr(container, "__pt_container__"): 

2725 return to_container(container.__pt_container__()) 

2726 else: 

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

2728 

2729 

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

2731 """ 

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

2733 """ 

2734 if isinstance(container, Window): 

2735 return container 

2736 elif hasattr(container, "__pt_container__"): 

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

2738 else: 

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

2740 

2741 

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

2743 """ 

2744 Checks whether the given value is a container object 

2745 (for use in assert statements). 

2746 """ 

2747 if isinstance(value, Container): 

2748 return True 

2749 if hasattr(value, "__pt_container__"): 

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

2751 return False