Source code for TermTk.TTkGui.TTkTextWrap.text_wrap

# MIT License
#
# Copyright (c) 2026 Eugenio Parodi <ceccopierangiolieugenio AT googlemail DOT com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

__all__ = ['TTkTextWrap']


from typing import Optional, Tuple

from TermTk.TTkCore.constant import TTkK
from TermTk.TTkCore.signal import pyTTkSignal, pyTTkSlot
from TermTk.TTkGui.textdocument import TTkTextDocument

from .text_wrap_data import _RetScreenPositions, _RetScreenRows, _WrapState, _ReWrapData
from .text_wrap_engine import _WrapEngine_Interface
from .text_wrap_engine_no_wrap import _WrapEngine_NoWrap
from .text_wrap_engine_vim_wrap import _WrapEngine_VimWrap
from .text_wrap_engine_vim_wrap_hybrid import _WrapEngine_HybridVimWrap
from .text_wrap_engine_fast_wrap import _WrapEngine_FastWrap
from .text_wrap_engine_full_wrap import _WrapEngine_FullWrap

_wrapEngines = {
    TTkK.WrapEngine.NoWrap : _WrapEngine_NoWrap,
    TTkK.WrapEngine.VimWrap : _WrapEngine_VimWrap,
    TTkK.WrapEngine.FastWrap : _WrapEngine_FastWrap,
    TTkK.WrapEngine.FullWrap : _WrapEngine_FullWrap,
    TTkK.WrapEngine.HybridVimWrap : _WrapEngine_HybridVimWrap,
}

[docs] class TTkTextWrap(): '''TTkTextWrap: Incremental text wrapping helper for :py:class:`TTkTextDocument`. It maps document positions to wrapped screen rows and vice versa. ''' __slots__ = ( '_wrapState', '_wrapEngine', # Signals 'wrapChanged' ) _wrapState: _WrapState _wrapEngine: _WrapEngine_Interface wrapChanged: pyTTkSignal ''' This signal is emitted whenever wrapped line mapping changes. It is triggered after incremental updates from document edits and after explicit full rewrap requests. ''' def __init__(self, document:TTkTextDocument) -> None: '''Create a wrap manager bound to a text document. :param document: the source text document to wrap. :type document: :py:class:`TTkTextDocument` ''' # signals self.wrapChanged = pyTTkSignal() self._wrapState = _WrapState( size=80, tabSpaces=4, textDocument=document, wordWrapMode=TTkK.WrapAnywhere, ) self._wrapEngine = _WrapEngine_NoWrap(state=self._wrapState) document.contentsChange.connect(self._documentContentsChange) @pyTTkSlot(int,int,int) def _documentContentsChange(self, line:int, removed:int, added:int) -> None: self._wrapEngine.rewrap(data=_ReWrapData(line,added,removed)) self.wrapChanged.emit()
[docs] def engine(self) -> TTkK.WrapEngine: for _e, _t in _wrapEngines.items(): if isinstance(self._wrapEngine, _t): return _e return TTkK.WrapEngine.NoWrap
[docs] def setEngine(self, engine:TTkK.WrapEngine, width:Optional[int]=None) -> None: '''Switch the wrapping backend implementation. :param engine: engine selector from :py:class:`TTkK.WrapEngine`. :type engine: :py:class:`TTkK.WrapEngine` :param width: optional wrap width applied before switching engines when the engine type actually changes. :type width: Optional[int] ''' engine_class = _wrapEngines.get(engine, _WrapEngine_NoWrap) if isinstance(self._wrapEngine, engine_class): return if width is not None: self._wrapState.size = width self._wrapEngine = engine_class(state=self._wrapState) self.rewrap()
[docs] def size(self) -> int: '''Return the estimated wrapped row count. :return: wrapped size in screen rows. :rtype: int ''' return self._wrapEngine.size()
[docs] def documentLineCount(self) -> int: '''Return the number of logical data lines in the document. :return: document line count. :rtype: int ''' return self._wrapState.textDocument.lineCount()
[docs] def wrapWidth(self) -> int: '''Return the current wrap width in terminal cells. :return: wrap width. :rtype: int ''' return self._wrapState.size
[docs] def setWrapWidth(self, width:int) -> None: '''Set wrap width and trigger a full rewrap. :param width: target width in terminal cells. :type width: int ''' self._wrapState.size = width self.rewrap()
[docs] def wordWrapMode(self) -> TTkK.WrapMode: '''Return the active word-wrap mode. :return: current wrap mode. :rtype: :py:class:`TTkK.WrapMode` ''' return self._wrapState.wordWrapMode
[docs] def setWordWrapMode(self, mode:TTkK.WrapMode) -> None: '''Set the word-wrap mode and invalidate cached wrapping. :param mode: new wrap mode. :type mode: :py:class:`TTkK.WrapMode` ''' self._wrapState.wordWrapMode = mode self.rewrap()
[docs] def screenRows(self, y:int, h:int) -> _RetScreenRows: '''Return wrapped slices visible in the requested viewport. :param y: first screen row. :type y: int :param h: number of rows to extract. :type h: int :return: wrapped row slices. :rtype: :py:class:`_RetScreenRows` ''' if h <= 0: return _RetScreenRows(rows=[]) return self._wrapEngine.screenRows(y=y,h=h)
[docs] @pyTTkSlot(int, int) def ensureScreenRows(self, y:int, h:int) -> None: '''Force materialization of wrapped rows in a viewport range. For lazy-loading engines (FastWrap, VimWrap, HybridVimWrap), this materializes the rows that would be returned by :py:meth:`screenRows`. For eager engines (FullWrap, NoWrap), this is typically a no-op. This is useful when you need :py:meth:`size()` to return an accurate estimate instead of a prediction, as lazy engines improve their estimates as more chunks are materialized. :param y: first screen row to materialize. :type y: int :param h: number of rows to materialize. :type h: int ''' self._wrapEngine.ensureScreenRows(y=y, h=h)
[docs] def rewrap(self) -> None: '''Force a complete wrap refresh and emit ``wrapChanged``. This invalidates any incremental wrapping cache maintained by the active engine. ''' self._wrapEngine.rewrap() self.wrapChanged.emit()
[docs] def dataToScreenPosition(self, line:int, pos:int) -> _RetScreenPositions: '''Map a document position to wrapped screen coordinates. :param line: logical line index. :type line: int :param pos: character position in the logical line. :type pos: int :return: wrapped screen coordinates. :rtype: :py:class:`_RetScreenPositions` ''' return self._wrapEngine.dataToScreenPosition(line=line, pos=pos)
[docs] def screenToDataPosition(self, x:int, y:int) -> Tuple[int, int]: '''Map wrapped screen coordinates to a document position. :param x: horizontal screen coordinate. :type x: int :param y: vertical screen coordinate. :type y: int :return: ``(line, pos)`` document position. :rtype: Tuple[int, int] ''' return self._wrapEngine.screenToDataPosition(x=x, y=y)
[docs] def normalizeScreenPosition(self, x:int, y:int) -> Tuple[int, int]: '''Snap a screen position to the nearest editable character cell. :param x: horizontal widget-relative coordinate. :type x: int :param y: vertical widget-relative coordinate. :type y: int :return: normalized ``(x, y)`` screen position. :rtype: Tuple[int, int] ''' return self._wrapEngine.normalizeScreenPosition(x=x, y=y)