# 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)