namespace Terminal.Gui.Views; /// /// Manages word wrapping for a in a . /// /// /// The class provides functionality to handle word wrapping for multi-line text. /// It works with a to manage wrapped lines, calculate positions, and update the model /// when text is added, removed, or modified. This is used internally by text input controls like /// /// to ensure proper word wrapping behavior. /// internal class WordWrapManager (TextModel model) { private int _frameWidth; private bool _isWrapModelRefreshing; private List _wrappedModelLines = []; public TextModel Model { get; private set; } = model; public void AddLine (int row, int col) { int modelRow = GetModelLineFromWrappedLines (row); int modelCol = GetModelColFromWrappedLines (row, col); List line = GetCurrentLine (modelRow); int restCount = line.Count - modelCol; List rest = line.GetRange (modelCol, restCount); line.RemoveRange (modelCol, restCount); Model.AddLine (modelRow + 1, rest); _isWrapModelRefreshing = true; WrapModel (_frameWidth, out _, out _, out _, out _, modelRow + 1); _isWrapModelRefreshing = false; } public int GetModelColFromWrappedLines (int line, int col) { if (_wrappedModelLines?.Count == 0) { return 0; } int modelLine = GetModelLineFromWrappedLines (line); int firstLine = _wrappedModelLines.IndexOf (r => r.ModelLine == modelLine); var modelCol = 0; for (int i = firstLine; i <= Math.Min (line, _wrappedModelLines!.Count - 1); i++) { WrappedLine wLine = _wrappedModelLines [i]; if (i < line) { modelCol += wLine.ColWidth; } else { modelCol += col; } } return modelCol; } public int GetModelLineFromWrappedLines (int line) { return _wrappedModelLines.Count > 0 ? _wrappedModelLines [Math.Min ( line, _wrappedModelLines.Count - 1 )].ModelLine : 0; } public int GetWrappedLineColWidth (int line, int col, WordWrapManager wrapManager) { if (_wrappedModelLines?.Count == 0) { return 0; } List wModelLines = wrapManager._wrappedModelLines; int modelLine = GetModelLineFromWrappedLines (line); int firstLine = _wrappedModelLines.IndexOf (r => r.ModelLine == modelLine); var modelCol = 0; var colWidthOffset = 0; int i = firstLine; while (modelCol < col) { WrappedLine wLine = _wrappedModelLines! [i]; WrappedLine wLineToCompare = wModelLines [i]; if (wLine.ModelLine != modelLine || wLineToCompare.ModelLine != modelLine) { break; } modelCol += Math.Max (wLine.ColWidth, wLineToCompare.ColWidth); colWidthOffset += wLine.ColWidth - wLineToCompare.ColWidth; if (modelCol > col) { modelCol += col - modelCol; } i++; } return modelCol - colWidthOffset; } public bool Insert (int row, int col, Cell cell) { List line = GetCurrentLine (GetModelLineFromWrappedLines (row)); line.Insert (GetModelColFromWrappedLines (row, col), cell); if (line.Count > _frameWidth) { return true; } return false; } public bool RemoveAt (int row, int col) { int modelRow = GetModelLineFromWrappedLines (row); List line = GetCurrentLine (modelRow); int modelCol = GetModelColFromWrappedLines (row, col); if (modelCol > line.Count) { Model.RemoveLine (modelRow); RemoveAt (row, 0); return false; } if (modelCol < line.Count) { line.RemoveAt (modelCol); } if (line.Count > _frameWidth || (row + 1 < _wrappedModelLines.Count && _wrappedModelLines [row + 1].ModelLine == modelRow)) { return true; } return false; } public bool RemoveLine (int row, int col, out bool lineRemoved, bool forward = true) { lineRemoved = false; int modelRow = GetModelLineFromWrappedLines (row); List line = GetCurrentLine (modelRow); int modelCol = GetModelColFromWrappedLines (row, col); if (modelCol == 0 && line.Count == 0) { Model.RemoveLine (modelRow); return false; } if (modelCol < line.Count) { if (forward) { line.RemoveAt (modelCol); return true; } if (modelCol - 1 > -1) { line.RemoveAt (modelCol - 1); return true; } } lineRemoved = true; if (forward) { if (modelRow + 1 == Model.Count) { return false; } List nextLine = Model.GetLine (modelRow + 1); line.AddRange (nextLine); Model.RemoveLine (modelRow + 1); if (line.Count > _frameWidth) { return true; } } else { if (modelRow == 0) { return false; } List prevLine = Model.GetLine (modelRow - 1); prevLine.AddRange (line); Model.RemoveLine (modelRow); if (prevLine.Count > _frameWidth) { return true; } } return false; } public bool RemoveRange (int row, int index, int count) { int modelRow = GetModelLineFromWrappedLines (row); List line = GetCurrentLine (modelRow); int modelCol = GetModelColFromWrappedLines (row, index); try { line.RemoveRange (modelCol, count); } catch (Exception) { return false; } return true; } public List> ToListRune (List textList) { List> runesList = new (); foreach (string text in textList) { runesList.Add (Cell.ToCellList (text)); } return runesList; } public void UpdateModel ( TextModel model, out int nRow, out int nCol, out int nStartRow, out int nStartCol, int row, int col, int startRow, int startCol, bool preserveTrailingSpaces ) { _isWrapModelRefreshing = true; Model = model; WrapModel ( _frameWidth, out nRow, out nCol, out nStartRow, out nStartCol, row, col, startRow, startCol, 0, preserveTrailingSpaces ); _isWrapModelRefreshing = false; } public TextModel WrapModel ( int width, out int nRow, out int nCol, out int nStartRow, out int nStartCol, int row = 0, int col = 0, int startRow = 0, int startCol = 0, int tabWidth = 0, bool preserveTrailingSpaces = true ) { _frameWidth = width; int modelRow = _isWrapModelRefreshing ? row : GetModelLineFromWrappedLines (row); int modelCol = _isWrapModelRefreshing ? col : GetModelColFromWrappedLines (row, col); int modelStartRow = _isWrapModelRefreshing ? startRow : GetModelLineFromWrappedLines (startRow); int modelStartCol = _isWrapModelRefreshing ? startCol : GetModelColFromWrappedLines (startRow, startCol); var wrappedModel = new TextModel (); var lines = 0; nRow = 0; nCol = 0; nStartRow = 0; nStartCol = 0; bool isRowAndColSet = row == 0 && col == 0; bool isStartRowAndColSet = startRow == 0 && startCol == 0; List wModelLines = new (); for (var i = 0; i < Model.Count; i++) { List line = Model.GetLine (i); List> wrappedLines = ToListRune ( TextFormatter.Format ( Cell.ToString (line), width, Alignment.Start, true, preserveTrailingSpaces, tabWidth ) ); var sumColWidth = 0; for (var j = 0; j < wrappedLines.Count; j++) { List wrapLine = wrappedLines [j]; if (!isRowAndColSet && modelRow == i) { if (nCol + wrapLine.Count <= modelCol) { nCol += wrapLine.Count; nRow = lines; if (nCol == modelCol) { nCol = wrapLine.Count; isRowAndColSet = true; } else if (j == wrappedLines.Count - 1) { nCol = wrapLine.Count - j + modelCol - nCol; isRowAndColSet = true; } } else { int offset = nCol + wrapLine.Count - modelCol; nCol = wrapLine.Count - offset; nRow = lines; isRowAndColSet = true; } } if (!isStartRowAndColSet && modelStartRow == i) { if (nStartCol + wrapLine.Count <= modelStartCol) { nStartCol += wrapLine.Count; nStartRow = lines; if (nStartCol == modelStartCol) { nStartCol = wrapLine.Count; isStartRowAndColSet = true; } else if (j == wrappedLines.Count - 1) { nStartCol = wrapLine.Count - j + modelStartCol - nStartCol; isStartRowAndColSet = true; } } else { int offset = nStartCol + wrapLine.Count - modelStartCol; nStartCol = wrapLine.Count - offset; nStartRow = lines; isStartRowAndColSet = true; } } for (int k = j; k < wrapLine.Count; k++) { Cell cell = wrapLine [k]; cell.Attribute = line [k].Attribute; wrapLine [k] = cell; } wrappedModel.AddLine (lines, wrapLine); sumColWidth += wrapLine.Count; var wrappedLine = new WrappedLine { ModelLine = i, Row = lines, RowIndex = j, ColWidth = wrapLine.Count }; wModelLines.Add (wrappedLine); lines++; } } _wrappedModelLines = wModelLines; return wrappedModel; } private List GetCurrentLine (int row) { return Model.GetLine (row); } private class WrappedLine { public int ColWidth; public int ModelLine; public int Row; public int RowIndex; } }