12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738 |
- """
- Container for the layout.
- (Containers can contain other containers or user interface controls.)
- """
- from __future__ import annotations
- from abc import ABCMeta, abstractmethod
- from enum import Enum
- from functools import partial
- from typing import TYPE_CHECKING, Callable, Sequence, Union, cast
- from prompt_toolkit.application.current import get_app
- from prompt_toolkit.cache import SimpleCache
- from prompt_toolkit.data_structures import Point
- from prompt_toolkit.filters import (
- FilterOrBool,
- emacs_insert_mode,
- to_filter,
- vi_insert_mode,
- )
- from prompt_toolkit.formatted_text import (
- AnyFormattedText,
- StyleAndTextTuples,
- to_formatted_text,
- )
- from prompt_toolkit.formatted_text.utils import (
- fragment_list_to_text,
- fragment_list_width,
- )
- from prompt_toolkit.key_binding import KeyBindingsBase
- from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
- from prompt_toolkit.utils import get_cwidth, take_using_weights, to_int, to_str
- from .controls import (
- DummyControl,
- FormattedTextControl,
- GetLinePrefixCallable,
- UIContent,
- UIControl,
- )
- from .dimension import (
- AnyDimension,
- Dimension,
- max_layout_dimensions,
- sum_layout_dimensions,
- to_dimension,
- )
- from .margins import Margin
- from .mouse_handlers import MouseHandlers
- from .screen import _CHAR_CACHE, Screen, WritePosition
- from .utils import explode_text_fragments
- if TYPE_CHECKING:
- from typing_extensions import Protocol, TypeGuard
- from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
- __all__ = [
- "AnyContainer",
- "Container",
- "HorizontalAlign",
- "VerticalAlign",
- "HSplit",
- "VSplit",
- "FloatContainer",
- "Float",
- "WindowAlign",
- "Window",
- "WindowRenderInfo",
- "ConditionalContainer",
- "ScrollOffsets",
- "ColorColumn",
- "to_container",
- "to_window",
- "is_container",
- "DynamicContainer",
- ]
- class Container(metaclass=ABCMeta):
- """
- Base class for user interface layout.
- """
- @abstractmethod
- def reset(self) -> None:
- """
- Reset the state of this container and all the children.
- (E.g. reset scroll offsets, etc...)
- """
- @abstractmethod
- def preferred_width(self, max_available_width: int) -> Dimension:
- """
- Return a :class:`~prompt_toolkit.layout.Dimension` that represents the
- desired width for this container.
- """
- @abstractmethod
- def preferred_height(self, width: int, max_available_height: int) -> Dimension:
- """
- Return a :class:`~prompt_toolkit.layout.Dimension` that represents the
- desired height for this container.
- """
- @abstractmethod
- def write_to_screen(
- self,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- parent_style: str,
- erase_bg: bool,
- z_index: int | None,
- ) -> None:
- """
- Write the actual content to the screen.
- :param screen: :class:`~prompt_toolkit.layout.screen.Screen`
- :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`.
- :param parent_style: Style string to pass to the :class:`.Window`
- object. This will be applied to all content of the windows.
- :class:`.VSplit` and :class:`.HSplit` can use it to pass their
- style down to the windows that they contain.
- :param z_index: Used for propagating z_index from parent to child.
- """
- def is_modal(self) -> bool:
- """
- When this container is modal, key bindings from parent containers are
- not taken into account if a user control in this container is focused.
- """
- return False
- def get_key_bindings(self) -> KeyBindingsBase | None:
- """
- Returns a :class:`.KeyBindings` object. These bindings become active when any
- user control in this container has the focus, except if any containers
- between this container and the focused user control is modal.
- """
- return None
- @abstractmethod
- def get_children(self) -> list[Container]:
- """
- Return the list of child :class:`.Container` objects.
- """
- return []
- if TYPE_CHECKING:
- class MagicContainer(Protocol):
- """
- Any object that implements ``__pt_container__`` represents a container.
- """
- def __pt_container__(self) -> AnyContainer: ...
- AnyContainer = Union[Container, "MagicContainer"]
- def _window_too_small() -> Window:
- "Create a `Window` that displays the 'Window too small' text."
- return Window(
- FormattedTextControl(text=[("class:window-too-small", " Window too small... ")])
- )
- class VerticalAlign(Enum):
- "Alignment for `HSplit`."
- TOP = "TOP"
- CENTER = "CENTER"
- BOTTOM = "BOTTOM"
- JUSTIFY = "JUSTIFY"
- class HorizontalAlign(Enum):
- "Alignment for `VSplit`."
- LEFT = "LEFT"
- CENTER = "CENTER"
- RIGHT = "RIGHT"
- JUSTIFY = "JUSTIFY"
- class _Split(Container):
- """
- The common parts of `VSplit` and `HSplit`.
- """
- def __init__(
- self,
- children: Sequence[AnyContainer],
- window_too_small: Container | None = None,
- padding: AnyDimension = Dimension.exact(0),
- padding_char: str | None = None,
- padding_style: str = "",
- width: AnyDimension = None,
- height: AnyDimension = None,
- z_index: int | None = None,
- modal: bool = False,
- key_bindings: KeyBindingsBase | None = None,
- style: str | Callable[[], str] = "",
- ) -> None:
- self.children = [to_container(c) for c in children]
- self.window_too_small = window_too_small or _window_too_small()
- self.padding = padding
- self.padding_char = padding_char
- self.padding_style = padding_style
- self.width = width
- self.height = height
- self.z_index = z_index
- self.modal = modal
- self.key_bindings = key_bindings
- self.style = style
- def is_modal(self) -> bool:
- return self.modal
- def get_key_bindings(self) -> KeyBindingsBase | None:
- return self.key_bindings
- def get_children(self) -> list[Container]:
- return self.children
- class HSplit(_Split):
- """
- Several layouts, one stacked above/under the other. ::
- +--------------------+
- | |
- +--------------------+
- | |
- +--------------------+
- By default, this doesn't display a horizontal line between the children,
- but if this is something you need, then create a HSplit as follows::
- HSplit(children=[ ... ], padding_char='-',
- padding=1, padding_style='#ffff00')
- :param children: List of child :class:`.Container` objects.
- :param window_too_small: A :class:`.Container` object that is displayed if
- there is not enough space for all the children. By default, this is a
- "Window too small" message.
- :param align: `VerticalAlign` value.
- :param width: When given, use this width instead of looking at the children.
- :param height: When given, use this height instead of looking at the children.
- :param z_index: (int or None) When specified, this can be used to bring
- element in front of floating elements. `None` means: inherit from parent.
- :param style: A style string.
- :param modal: ``True`` or ``False``.
- :param key_bindings: ``None`` or a :class:`.KeyBindings` object.
- :param padding: (`Dimension` or int), size to be used for the padding.
- :param padding_char: Character to be used for filling in the padding.
- :param padding_style: Style to applied to the padding.
- """
- def __init__(
- self,
- children: Sequence[AnyContainer],
- window_too_small: Container | None = None,
- align: VerticalAlign = VerticalAlign.JUSTIFY,
- padding: AnyDimension = 0,
- padding_char: str | None = None,
- padding_style: str = "",
- width: AnyDimension = None,
- height: AnyDimension = None,
- z_index: int | None = None,
- modal: bool = False,
- key_bindings: KeyBindingsBase | None = None,
- style: str | Callable[[], str] = "",
- ) -> None:
- super().__init__(
- children=children,
- window_too_small=window_too_small,
- padding=padding,
- padding_char=padding_char,
- padding_style=padding_style,
- width=width,
- height=height,
- z_index=z_index,
- modal=modal,
- key_bindings=key_bindings,
- style=style,
- )
- self.align = align
- self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = (
- SimpleCache(maxsize=1)
- )
- self._remaining_space_window = Window() # Dummy window.
- def preferred_width(self, max_available_width: int) -> Dimension:
- if self.width is not None:
- return to_dimension(self.width)
- if self.children:
- dimensions = [c.preferred_width(max_available_width) for c in self.children]
- return max_layout_dimensions(dimensions)
- else:
- return Dimension()
- def preferred_height(self, width: int, max_available_height: int) -> Dimension:
- if self.height is not None:
- return to_dimension(self.height)
- dimensions = [
- c.preferred_height(width, max_available_height) for c in self._all_children
- ]
- return sum_layout_dimensions(dimensions)
- def reset(self) -> None:
- for c in self.children:
- c.reset()
- @property
- def _all_children(self) -> list[Container]:
- """
- List of child objects, including padding.
- """
- def get() -> list[Container]:
- result: list[Container] = []
- # Padding Top.
- if self.align in (VerticalAlign.CENTER, VerticalAlign.BOTTOM):
- result.append(Window(width=Dimension(preferred=0)))
- # The children with padding.
- for child in self.children:
- result.append(child)
- result.append(
- Window(
- height=self.padding,
- char=self.padding_char,
- style=self.padding_style,
- )
- )
- if result:
- result.pop()
- # Padding right.
- if self.align in (VerticalAlign.CENTER, VerticalAlign.TOP):
- result.append(Window(width=Dimension(preferred=0)))
- return result
- return self._children_cache.get(tuple(self.children), get)
- def write_to_screen(
- self,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- parent_style: str,
- erase_bg: bool,
- z_index: int | None,
- ) -> None:
- """
- Render the prompt to a `Screen` instance.
- :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
- to which the output has to be written.
- """
- sizes = self._divide_heights(write_position)
- style = parent_style + " " + to_str(self.style)
- z_index = z_index if self.z_index is None else self.z_index
- if sizes is None:
- self.window_too_small.write_to_screen(
- screen, mouse_handlers, write_position, style, erase_bg, z_index
- )
- else:
- #
- ypos = write_position.ypos
- xpos = write_position.xpos
- width = write_position.width
- # Draw child panes.
- for s, c in zip(sizes, self._all_children):
- c.write_to_screen(
- screen,
- mouse_handlers,
- WritePosition(xpos, ypos, width, s),
- style,
- erase_bg,
- z_index,
- )
- ypos += s
- # Fill in the remaining space. This happens when a child control
- # refuses to take more space and we don't have any padding. Adding a
- # dummy child control for this (in `self._all_children`) is not
- # desired, because in some situations, it would take more space, even
- # when it's not required. This is required to apply the styling.
- remaining_height = write_position.ypos + write_position.height - ypos
- if remaining_height > 0:
- self._remaining_space_window.write_to_screen(
- screen,
- mouse_handlers,
- WritePosition(xpos, ypos, width, remaining_height),
- style,
- erase_bg,
- z_index,
- )
- def _divide_heights(self, write_position: WritePosition) -> list[int] | None:
- """
- Return the heights for all rows.
- Or None when there is not enough space.
- """
- if not self.children:
- return []
- width = write_position.width
- height = write_position.height
- # Calculate heights.
- dimensions = [c.preferred_height(width, height) for c in self._all_children]
- # Sum dimensions
- sum_dimensions = sum_layout_dimensions(dimensions)
- # If there is not enough space for both.
- # Don't do anything.
- if sum_dimensions.min > height:
- return None
- # Find optimal sizes. (Start with minimal size, increase until we cover
- # the whole height.)
- sizes = [d.min for d in dimensions]
- child_generator = take_using_weights(
- items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
- )
- i = next(child_generator)
- # Increase until we meet at least the 'preferred' size.
- preferred_stop = min(height, sum_dimensions.preferred)
- preferred_dimensions = [d.preferred for d in dimensions]
- while sum(sizes) < preferred_stop:
- if sizes[i] < preferred_dimensions[i]:
- sizes[i] += 1
- i = next(child_generator)
- # Increase until we use all the available space. (or until "max")
- if not get_app().is_done:
- max_stop = min(height, sum_dimensions.max)
- max_dimensions = [d.max for d in dimensions]
- while sum(sizes) < max_stop:
- if sizes[i] < max_dimensions[i]:
- sizes[i] += 1
- i = next(child_generator)
- return sizes
- class VSplit(_Split):
- """
- Several layouts, one stacked left/right of the other. ::
- +---------+----------+
- | | |
- | | |
- +---------+----------+
- By default, this doesn't display a vertical line between the children, but
- if this is something you need, then create a HSplit as follows::
- VSplit(children=[ ... ], padding_char='|',
- padding=1, padding_style='#ffff00')
- :param children: List of child :class:`.Container` objects.
- :param window_too_small: A :class:`.Container` object that is displayed if
- there is not enough space for all the children. By default, this is a
- "Window too small" message.
- :param align: `HorizontalAlign` value.
- :param width: When given, use this width instead of looking at the children.
- :param height: When given, use this height instead of looking at the children.
- :param z_index: (int or None) When specified, this can be used to bring
- element in front of floating elements. `None` means: inherit from parent.
- :param style: A style string.
- :param modal: ``True`` or ``False``.
- :param key_bindings: ``None`` or a :class:`.KeyBindings` object.
- :param padding: (`Dimension` or int), size to be used for the padding.
- :param padding_char: Character to be used for filling in the padding.
- :param padding_style: Style to applied to the padding.
- """
- def __init__(
- self,
- children: Sequence[AnyContainer],
- window_too_small: Container | None = None,
- align: HorizontalAlign = HorizontalAlign.JUSTIFY,
- padding: AnyDimension = 0,
- padding_char: str | None = None,
- padding_style: str = "",
- width: AnyDimension = None,
- height: AnyDimension = None,
- z_index: int | None = None,
- modal: bool = False,
- key_bindings: KeyBindingsBase | None = None,
- style: str | Callable[[], str] = "",
- ) -> None:
- super().__init__(
- children=children,
- window_too_small=window_too_small,
- padding=padding,
- padding_char=padding_char,
- padding_style=padding_style,
- width=width,
- height=height,
- z_index=z_index,
- modal=modal,
- key_bindings=key_bindings,
- style=style,
- )
- self.align = align
- self._children_cache: SimpleCache[tuple[Container, ...], list[Container]] = (
- SimpleCache(maxsize=1)
- )
- self._remaining_space_window = Window() # Dummy window.
- def preferred_width(self, max_available_width: int) -> Dimension:
- if self.width is not None:
- return to_dimension(self.width)
- dimensions = [
- c.preferred_width(max_available_width) for c in self._all_children
- ]
- return sum_layout_dimensions(dimensions)
- def preferred_height(self, width: int, max_available_height: int) -> Dimension:
- if self.height is not None:
- return to_dimension(self.height)
- # At the point where we want to calculate the heights, the widths have
- # already been decided. So we can trust `width` to be the actual
- # `width` that's going to be used for the rendering. So,
- # `divide_widths` is supposed to use all of the available width.
- # Using only the `preferred` width caused a bug where the reported
- # height was more than required. (we had a `BufferControl` which did
- # wrap lines because of the smaller width returned by `_divide_widths`.
- sizes = self._divide_widths(width)
- children = self._all_children
- if sizes is None:
- return Dimension()
- else:
- dimensions = [
- c.preferred_height(s, max_available_height)
- for s, c in zip(sizes, children)
- ]
- return max_layout_dimensions(dimensions)
- def reset(self) -> None:
- for c in self.children:
- c.reset()
- @property
- def _all_children(self) -> list[Container]:
- """
- List of child objects, including padding.
- """
- def get() -> list[Container]:
- result: list[Container] = []
- # Padding left.
- if self.align in (HorizontalAlign.CENTER, HorizontalAlign.RIGHT):
- result.append(Window(width=Dimension(preferred=0)))
- # The children with padding.
- for child in self.children:
- result.append(child)
- result.append(
- Window(
- width=self.padding,
- char=self.padding_char,
- style=self.padding_style,
- )
- )
- if result:
- result.pop()
- # Padding right.
- if self.align in (HorizontalAlign.CENTER, HorizontalAlign.LEFT):
- result.append(Window(width=Dimension(preferred=0)))
- return result
- return self._children_cache.get(tuple(self.children), get)
- def _divide_widths(self, width: int) -> list[int] | None:
- """
- Return the widths for all columns.
- Or None when there is not enough space.
- """
- children = self._all_children
- if not children:
- return []
- # Calculate widths.
- dimensions = [c.preferred_width(width) for c in children]
- preferred_dimensions = [d.preferred for d in dimensions]
- # Sum dimensions
- sum_dimensions = sum_layout_dimensions(dimensions)
- # If there is not enough space for both.
- # Don't do anything.
- if sum_dimensions.min > width:
- return None
- # Find optimal sizes. (Start with minimal size, increase until we cover
- # the whole width.)
- sizes = [d.min for d in dimensions]
- child_generator = take_using_weights(
- items=list(range(len(dimensions))), weights=[d.weight for d in dimensions]
- )
- i = next(child_generator)
- # Increase until we meet at least the 'preferred' size.
- preferred_stop = min(width, sum_dimensions.preferred)
- while sum(sizes) < preferred_stop:
- if sizes[i] < preferred_dimensions[i]:
- sizes[i] += 1
- i = next(child_generator)
- # Increase until we use all the available space.
- max_dimensions = [d.max for d in dimensions]
- max_stop = min(width, sum_dimensions.max)
- while sum(sizes) < max_stop:
- if sizes[i] < max_dimensions[i]:
- sizes[i] += 1
- i = next(child_generator)
- return sizes
- def write_to_screen(
- self,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- parent_style: str,
- erase_bg: bool,
- z_index: int | None,
- ) -> None:
- """
- Render the prompt to a `Screen` instance.
- :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
- to which the output has to be written.
- """
- if not self.children:
- return
- children = self._all_children
- sizes = self._divide_widths(write_position.width)
- style = parent_style + " " + to_str(self.style)
- z_index = z_index if self.z_index is None else self.z_index
- # If there is not enough space.
- if sizes is None:
- self.window_too_small.write_to_screen(
- screen, mouse_handlers, write_position, style, erase_bg, z_index
- )
- return
- # Calculate heights, take the largest possible, but not larger than
- # write_position.height.
- heights = [
- child.preferred_height(width, write_position.height).preferred
- for width, child in zip(sizes, children)
- ]
- height = max(write_position.height, min(write_position.height, max(heights)))
- #
- ypos = write_position.ypos
- xpos = write_position.xpos
- # Draw all child panes.
- for s, c in zip(sizes, children):
- c.write_to_screen(
- screen,
- mouse_handlers,
- WritePosition(xpos, ypos, s, height),
- style,
- erase_bg,
- z_index,
- )
- xpos += s
- # Fill in the remaining space. This happens when a child control
- # refuses to take more space and we don't have any padding. Adding a
- # dummy child control for this (in `self._all_children`) is not
- # desired, because in some situations, it would take more space, even
- # when it's not required. This is required to apply the styling.
- remaining_width = write_position.xpos + write_position.width - xpos
- if remaining_width > 0:
- self._remaining_space_window.write_to_screen(
- screen,
- mouse_handlers,
- WritePosition(xpos, ypos, remaining_width, height),
- style,
- erase_bg,
- z_index,
- )
- class FloatContainer(Container):
- """
- Container which can contain another container for the background, as well
- as a list of floating containers on top of it.
- Example Usage::
- FloatContainer(content=Window(...),
- floats=[
- Float(xcursor=True,
- ycursor=True,
- content=CompletionsMenu(...))
- ])
- :param z_index: (int or None) When specified, this can be used to bring
- element in front of floating elements. `None` means: inherit from parent.
- This is the z_index for the whole `Float` container as a whole.
- """
- def __init__(
- self,
- content: AnyContainer,
- floats: list[Float],
- modal: bool = False,
- key_bindings: KeyBindingsBase | None = None,
- style: str | Callable[[], str] = "",
- z_index: int | None = None,
- ) -> None:
- self.content = to_container(content)
- self.floats = floats
- self.modal = modal
- self.key_bindings = key_bindings
- self.style = style
- self.z_index = z_index
- def reset(self) -> None:
- self.content.reset()
- for f in self.floats:
- f.content.reset()
- def preferred_width(self, max_available_width: int) -> Dimension:
- return self.content.preferred_width(max_available_width)
- def preferred_height(self, width: int, max_available_height: int) -> Dimension:
- """
- Return the preferred height of the float container.
- (We don't care about the height of the floats, they should always fit
- into the dimensions provided by the container.)
- """
- return self.content.preferred_height(width, max_available_height)
- def write_to_screen(
- self,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- parent_style: str,
- erase_bg: bool,
- z_index: int | None,
- ) -> None:
- style = parent_style + " " + to_str(self.style)
- z_index = z_index if self.z_index is None else self.z_index
- self.content.write_to_screen(
- screen, mouse_handlers, write_position, style, erase_bg, z_index
- )
- for number, fl in enumerate(self.floats):
- # z_index of a Float is computed by summing the z_index of the
- # container and the `Float`.
- new_z_index = (z_index or 0) + fl.z_index
- style = parent_style + " " + to_str(self.style)
- # If the float that we have here, is positioned relative to the
- # cursor position, but the Window that specifies the cursor
- # position is not drawn yet, because it's a Float itself, we have
- # to postpone this calculation. (This is a work-around, but good
- # enough for now.)
- postpone = fl.xcursor is not None or fl.ycursor is not None
- if postpone:
- new_z_index = (
- number + 10**8
- ) # Draw as late as possible, but keep the order.
- screen.draw_with_z_index(
- z_index=new_z_index,
- draw_func=partial(
- self._draw_float,
- fl,
- screen,
- mouse_handlers,
- write_position,
- style,
- erase_bg,
- new_z_index,
- ),
- )
- else:
- self._draw_float(
- fl,
- screen,
- mouse_handlers,
- write_position,
- style,
- erase_bg,
- new_z_index,
- )
- def _draw_float(
- self,
- fl: Float,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- style: str,
- erase_bg: bool,
- z_index: int | None,
- ) -> None:
- "Draw a single Float."
- # When a menu_position was given, use this instead of the cursor
- # position. (These cursor positions are absolute, translate again
- # relative to the write_position.)
- # Note: This should be inside the for-loop, because one float could
- # set the cursor position to be used for the next one.
- cpos = screen.get_menu_position(
- fl.attach_to_window or get_app().layout.current_window
- )
- cursor_position = Point(
- x=cpos.x - write_position.xpos, y=cpos.y - write_position.ypos
- )
- fl_width = fl.get_width()
- fl_height = fl.get_height()
- width: int
- height: int
- xpos: int
- ypos: int
- # Left & width given.
- if fl.left is not None and fl_width is not None:
- xpos = fl.left
- width = fl_width
- # Left & right given -> calculate width.
- elif fl.left is not None and fl.right is not None:
- xpos = fl.left
- width = write_position.width - fl.left - fl.right
- # Width & right given -> calculate left.
- elif fl_width is not None and fl.right is not None:
- xpos = write_position.width - fl.right - fl_width
- width = fl_width
- # Near x position of cursor.
- elif fl.xcursor:
- if fl_width is None:
- width = fl.content.preferred_width(write_position.width).preferred
- width = min(write_position.width, width)
- else:
- width = fl_width
- xpos = cursor_position.x
- if xpos + width > write_position.width:
- xpos = max(0, write_position.width - width)
- # Only width given -> center horizontally.
- elif fl_width:
- xpos = int((write_position.width - fl_width) / 2)
- width = fl_width
- # Otherwise, take preferred width from float content.
- else:
- width = fl.content.preferred_width(write_position.width).preferred
- if fl.left is not None:
- xpos = fl.left
- elif fl.right is not None:
- xpos = max(0, write_position.width - width - fl.right)
- else: # Center horizontally.
- xpos = max(0, int((write_position.width - width) / 2))
- # Trim.
- width = min(width, write_position.width - xpos)
- # Top & height given.
- if fl.top is not None and fl_height is not None:
- ypos = fl.top
- height = fl_height
- # Top & bottom given -> calculate height.
- elif fl.top is not None and fl.bottom is not None:
- ypos = fl.top
- height = write_position.height - fl.top - fl.bottom
- # Height & bottom given -> calculate top.
- elif fl_height is not None and fl.bottom is not None:
- ypos = write_position.height - fl_height - fl.bottom
- height = fl_height
- # Near cursor.
- elif fl.ycursor:
- ypos = cursor_position.y + (0 if fl.allow_cover_cursor else 1)
- if fl_height is None:
- height = fl.content.preferred_height(
- width, write_position.height
- ).preferred
- else:
- height = fl_height
- # Reduce height if not enough space. (We can use the height
- # when the content requires it.)
- if height > write_position.height - ypos:
- if write_position.height - ypos + 1 >= ypos:
- # When the space below the cursor is more than
- # the space above, just reduce the height.
- height = write_position.height - ypos
- else:
- # Otherwise, fit the float above the cursor.
- height = min(height, cursor_position.y)
- ypos = cursor_position.y - height
- # Only height given -> center vertically.
- elif fl_height:
- ypos = int((write_position.height - fl_height) / 2)
- height = fl_height
- # Otherwise, take preferred height from content.
- else:
- height = fl.content.preferred_height(width, write_position.height).preferred
- if fl.top is not None:
- ypos = fl.top
- elif fl.bottom is not None:
- ypos = max(0, write_position.height - height - fl.bottom)
- else: # Center vertically.
- ypos = max(0, int((write_position.height - height) / 2))
- # Trim.
- height = min(height, write_position.height - ypos)
- # Write float.
- # (xpos and ypos can be negative: a float can be partially visible.)
- if height > 0 and width > 0:
- wp = WritePosition(
- xpos=xpos + write_position.xpos,
- ypos=ypos + write_position.ypos,
- width=width,
- height=height,
- )
- if not fl.hide_when_covering_content or self._area_is_empty(screen, wp):
- fl.content.write_to_screen(
- screen,
- mouse_handlers,
- wp,
- style,
- erase_bg=not fl.transparent(),
- z_index=z_index,
- )
- def _area_is_empty(self, screen: Screen, write_position: WritePosition) -> bool:
- """
- Return True when the area below the write position is still empty.
- (For floats that should not hide content underneath.)
- """
- wp = write_position
- for y in range(wp.ypos, wp.ypos + wp.height):
- if y in screen.data_buffer:
- row = screen.data_buffer[y]
- for x in range(wp.xpos, wp.xpos + wp.width):
- c = row[x]
- if c.char != " ":
- return False
- return True
- def is_modal(self) -> bool:
- return self.modal
- def get_key_bindings(self) -> KeyBindingsBase | None:
- return self.key_bindings
- def get_children(self) -> list[Container]:
- children = [self.content]
- children.extend(f.content for f in self.floats)
- return children
- class Float:
- """
- Float for use in a :class:`.FloatContainer`.
- Except for the `content` parameter, all other options are optional.
- :param content: :class:`.Container` instance.
- :param width: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
- :param height: :class:`.Dimension` or callable which returns a :class:`.Dimension`.
- :param left: Distance to the left edge of the :class:`.FloatContainer`.
- :param right: Distance to the right edge of the :class:`.FloatContainer`.
- :param top: Distance to the top of the :class:`.FloatContainer`.
- :param bottom: Distance to the bottom of the :class:`.FloatContainer`.
- :param attach_to_window: Attach to the cursor from this window, instead of
- the current window.
- :param hide_when_covering_content: Hide the float when it covers content underneath.
- :param allow_cover_cursor: When `False`, make sure to display the float
- below the cursor. Not on top of the indicated position.
- :param z_index: Z-index position. For a Float, this needs to be at least
- one. It is relative to the z_index of the parent container.
- :param transparent: :class:`.Filter` indicating whether this float needs to be
- drawn transparently.
- """
- def __init__(
- self,
- content: AnyContainer,
- top: int | None = None,
- right: int | None = None,
- bottom: int | None = None,
- left: int | None = None,
- width: int | Callable[[], int] | None = None,
- height: int | Callable[[], int] | None = None,
- xcursor: bool = False,
- ycursor: bool = False,
- attach_to_window: AnyContainer | None = None,
- hide_when_covering_content: bool = False,
- allow_cover_cursor: bool = False,
- z_index: int = 1,
- transparent: bool = False,
- ) -> None:
- assert z_index >= 1
- self.left = left
- self.right = right
- self.top = top
- self.bottom = bottom
- self.width = width
- self.height = height
- self.xcursor = xcursor
- self.ycursor = ycursor
- self.attach_to_window = (
- to_window(attach_to_window) if attach_to_window else None
- )
- self.content = to_container(content)
- self.hide_when_covering_content = hide_when_covering_content
- self.allow_cover_cursor = allow_cover_cursor
- self.z_index = z_index
- self.transparent = to_filter(transparent)
- def get_width(self) -> int | None:
- if callable(self.width):
- return self.width()
- return self.width
- def get_height(self) -> int | None:
- if callable(self.height):
- return self.height()
- return self.height
- def __repr__(self) -> str:
- return f"Float(content={self.content!r})"
- class WindowRenderInfo:
- """
- Render information for the last render time of this control.
- It stores mapping information between the input buffers (in case of a
- :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual
- render position on the output screen.
- (Could be used for implementation of the Vi 'H' and 'L' key bindings as
- well as implementing mouse support.)
- :param ui_content: The original :class:`.UIContent` instance that contains
- the whole input, without clipping. (ui_content)
- :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance.
- :param vertical_scroll: The vertical scroll of the :class:`.Window` instance.
- :param window_width: The width of the window that displays the content,
- without the margins.
- :param window_height: The height of the window that displays the content.
- :param configured_scroll_offsets: The scroll offsets as configured for the
- :class:`Window` instance.
- :param visible_line_to_row_col: Mapping that maps the row numbers on the
- displayed screen (starting from zero for the first visible line) to
- (row, col) tuples pointing to the row and column of the :class:`.UIContent`.
- :param rowcol_to_yx: Mapping that maps (row, column) tuples representing
- coordinates of the :class:`UIContent` to (y, x) absolute coordinates at
- the rendered screen.
- """
- def __init__(
- self,
- window: Window,
- ui_content: UIContent,
- horizontal_scroll: int,
- vertical_scroll: int,
- window_width: int,
- window_height: int,
- configured_scroll_offsets: ScrollOffsets,
- visible_line_to_row_col: dict[int, tuple[int, int]],
- rowcol_to_yx: dict[tuple[int, int], tuple[int, int]],
- x_offset: int,
- y_offset: int,
- wrap_lines: bool,
- ) -> None:
- self.window = window
- self.ui_content = ui_content
- self.vertical_scroll = vertical_scroll
- self.window_width = window_width # Width without margins.
- self.window_height = window_height
- self.configured_scroll_offsets = configured_scroll_offsets
- self.visible_line_to_row_col = visible_line_to_row_col
- self.wrap_lines = wrap_lines
- self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x
- # screen coordinates.
- self._x_offset = x_offset
- self._y_offset = y_offset
- @property
- def visible_line_to_input_line(self) -> dict[int, int]:
- return {
- visible_line: rowcol[0]
- for visible_line, rowcol in self.visible_line_to_row_col.items()
- }
- @property
- def cursor_position(self) -> Point:
- """
- Return the cursor position coordinates, relative to the left/top corner
- of the rendered screen.
- """
- cpos = self.ui_content.cursor_position
- try:
- y, x = self._rowcol_to_yx[cpos.y, cpos.x]
- except KeyError:
- # For `DummyControl` for instance, the content can be empty, and so
- # will `_rowcol_to_yx` be. Return 0/0 by default.
- return Point(x=0, y=0)
- else:
- return Point(x=x - self._x_offset, y=y - self._y_offset)
- @property
- def applied_scroll_offsets(self) -> ScrollOffsets:
- """
- Return a :class:`.ScrollOffsets` instance that indicates the actual
- offset. This can be less than or equal to what's configured. E.g, when
- the cursor is completely at the top, the top offset will be zero rather
- than what's configured.
- """
- if self.displayed_lines[0] == 0:
- top = 0
- else:
- # Get row where the cursor is displayed.
- y = self.input_line_to_visible_line[self.ui_content.cursor_position.y]
- top = min(y, self.configured_scroll_offsets.top)
- return ScrollOffsets(
- top=top,
- bottom=min(
- self.ui_content.line_count - self.displayed_lines[-1] - 1,
- self.configured_scroll_offsets.bottom,
- ),
- # For left/right, it probably doesn't make sense to return something.
- # (We would have to calculate the widths of all the lines and keep
- # double width characters in mind.)
- left=0,
- right=0,
- )
- @property
- def displayed_lines(self) -> list[int]:
- """
- List of all the visible rows. (Line numbers of the input buffer.)
- The last line may not be entirely visible.
- """
- return sorted(row for row, col in self.visible_line_to_row_col.values())
- @property
- def input_line_to_visible_line(self) -> dict[int, int]:
- """
- Return the dictionary mapping the line numbers of the input buffer to
- the lines of the screen. When a line spans several rows at the screen,
- the first row appears in the dictionary.
- """
- result: dict[int, int] = {}
- for k, v in self.visible_line_to_input_line.items():
- if v in result:
- result[v] = min(result[v], k)
- else:
- result[v] = k
- return result
- def first_visible_line(self, after_scroll_offset: bool = False) -> int:
- """
- Return the line number (0 based) of the input document that corresponds
- with the first visible line.
- """
- if after_scroll_offset:
- return self.displayed_lines[self.applied_scroll_offsets.top]
- else:
- return self.displayed_lines[0]
- def last_visible_line(self, before_scroll_offset: bool = False) -> int:
- """
- Like `first_visible_line`, but for the last visible line.
- """
- if before_scroll_offset:
- return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom]
- else:
- return self.displayed_lines[-1]
- def center_visible_line(
- self, before_scroll_offset: bool = False, after_scroll_offset: bool = False
- ) -> int:
- """
- Like `first_visible_line`, but for the center visible line.
- """
- return (
- self.first_visible_line(after_scroll_offset)
- + (
- self.last_visible_line(before_scroll_offset)
- - self.first_visible_line(after_scroll_offset)
- )
- // 2
- )
- @property
- def content_height(self) -> int:
- """
- The full height of the user control.
- """
- return self.ui_content.line_count
- @property
- def full_height_visible(self) -> bool:
- """
- True when the full height is visible (There is no vertical scroll.)
- """
- return (
- self.vertical_scroll == 0
- and self.last_visible_line() == self.content_height
- )
- @property
- def top_visible(self) -> bool:
- """
- True when the top of the buffer is visible.
- """
- return self.vertical_scroll == 0
- @property
- def bottom_visible(self) -> bool:
- """
- True when the bottom of the buffer is visible.
- """
- return self.last_visible_line() == self.content_height - 1
- @property
- def vertical_scroll_percentage(self) -> int:
- """
- Vertical scroll as a percentage. (0 means: the top is visible,
- 100 means: the bottom is visible.)
- """
- if self.bottom_visible:
- return 100
- else:
- return 100 * self.vertical_scroll // self.content_height
- def get_height_for_line(self, lineno: int) -> int:
- """
- Return the height of the given line.
- (The height that it would take, if this line became visible.)
- """
- if self.wrap_lines:
- return self.ui_content.get_height_for_line(
- lineno, self.window_width, self.window.get_line_prefix
- )
- else:
- return 1
- class ScrollOffsets:
- """
- Scroll offsets for the :class:`.Window` class.
- Note that left/right offsets only make sense if line wrapping is disabled.
- """
- def __init__(
- self,
- top: int | Callable[[], int] = 0,
- bottom: int | Callable[[], int] = 0,
- left: int | Callable[[], int] = 0,
- right: int | Callable[[], int] = 0,
- ) -> None:
- self._top = top
- self._bottom = bottom
- self._left = left
- self._right = right
- @property
- def top(self) -> int:
- return to_int(self._top)
- @property
- def bottom(self) -> int:
- return to_int(self._bottom)
- @property
- def left(self) -> int:
- return to_int(self._left)
- @property
- def right(self) -> int:
- return to_int(self._right)
- def __repr__(self) -> str:
- return f"ScrollOffsets(top={self._top!r}, bottom={self._bottom!r}, left={self._left!r}, right={self._right!r})"
- class ColorColumn:
- """
- Column for a :class:`.Window` to be colored.
- """
- def __init__(self, position: int, style: str = "class:color-column") -> None:
- self.position = position
- self.style = style
- _in_insert_mode = vi_insert_mode | emacs_insert_mode
- class WindowAlign(Enum):
- """
- Alignment of the Window content.
- Note that this is different from `HorizontalAlign` and `VerticalAlign`,
- which are used for the alignment of the child containers in respectively
- `VSplit` and `HSplit`.
- """
- LEFT = "LEFT"
- RIGHT = "RIGHT"
- CENTER = "CENTER"
- class Window(Container):
- """
- Container that holds a control.
- :param content: :class:`.UIControl` instance.
- :param width: :class:`.Dimension` instance or callable.
- :param height: :class:`.Dimension` instance or callable.
- :param z_index: When specified, this can be used to bring element in front
- of floating elements.
- :param dont_extend_width: When `True`, don't take up more width then the
- preferred width reported by the control.
- :param dont_extend_height: When `True`, don't take up more width then the
- preferred height reported by the control.
- :param ignore_content_width: A `bool` or :class:`.Filter` instance. Ignore
- the :class:`.UIContent` width when calculating the dimensions.
- :param ignore_content_height: A `bool` or :class:`.Filter` instance. Ignore
- the :class:`.UIContent` height when calculating the dimensions.
- :param left_margins: A list of :class:`.Margin` instance to be displayed on
- the left. For instance: :class:`~prompt_toolkit.layout.NumberedMargin`
- can be one of them in order to show line numbers.
- :param right_margins: Like `left_margins`, but on the other side.
- :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
- preferred amount of lines/columns to be always visible before/after the
- cursor. When both top and bottom are a very high number, the cursor
- will be centered vertically most of the time.
- :param allow_scroll_beyond_bottom: A `bool` or
- :class:`.Filter` instance. When True, allow scrolling so far, that the
- top part of the content is not visible anymore, while there is still
- empty space available at the bottom of the window. In the Vi editor for
- instance, this is possible. You will see tildes while the top part of
- the body is hidden.
- :param wrap_lines: A `bool` or :class:`.Filter` instance. When True, don't
- scroll horizontally, but wrap lines instead.
- :param get_vertical_scroll: Callable that takes this window
- instance as input and returns a preferred vertical scroll.
- (When this is `None`, the scroll is only determined by the last and
- current cursor position.)
- :param get_horizontal_scroll: Callable that takes this window
- instance as input and returns a preferred vertical scroll.
- :param always_hide_cursor: A `bool` or
- :class:`.Filter` instance. When True, never display the cursor, even
- when the user control specifies a cursor position.
- :param cursorline: A `bool` or :class:`.Filter` instance. When True,
- display a cursorline.
- :param cursorcolumn: A `bool` or :class:`.Filter` instance. When True,
- display a cursorcolumn.
- :param colorcolumns: A list of :class:`.ColorColumn` instances that
- describe the columns to be highlighted, or a callable that returns such
- a list.
- :param align: :class:`.WindowAlign` value or callable that returns an
- :class:`.WindowAlign` value. alignment of content.
- :param style: A style string. Style to be applied to all the cells in this
- window. (This can be a callable that returns a string.)
- :param char: (string) Character to be used for filling the background. This
- can also be a callable that returns a character.
- :param get_line_prefix: None or a callable that returns formatted text to
- be inserted before a line. It takes a line number (int) and a
- wrap_count and returns formatted text. This can be used for
- implementation of line continuations, things like Vim "breakindent" and
- so on.
- """
- def __init__(
- self,
- content: UIControl | None = None,
- width: AnyDimension = None,
- height: AnyDimension = None,
- z_index: int | None = None,
- dont_extend_width: FilterOrBool = False,
- dont_extend_height: FilterOrBool = False,
- ignore_content_width: FilterOrBool = False,
- ignore_content_height: FilterOrBool = False,
- left_margins: Sequence[Margin] | None = None,
- right_margins: Sequence[Margin] | None = None,
- scroll_offsets: ScrollOffsets | None = None,
- allow_scroll_beyond_bottom: FilterOrBool = False,
- wrap_lines: FilterOrBool = False,
- get_vertical_scroll: Callable[[Window], int] | None = None,
- get_horizontal_scroll: Callable[[Window], int] | None = None,
- always_hide_cursor: FilterOrBool = False,
- cursorline: FilterOrBool = False,
- cursorcolumn: FilterOrBool = False,
- colorcolumns: (
- None | list[ColorColumn] | Callable[[], list[ColorColumn]]
- ) = None,
- align: WindowAlign | Callable[[], WindowAlign] = WindowAlign.LEFT,
- style: str | Callable[[], str] = "",
- char: None | str | Callable[[], str] = None,
- get_line_prefix: GetLinePrefixCallable | None = None,
- ) -> None:
- self.allow_scroll_beyond_bottom = to_filter(allow_scroll_beyond_bottom)
- self.always_hide_cursor = to_filter(always_hide_cursor)
- self.wrap_lines = to_filter(wrap_lines)
- self.cursorline = to_filter(cursorline)
- self.cursorcolumn = to_filter(cursorcolumn)
- self.content = content or DummyControl()
- self.dont_extend_width = to_filter(dont_extend_width)
- self.dont_extend_height = to_filter(dont_extend_height)
- self.ignore_content_width = to_filter(ignore_content_width)
- self.ignore_content_height = to_filter(ignore_content_height)
- self.left_margins = left_margins or []
- self.right_margins = right_margins or []
- self.scroll_offsets = scroll_offsets or ScrollOffsets()
- self.get_vertical_scroll = get_vertical_scroll
- self.get_horizontal_scroll = get_horizontal_scroll
- self.colorcolumns = colorcolumns or []
- self.align = align
- self.style = style
- self.char = char
- self.get_line_prefix = get_line_prefix
- self.width = width
- self.height = height
- self.z_index = z_index
- # Cache for the screens generated by the margin.
- self._ui_content_cache: SimpleCache[tuple[int, int, int], UIContent] = (
- SimpleCache(maxsize=8)
- )
- self._margin_width_cache: SimpleCache[tuple[Margin, int], int] = SimpleCache(
- maxsize=1
- )
- self.reset()
- def __repr__(self) -> str:
- return f"Window(content={self.content!r})"
- def reset(self) -> None:
- self.content.reset()
- #: Scrolling position of the main content.
- self.vertical_scroll = 0
- self.horizontal_scroll = 0
- # Vertical scroll 2: this is the vertical offset that a line is
- # scrolled if a single line (the one that contains the cursor) consumes
- # all of the vertical space.
- self.vertical_scroll_2 = 0
- #: Keep render information (mappings between buffer input and render
- #: output.)
- self.render_info: WindowRenderInfo | None = None
- def _get_margin_width(self, margin: Margin) -> int:
- """
- Return the width for this margin.
- (Calculate only once per render time.)
- """
- # Margin.get_width, needs to have a UIContent instance.
- def get_ui_content() -> UIContent:
- return self._get_ui_content(width=0, height=0)
- def get_width() -> int:
- return margin.get_width(get_ui_content)
- key = (margin, get_app().render_counter)
- return self._margin_width_cache.get(key, get_width)
- def _get_total_margin_width(self) -> int:
- """
- Calculate and return the width of the margin (left + right).
- """
- return sum(self._get_margin_width(m) for m in self.left_margins) + sum(
- self._get_margin_width(m) for m in self.right_margins
- )
- def preferred_width(self, max_available_width: int) -> Dimension:
- """
- Calculate the preferred width for this window.
- """
- def preferred_content_width() -> int | None:
- """Content width: is only calculated if no exact width for the
- window was given."""
- if self.ignore_content_width():
- return None
- # Calculate the width of the margin.
- total_margin_width = self._get_total_margin_width()
- # Window of the content. (Can be `None`.)
- preferred_width = self.content.preferred_width(
- max_available_width - total_margin_width
- )
- if preferred_width is not None:
- # Include width of the margins.
- preferred_width += total_margin_width
- return preferred_width
- # Merge.
- return self._merge_dimensions(
- dimension=to_dimension(self.width),
- get_preferred=preferred_content_width,
- dont_extend=self.dont_extend_width(),
- )
- def preferred_height(self, width: int, max_available_height: int) -> Dimension:
- """
- Calculate the preferred height for this window.
- """
- def preferred_content_height() -> int | None:
- """Content height: is only calculated if no exact height for the
- window was given."""
- if self.ignore_content_height():
- return None
- total_margin_width = self._get_total_margin_width()
- wrap_lines = self.wrap_lines()
- return self.content.preferred_height(
- width - total_margin_width,
- max_available_height,
- wrap_lines,
- self.get_line_prefix,
- )
- return self._merge_dimensions(
- dimension=to_dimension(self.height),
- get_preferred=preferred_content_height,
- dont_extend=self.dont_extend_height(),
- )
- @staticmethod
- def _merge_dimensions(
- dimension: Dimension | None,
- get_preferred: Callable[[], int | None],
- dont_extend: bool = False,
- ) -> Dimension:
- """
- Take the Dimension from this `Window` class and the received preferred
- size from the `UIControl` and return a `Dimension` to report to the
- parent container.
- """
- dimension = dimension or Dimension()
- # When a preferred dimension was explicitly given to the Window,
- # ignore the UIControl.
- preferred: int | None
- if dimension.preferred_specified:
- preferred = dimension.preferred
- else:
- # Otherwise, calculate the preferred dimension from the UI control
- # content.
- preferred = get_preferred()
- # When a 'preferred' dimension is given by the UIControl, make sure
- # that it stays within the bounds of the Window.
- if preferred is not None:
- if dimension.max_specified:
- preferred = min(preferred, dimension.max)
- if dimension.min_specified:
- preferred = max(preferred, dimension.min)
- # When a `dont_extend` flag has been given, use the preferred dimension
- # also as the max dimension.
- max_: int | None
- min_: int | None
- if dont_extend and preferred is not None:
- max_ = min(dimension.max, preferred)
- else:
- max_ = dimension.max if dimension.max_specified else None
- min_ = dimension.min if dimension.min_specified else None
- return Dimension(
- min=min_, max=max_, preferred=preferred, weight=dimension.weight
- )
- def _get_ui_content(self, width: int, height: int) -> UIContent:
- """
- Create a `UIContent` instance.
- """
- def get_content() -> UIContent:
- return self.content.create_content(width=width, height=height)
- key = (get_app().render_counter, width, height)
- return self._ui_content_cache.get(key, get_content)
- def _get_digraph_char(self) -> str | None:
- "Return `False`, or the Digraph symbol to be used."
- app = get_app()
- if app.quoted_insert:
- return "^"
- if app.vi_state.waiting_for_digraph:
- if app.vi_state.digraph_symbol1:
- return app.vi_state.digraph_symbol1
- return "?"
- return None
- def write_to_screen(
- self,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- parent_style: str,
- erase_bg: bool,
- z_index: int | None,
- ) -> None:
- """
- Write window to screen. This renders the user control, the margins and
- copies everything over to the absolute position at the given screen.
- """
- # If dont_extend_width/height was given. Then reduce width/height in
- # WritePosition if the parent wanted us to paint in a bigger area.
- # (This happens if this window is bundled with another window in a
- # HSplit/VSplit, but with different size requirements.)
- write_position = WritePosition(
- xpos=write_position.xpos,
- ypos=write_position.ypos,
- width=write_position.width,
- height=write_position.height,
- )
- if self.dont_extend_width():
- write_position.width = min(
- write_position.width,
- self.preferred_width(write_position.width).preferred,
- )
- if self.dont_extend_height():
- write_position.height = min(
- write_position.height,
- self.preferred_height(
- write_position.width, write_position.height
- ).preferred,
- )
- # Draw
- z_index = z_index if self.z_index is None else self.z_index
- draw_func = partial(
- self._write_to_screen_at_index,
- screen,
- mouse_handlers,
- write_position,
- parent_style,
- erase_bg,
- )
- if z_index is None or z_index <= 0:
- # When no z_index is given, draw right away.
- draw_func()
- else:
- # Otherwise, postpone.
- screen.draw_with_z_index(z_index=z_index, draw_func=draw_func)
- def _write_to_screen_at_index(
- self,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- parent_style: str,
- erase_bg: bool,
- ) -> None:
- # Don't bother writing invisible windows.
- # (We save some time, but also avoid applying last-line styling.)
- if write_position.height <= 0 or write_position.width <= 0:
- return
- # Calculate margin sizes.
- left_margin_widths = [self._get_margin_width(m) for m in self.left_margins]
- right_margin_widths = [self._get_margin_width(m) for m in self.right_margins]
- total_margin_width = sum(left_margin_widths + right_margin_widths)
- # Render UserControl.
- ui_content = self.content.create_content(
- write_position.width - total_margin_width, write_position.height
- )
- assert isinstance(ui_content, UIContent)
- # Scroll content.
- wrap_lines = self.wrap_lines()
- self._scroll(
- ui_content, write_position.width - total_margin_width, write_position.height
- )
- # Erase background and fill with `char`.
- self._fill_bg(screen, write_position, erase_bg)
- # Resolve `align` attribute.
- align = self.align() if callable(self.align) else self.align
- # Write body
- visible_line_to_row_col, rowcol_to_yx = self._copy_body(
- ui_content,
- screen,
- write_position,
- sum(left_margin_widths),
- write_position.width - total_margin_width,
- self.vertical_scroll,
- self.horizontal_scroll,
- wrap_lines=wrap_lines,
- highlight_lines=True,
- vertical_scroll_2=self.vertical_scroll_2,
- always_hide_cursor=self.always_hide_cursor(),
- has_focus=get_app().layout.current_control == self.content,
- align=align,
- get_line_prefix=self.get_line_prefix,
- )
- # Remember render info. (Set before generating the margins. They need this.)
- x_offset = write_position.xpos + sum(left_margin_widths)
- y_offset = write_position.ypos
- render_info = WindowRenderInfo(
- window=self,
- ui_content=ui_content,
- horizontal_scroll=self.horizontal_scroll,
- vertical_scroll=self.vertical_scroll,
- window_width=write_position.width - total_margin_width,
- window_height=write_position.height,
- configured_scroll_offsets=self.scroll_offsets,
- visible_line_to_row_col=visible_line_to_row_col,
- rowcol_to_yx=rowcol_to_yx,
- x_offset=x_offset,
- y_offset=y_offset,
- wrap_lines=wrap_lines,
- )
- self.render_info = render_info
- # Set mouse handlers.
- def mouse_handler(mouse_event: MouseEvent) -> NotImplementedOrNone:
- """
- Wrapper around the mouse_handler of the `UIControl` that turns
- screen coordinates into line coordinates.
- Returns `NotImplemented` if no UI invalidation should be done.
- """
- # Don't handle mouse events outside of the current modal part of
- # the UI.
- if self not in get_app().layout.walk_through_modal_area():
- return NotImplemented
- # Find row/col position first.
- yx_to_rowcol = {v: k for k, v in rowcol_to_yx.items()}
- y = mouse_event.position.y
- x = mouse_event.position.x
- # If clicked below the content area, look for a position in the
- # last line instead.
- max_y = write_position.ypos + len(visible_line_to_row_col) - 1
- y = min(max_y, y)
- result: NotImplementedOrNone
- while x >= 0:
- try:
- row, col = yx_to_rowcol[y, x]
- except KeyError:
- # Try again. (When clicking on the right side of double
- # width characters, or on the right side of the input.)
- x -= 1
- else:
- # Found position, call handler of UIControl.
- result = self.content.mouse_handler(
- MouseEvent(
- position=Point(x=col, y=row),
- event_type=mouse_event.event_type,
- button=mouse_event.button,
- modifiers=mouse_event.modifiers,
- )
- )
- break
- else:
- # nobreak.
- # (No x/y coordinate found for the content. This happens in
- # case of a DummyControl, that does not have any content.
- # Report (0,0) instead.)
- result = self.content.mouse_handler(
- MouseEvent(
- position=Point(x=0, y=0),
- event_type=mouse_event.event_type,
- button=mouse_event.button,
- modifiers=mouse_event.modifiers,
- )
- )
- # If it returns NotImplemented, handle it here.
- if result == NotImplemented:
- result = self._mouse_handler(mouse_event)
- return result
- mouse_handlers.set_mouse_handler_for_range(
- x_min=write_position.xpos + sum(left_margin_widths),
- x_max=write_position.xpos + write_position.width - total_margin_width,
- y_min=write_position.ypos,
- y_max=write_position.ypos + write_position.height,
- handler=mouse_handler,
- )
- # Render and copy margins.
- move_x = 0
- def render_margin(m: Margin, width: int) -> UIContent:
- "Render margin. Return `Screen`."
- # Retrieve margin fragments.
- fragments = m.create_margin(render_info, width, write_position.height)
- # Turn it into a UIContent object.
- # already rendered those fragments using this size.)
- return FormattedTextControl(fragments).create_content(
- width + 1, write_position.height
- )
- for m, width in zip(self.left_margins, left_margin_widths):
- if width > 0: # (ConditionalMargin returns a zero width. -- Don't render.)
- # Create screen for margin.
- margin_content = render_margin(m, width)
- # Copy and shift X.
- self._copy_margin(margin_content, screen, write_position, move_x, width)
- move_x += width
- move_x = write_position.width - sum(right_margin_widths)
- for m, width in zip(self.right_margins, right_margin_widths):
- # Create screen for margin.
- margin_content = render_margin(m, width)
- # Copy and shift X.
- self._copy_margin(margin_content, screen, write_position, move_x, width)
- move_x += width
- # Apply 'self.style'
- self._apply_style(screen, write_position, parent_style)
- # Tell the screen that this user control has been painted at this
- # position.
- screen.visible_windows_to_write_positions[self] = write_position
- def _copy_body(
- self,
- ui_content: UIContent,
- new_screen: Screen,
- write_position: WritePosition,
- move_x: int,
- width: int,
- vertical_scroll: int = 0,
- horizontal_scroll: int = 0,
- wrap_lines: bool = False,
- highlight_lines: bool = False,
- vertical_scroll_2: int = 0,
- always_hide_cursor: bool = False,
- has_focus: bool = False,
- align: WindowAlign = WindowAlign.LEFT,
- get_line_prefix: Callable[[int, int], AnyFormattedText] | None = None,
- ) -> tuple[dict[int, tuple[int, int]], dict[tuple[int, int], tuple[int, int]]]:
- """
- Copy the UIContent into the output screen.
- Return (visible_line_to_row_col, rowcol_to_yx) tuple.
- :param get_line_prefix: None or a callable that takes a line number
- (int) and a wrap_count (int) and returns formatted text.
- """
- xpos = write_position.xpos + move_x
- ypos = write_position.ypos
- line_count = ui_content.line_count
- new_buffer = new_screen.data_buffer
- empty_char = _CHAR_CACHE["", ""]
- # Map visible line number to (row, col) of input.
- # 'col' will always be zero if line wrapping is off.
- visible_line_to_row_col: dict[int, tuple[int, int]] = {}
- # Maps (row, col) from the input to (y, x) screen coordinates.
- rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
- def copy_line(
- line: StyleAndTextTuples,
- lineno: int,
- x: int,
- y: int,
- is_input: bool = False,
- ) -> tuple[int, int]:
- """
- Copy over a single line to the output screen. This can wrap over
- multiple lines in the output. It will call the prefix (prompt)
- function before every line.
- """
- if is_input:
- current_rowcol_to_yx = rowcol_to_yx
- else:
- current_rowcol_to_yx = {} # Throwaway dictionary.
- # Draw line prefix.
- if is_input and get_line_prefix:
- prompt = to_formatted_text(get_line_prefix(lineno, 0))
- x, y = copy_line(prompt, lineno, x, y, is_input=False)
- # Scroll horizontally.
- skipped = 0 # Characters skipped because of horizontal scrolling.
- if horizontal_scroll and is_input:
- h_scroll = horizontal_scroll
- line = explode_text_fragments(line)
- while h_scroll > 0 and line:
- h_scroll -= get_cwidth(line[0][1])
- skipped += 1
- del line[:1] # Remove first character.
- x -= h_scroll # When scrolling over double width character,
- # this can end up being negative.
- # Align this line. (Note that this doesn't work well when we use
- # get_line_prefix and that function returns variable width prefixes.)
- if align == WindowAlign.CENTER:
- line_width = fragment_list_width(line)
- if line_width < width:
- x += (width - line_width) // 2
- elif align == WindowAlign.RIGHT:
- line_width = fragment_list_width(line)
- if line_width < width:
- x += width - line_width
- col = 0
- wrap_count = 0
- for style, text, *_ in line:
- new_buffer_row = new_buffer[y + ypos]
- # Remember raw VT escape sequences. (E.g. FinalTerm's
- # escape sequences.)
- if "[ZeroWidthEscape]" in style:
- new_screen.zero_width_escapes[y + ypos][x + xpos] += text
- continue
- for c in text:
- char = _CHAR_CACHE[c, style]
- char_width = char.width
- # Wrap when the line width is exceeded.
- if wrap_lines and x + char_width > width:
- visible_line_to_row_col[y + 1] = (
- lineno,
- visible_line_to_row_col[y][1] + x,
- )
- y += 1
- wrap_count += 1
- x = 0
- # Insert line prefix (continuation prompt).
- if is_input and get_line_prefix:
- prompt = to_formatted_text(
- get_line_prefix(lineno, wrap_count)
- )
- x, y = copy_line(prompt, lineno, x, y, is_input=False)
- new_buffer_row = new_buffer[y + ypos]
- if y >= write_position.height:
- return x, y # Break out of all for loops.
- # Set character in screen and shift 'x'.
- if x >= 0 and y >= 0 and x < width:
- new_buffer_row[x + xpos] = char
- # When we print a multi width character, make sure
- # to erase the neighbors positions in the screen.
- # (The empty string if different from everything,
- # so next redraw this cell will repaint anyway.)
- if char_width > 1:
- for i in range(1, char_width):
- new_buffer_row[x + xpos + i] = empty_char
- # If this is a zero width characters, then it's
- # probably part of a decomposed unicode character.
- # See: https://en.wikipedia.org/wiki/Unicode_equivalence
- # Merge it in the previous cell.
- elif char_width == 0:
- # Handle all character widths. If the previous
- # character is a multiwidth character, then
- # merge it two positions back.
- for pw in [2, 1]: # Previous character width.
- if (
- x - pw >= 0
- and new_buffer_row[x + xpos - pw].width == pw
- ):
- prev_char = new_buffer_row[x + xpos - pw]
- char2 = _CHAR_CACHE[
- prev_char.char + c, prev_char.style
- ]
- new_buffer_row[x + xpos - pw] = char2
- # Keep track of write position for each character.
- current_rowcol_to_yx[lineno, col + skipped] = (
- y + ypos,
- x + xpos,
- )
- col += 1
- x += char_width
- return x, y
- # Copy content.
- def copy() -> int:
- y = -vertical_scroll_2
- lineno = vertical_scroll
- while y < write_position.height and lineno < line_count:
- # Take the next line and copy it in the real screen.
- line = ui_content.get_line(lineno)
- visible_line_to_row_col[y] = (lineno, horizontal_scroll)
- # Copy margin and actual line.
- x = 0
- x, y = copy_line(line, lineno, x, y, is_input=True)
- lineno += 1
- y += 1
- return y
- copy()
- def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
- "Translate row/col from UIContent to real Screen coordinates."
- try:
- y, x = rowcol_to_yx[row, col]
- except KeyError:
- # Normally this should never happen. (It is a bug, if it happens.)
- # But to be sure, return (0, 0)
- return Point(x=0, y=0)
- # raise ValueError(
- # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
- # 'horizontal_scroll=%r, height=%r' %
- # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
- else:
- return Point(x=x, y=y)
- # Set cursor and menu positions.
- if ui_content.cursor_position:
- screen_cursor_position = cursor_pos_to_screen_pos(
- ui_content.cursor_position.y, ui_content.cursor_position.x
- )
- if has_focus:
- new_screen.set_cursor_position(self, screen_cursor_position)
- if always_hide_cursor:
- new_screen.show_cursor = False
- else:
- new_screen.show_cursor = ui_content.show_cursor
- self._highlight_digraph(new_screen)
- if highlight_lines:
- self._highlight_cursorlines(
- new_screen,
- screen_cursor_position,
- xpos,
- ypos,
- width,
- write_position.height,
- )
- # Draw input characters from the input processor queue.
- if has_focus and ui_content.cursor_position:
- self._show_key_processor_key_buffer(new_screen)
- # Set menu position.
- if ui_content.menu_position:
- new_screen.set_menu_position(
- self,
- cursor_pos_to_screen_pos(
- ui_content.menu_position.y, ui_content.menu_position.x
- ),
- )
- # Update output screen height.
- new_screen.height = max(new_screen.height, ypos + write_position.height)
- return visible_line_to_row_col, rowcol_to_yx
- def _fill_bg(
- self, screen: Screen, write_position: WritePosition, erase_bg: bool
- ) -> None:
- """
- Erase/fill the background.
- (Useful for floats and when a `char` has been given.)
- """
- char: str | None
- if callable(self.char):
- char = self.char()
- else:
- char = self.char
- if erase_bg or char:
- wp = write_position
- char_obj = _CHAR_CACHE[char or " ", ""]
- for y in range(wp.ypos, wp.ypos + wp.height):
- row = screen.data_buffer[y]
- for x in range(wp.xpos, wp.xpos + wp.width):
- row[x] = char_obj
- def _apply_style(
- self, new_screen: Screen, write_position: WritePosition, parent_style: str
- ) -> None:
- # Apply `self.style`.
- style = parent_style + " " + to_str(self.style)
- new_screen.fill_area(write_position, style=style, after=False)
- # Apply the 'last-line' class to the last line of each Window. This can
- # be used to apply an 'underline' to the user control.
- wp = WritePosition(
- write_position.xpos,
- write_position.ypos + write_position.height - 1,
- write_position.width,
- 1,
- )
- new_screen.fill_area(wp, "class:last-line", after=True)
- def _highlight_digraph(self, new_screen: Screen) -> None:
- """
- When we are in Vi digraph mode, put a question mark underneath the
- cursor.
- """
- digraph_char = self._get_digraph_char()
- if digraph_char:
- cpos = new_screen.get_cursor_position(self)
- new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[
- digraph_char, "class:digraph"
- ]
- def _show_key_processor_key_buffer(self, new_screen: Screen) -> None:
- """
- When the user is typing a key binding that consists of several keys,
- display the last pressed key if the user is in insert mode and the key
- is meaningful to be displayed.
- E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the
- first 'j' needs to be displayed in order to get some feedback.
- """
- app = get_app()
- key_buffer = app.key_processor.key_buffer
- if key_buffer and _in_insert_mode() and not app.is_done:
- # The textual data for the given key. (Can be a VT100 escape
- # sequence.)
- data = key_buffer[-1].data
- # Display only if this is a 1 cell width character.
- if get_cwidth(data) == 1:
- cpos = new_screen.get_cursor_position(self)
- new_screen.data_buffer[cpos.y][cpos.x] = _CHAR_CACHE[
- data, "class:partial-key-binding"
- ]
- def _highlight_cursorlines(
- self, new_screen: Screen, cpos: Point, x: int, y: int, width: int, height: int
- ) -> None:
- """
- Highlight cursor row/column.
- """
- cursor_line_style = " class:cursor-line "
- cursor_column_style = " class:cursor-column "
- data_buffer = new_screen.data_buffer
- # Highlight cursor line.
- if self.cursorline():
- row = data_buffer[cpos.y]
- for x in range(x, x + width):
- original_char = row[x]
- row[x] = _CHAR_CACHE[
- original_char.char, original_char.style + cursor_line_style
- ]
- # Highlight cursor column.
- if self.cursorcolumn():
- for y2 in range(y, y + height):
- row = data_buffer[y2]
- original_char = row[cpos.x]
- row[cpos.x] = _CHAR_CACHE[
- original_char.char, original_char.style + cursor_column_style
- ]
- # Highlight color columns
- colorcolumns = self.colorcolumns
- if callable(colorcolumns):
- colorcolumns = colorcolumns()
- for cc in colorcolumns:
- assert isinstance(cc, ColorColumn)
- column = cc.position
- if column < x + width: # Only draw when visible.
- color_column_style = " " + cc.style
- for y2 in range(y, y + height):
- row = data_buffer[y2]
- original_char = row[column + x]
- row[column + x] = _CHAR_CACHE[
- original_char.char, original_char.style + color_column_style
- ]
- def _copy_margin(
- self,
- margin_content: UIContent,
- new_screen: Screen,
- write_position: WritePosition,
- move_x: int,
- width: int,
- ) -> None:
- """
- Copy characters from the margin screen to the real screen.
- """
- xpos = write_position.xpos + move_x
- ypos = write_position.ypos
- margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
- self._copy_body(margin_content, new_screen, margin_write_position, 0, width)
- def _scroll(self, ui_content: UIContent, width: int, height: int) -> None:
- """
- Scroll body. Ensure that the cursor is visible.
- """
- if self.wrap_lines():
- func = self._scroll_when_linewrapping
- else:
- func = self._scroll_without_linewrapping
- func(ui_content, width, height)
- def _scroll_when_linewrapping(
- self, ui_content: UIContent, width: int, height: int
- ) -> None:
- """
- Scroll to make sure the cursor position is visible and that we maintain
- the requested scroll offset.
- Set `self.horizontal_scroll/vertical_scroll`.
- """
- scroll_offsets_bottom = self.scroll_offsets.bottom
- scroll_offsets_top = self.scroll_offsets.top
- # We don't have horizontal scrolling.
- self.horizontal_scroll = 0
- def get_line_height(lineno: int) -> int:
- return ui_content.get_height_for_line(lineno, width, self.get_line_prefix)
- # When there is no space, reset `vertical_scroll_2` to zero and abort.
- # This can happen if the margin is bigger than the window width.
- # Otherwise the text height will become "infinite" (a big number) and
- # the copy_line will spend a huge amount of iterations trying to render
- # nothing.
- if width <= 0:
- self.vertical_scroll = ui_content.cursor_position.y
- self.vertical_scroll_2 = 0
- return
- # If the current line consumes more than the whole window height,
- # then we have to scroll vertically inside this line. (We don't take
- # the scroll offsets into account for this.)
- # Also, ignore the scroll offsets in this case. Just set the vertical
- # scroll to this line.
- line_height = get_line_height(ui_content.cursor_position.y)
- if line_height > height - scroll_offsets_top:
- # Calculate the height of the text before the cursor (including
- # line prefixes).
- text_before_height = ui_content.get_height_for_line(
- ui_content.cursor_position.y,
- width,
- self.get_line_prefix,
- slice_stop=ui_content.cursor_position.x,
- )
- # Adjust scroll offset.
- self.vertical_scroll = ui_content.cursor_position.y
- self.vertical_scroll_2 = min(
- text_before_height - 1, # Keep the cursor visible.
- line_height
- - height, # Avoid blank lines at the bottom when scrolling up again.
- self.vertical_scroll_2,
- )
- self.vertical_scroll_2 = max(
- 0, text_before_height - height, self.vertical_scroll_2
- )
- return
- else:
- self.vertical_scroll_2 = 0
- # Current line doesn't consume the whole height. Take scroll offsets into account.
- def get_min_vertical_scroll() -> int:
- # Make sure that the cursor line is not below the bottom.
- # (Calculate how many lines can be shown between the cursor and the .)
- used_height = 0
- prev_lineno = ui_content.cursor_position.y
- for lineno in range(ui_content.cursor_position.y, -1, -1):
- used_height += get_line_height(lineno)
- if used_height > height - scroll_offsets_bottom:
- return prev_lineno
- else:
- prev_lineno = lineno
- return 0
- def get_max_vertical_scroll() -> int:
- # Make sure that the cursor line is not above the top.
- prev_lineno = ui_content.cursor_position.y
- used_height = 0
- for lineno in range(ui_content.cursor_position.y - 1, -1, -1):
- used_height += get_line_height(lineno)
- if used_height > scroll_offsets_top:
- return prev_lineno
- else:
- prev_lineno = lineno
- return prev_lineno
- def get_topmost_visible() -> int:
- """
- Calculate the upper most line that can be visible, while the bottom
- is still visible. We should not allow scroll more than this if
- `allow_scroll_beyond_bottom` is false.
- """
- prev_lineno = ui_content.line_count - 1
- used_height = 0
- for lineno in range(ui_content.line_count - 1, -1, -1):
- used_height += get_line_height(lineno)
- if used_height > height:
- return prev_lineno
- else:
- prev_lineno = lineno
- return prev_lineno
- # Scroll vertically. (Make sure that the whole line which contains the
- # cursor is visible.
- topmost_visible = get_topmost_visible()
- # Note: the `min(topmost_visible, ...)` is to make sure that we
- # don't require scrolling up because of the bottom scroll offset,
- # when we are at the end of the document.
- self.vertical_scroll = max(
- self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll())
- )
- self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll())
- # Disallow scrolling beyond bottom?
- if not self.allow_scroll_beyond_bottom():
- self.vertical_scroll = min(self.vertical_scroll, topmost_visible)
- def _scroll_without_linewrapping(
- self, ui_content: UIContent, width: int, height: int
- ) -> None:
- """
- Scroll to make sure the cursor position is visible and that we maintain
- the requested scroll offset.
- Set `self.horizontal_scroll/vertical_scroll`.
- """
- cursor_position = ui_content.cursor_position or Point(x=0, y=0)
- # Without line wrapping, we will never have to scroll vertically inside
- # a single line.
- self.vertical_scroll_2 = 0
- if ui_content.line_count == 0:
- self.vertical_scroll = 0
- self.horizontal_scroll = 0
- return
- else:
- current_line_text = fragment_list_to_text(
- ui_content.get_line(cursor_position.y)
- )
- def do_scroll(
- current_scroll: int,
- scroll_offset_start: int,
- scroll_offset_end: int,
- cursor_pos: int,
- window_size: int,
- content_size: int,
- ) -> int:
- "Scrolling algorithm. Used for both horizontal and vertical scrolling."
- # Calculate the scroll offset to apply.
- # This can obviously never be more than have the screen size. Also, when the
- # cursor appears at the top or bottom, we don't apply the offset.
- scroll_offset_start = int(
- min(scroll_offset_start, window_size / 2, cursor_pos)
- )
- scroll_offset_end = int(
- min(scroll_offset_end, window_size / 2, content_size - 1 - cursor_pos)
- )
- # Prevent negative scroll offsets.
- if current_scroll < 0:
- current_scroll = 0
- # Scroll back if we scrolled to much and there's still space to show more of the document.
- if (
- not self.allow_scroll_beyond_bottom()
- and current_scroll > content_size - window_size
- ):
- current_scroll = max(0, content_size - window_size)
- # Scroll up if cursor is before visible part.
- if current_scroll > cursor_pos - scroll_offset_start:
- current_scroll = max(0, cursor_pos - scroll_offset_start)
- # Scroll down if cursor is after visible part.
- if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
- current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end
- return current_scroll
- # When a preferred scroll is given, take that first into account.
- if self.get_vertical_scroll:
- self.vertical_scroll = self.get_vertical_scroll(self)
- assert isinstance(self.vertical_scroll, int)
- if self.get_horizontal_scroll:
- self.horizontal_scroll = self.get_horizontal_scroll(self)
- assert isinstance(self.horizontal_scroll, int)
- # Update horizontal/vertical scroll to make sure that the cursor
- # remains visible.
- offsets = self.scroll_offsets
- self.vertical_scroll = do_scroll(
- current_scroll=self.vertical_scroll,
- scroll_offset_start=offsets.top,
- scroll_offset_end=offsets.bottom,
- cursor_pos=ui_content.cursor_position.y,
- window_size=height,
- content_size=ui_content.line_count,
- )
- if self.get_line_prefix:
- current_line_prefix_width = fragment_list_width(
- to_formatted_text(self.get_line_prefix(ui_content.cursor_position.y, 0))
- )
- else:
- current_line_prefix_width = 0
- self.horizontal_scroll = do_scroll(
- current_scroll=self.horizontal_scroll,
- scroll_offset_start=offsets.left,
- scroll_offset_end=offsets.right,
- cursor_pos=get_cwidth(current_line_text[: ui_content.cursor_position.x]),
- window_size=width - current_line_prefix_width,
- # We can only analyze the current line. Calculating the width off
- # all the lines is too expensive.
- content_size=max(
- get_cwidth(current_line_text), self.horizontal_scroll + width
- ),
- )
- def _mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
- """
- Mouse handler. Called when the UI control doesn't handle this
- particular event.
- Return `NotImplemented` if nothing was done as a consequence of this
- key binding (no UI invalidate required in that case).
- """
- if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
- self._scroll_down()
- return None
- elif mouse_event.event_type == MouseEventType.SCROLL_UP:
- self._scroll_up()
- return None
- return NotImplemented
- def _scroll_down(self) -> None:
- "Scroll window down."
- info = self.render_info
- if info is None:
- return
- if self.vertical_scroll < info.content_height - info.window_height:
- if info.cursor_position.y <= info.configured_scroll_offsets.top:
- self.content.move_cursor_down()
- self.vertical_scroll += 1
- def _scroll_up(self) -> None:
- "Scroll window up."
- info = self.render_info
- if info is None:
- return
- if info.vertical_scroll > 0:
- # TODO: not entirely correct yet in case of line wrapping and long lines.
- if (
- info.cursor_position.y
- >= info.window_height - 1 - info.configured_scroll_offsets.bottom
- ):
- self.content.move_cursor_up()
- self.vertical_scroll -= 1
- def get_key_bindings(self) -> KeyBindingsBase | None:
- return self.content.get_key_bindings()
- def get_children(self) -> list[Container]:
- return []
- class ConditionalContainer(Container):
- """
- Wrapper around any other container that can change the visibility. The
- received `filter` determines whether the given container should be
- displayed or not.
- :param content: :class:`.Container` instance.
- :param filter: :class:`.Filter` instance.
- """
- def __init__(self, content: AnyContainer, filter: FilterOrBool) -> None:
- self.content = to_container(content)
- self.filter = to_filter(filter)
- def __repr__(self) -> str:
- return f"ConditionalContainer({self.content!r}, filter={self.filter!r})"
- def reset(self) -> None:
- self.content.reset()
- def preferred_width(self, max_available_width: int) -> Dimension:
- if self.filter():
- return self.content.preferred_width(max_available_width)
- else:
- return Dimension.zero()
- def preferred_height(self, width: int, max_available_height: int) -> Dimension:
- if self.filter():
- return self.content.preferred_height(width, max_available_height)
- else:
- return Dimension.zero()
- def write_to_screen(
- self,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- parent_style: str,
- erase_bg: bool,
- z_index: int | None,
- ) -> None:
- if self.filter():
- return self.content.write_to_screen(
- screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
- )
- def get_children(self) -> list[Container]:
- return [self.content]
- class DynamicContainer(Container):
- """
- Container class that dynamically returns any Container.
- :param get_container: Callable that returns a :class:`.Container` instance
- or any widget with a ``__pt_container__`` method.
- """
- def __init__(self, get_container: Callable[[], AnyContainer]) -> None:
- self.get_container = get_container
- def _get_container(self) -> Container:
- """
- Return the current container object.
- We call `to_container`, because `get_container` can also return a
- widget with a ``__pt_container__`` method.
- """
- obj = self.get_container()
- return to_container(obj)
- def reset(self) -> None:
- self._get_container().reset()
- def preferred_width(self, max_available_width: int) -> Dimension:
- return self._get_container().preferred_width(max_available_width)
- def preferred_height(self, width: int, max_available_height: int) -> Dimension:
- return self._get_container().preferred_height(width, max_available_height)
- def write_to_screen(
- self,
- screen: Screen,
- mouse_handlers: MouseHandlers,
- write_position: WritePosition,
- parent_style: str,
- erase_bg: bool,
- z_index: int | None,
- ) -> None:
- self._get_container().write_to_screen(
- screen, mouse_handlers, write_position, parent_style, erase_bg, z_index
- )
- def is_modal(self) -> bool:
- return False
- def get_key_bindings(self) -> KeyBindingsBase | None:
- # Key bindings will be collected when `layout.walk()` finds the child
- # container.
- return None
- def get_children(self) -> list[Container]:
- # Here we have to return the current active container itself, not its
- # children. Otherwise, we run into issues where `layout.walk()` will
- # never see an object of type `Window` if this contains a window. We
- # can't/shouldn't proxy the "isinstance" check.
- return [self._get_container()]
- def to_container(container: AnyContainer) -> Container:
- """
- Make sure that the given object is a :class:`.Container`.
- """
- if isinstance(container, Container):
- return container
- elif hasattr(container, "__pt_container__"):
- return to_container(container.__pt_container__())
- else:
- raise ValueError(f"Not a container object: {container!r}")
- def to_window(container: AnyContainer) -> Window:
- """
- Make sure that the given argument is a :class:`.Window`.
- """
- if isinstance(container, Window):
- return container
- elif hasattr(container, "__pt_container__"):
- return to_window(cast("MagicContainer", container).__pt_container__())
- else:
- raise ValueError(f"Not a Window object: {container!r}.")
- def is_container(value: object) -> TypeGuard[AnyContainer]:
- """
- Checks whether the given value is a container object
- (for use in assert statements).
- """
- if isinstance(value, Container):
- return True
- if hasattr(value, "__pt_container__"):
- return is_container(cast("MagicContainer", value).__pt_container__())
- return False
|