diff --git a/ui/README.md b/ui/README.md index fd8ec266..7c2716b1 100644 --- a/ui/README.md +++ b/ui/README.md @@ -80,6 +80,7 @@ - 调整了 `TreeView` 各层级节点背景框的左侧起点:子节点的圆角矩形现在会随缩进一起右移,更贴合本层内容的实际包裹范围 - 为 `TreeView` 补充了竖向层级引导线:祖先层级列和展开节点的子树主干线现在都会显示,层级结构更直观 - 微调了 `TreeView` 每个节点左侧的内部留白和引导线位置,让竖向层级线与下一层级节点边框之间留出更自然的间距 +- 单行 `TextBox` 现在支持基于光标位置的横向自动滚动:当文本向右溢出时,左右箭头、输入插入、删除和鼠标定位都会自动把当前 caret 滚动到可见区域 ## 构建 diff --git a/ui/src/TextBox.cpp b/ui/src/TextBox.cpp index 84a5c11c..b3329c35 100644 --- a/ui/src/TextBox.cpp +++ b/ui/src/TextBox.cpp @@ -113,6 +113,7 @@ void TextBox::Arrange(const RectF &finalRect) { RECT rc = ComputeEditorHostRect(); editor_->SetHostRect(rc); + editor_->SetSingleLine(preferredHeight_ <= 100.0f ? TRUE : FALSE); editor_->SetContentPadding({}); editor_->UpdateLayout(); editor_->SetContentPadding(ComputeEditorContentPadding()); diff --git a/ui/src/tsf/TextEditor.cpp b/ui/src/tsf/TextEditor.cpp index 02928fa4..32d99385 100644 --- a/ui/src/tsf/TextEditor.cpp +++ b/ui/src/tsf/TextEditor.cpp @@ -24,6 +24,7 @@ void CTextEditor::MoveSelection(UINT nSelStart, UINT nSelEnd) _nSelStart = nSelStart; _nSelEnd = nSelEnd; _layout.ResetCaretBlink(); + _layout.EnsureCaretVisible(_nSelEnd); _pTextStore->OnSelectionChange(); } @@ -149,6 +150,7 @@ BOOL CTextEditor::InsertAtSelection(LPCWSTR psz) _nSelEnd = _nSelStart; _layout.ResetCaretBlink(); UpdateLayout(); + _layout.EnsureCaretVisible(_nSelEnd); _pTextStore->OnTextChange(_nSelStart, lOldSelEnd, _nSelEnd); _pTextStore->OnSelectionChange(); @@ -177,6 +179,7 @@ BOOL CTextEditor::DeleteAtSelection(BOOL fBack) _layout.ResetCaretBlink(); UpdateLayout(); + _layout.EnsureCaretVisible(_nSelEnd); _pTextStore->OnTextChange(_nSelEnd, _nSelEnd + 1, _nSelEnd); } @@ -189,6 +192,7 @@ BOOL CTextEditor::DeleteAtSelection(BOOL fBack) _nSelEnd = _nSelStart; _layout.ResetCaretBlink(); UpdateLayout(); + _layout.EnsureCaretVisible(_nSelEnd); _pTextStore->OnTextChange(_nSelStart, _nSelStart + 1, _nSelStart); _pTextStore->OnSelectionChange(); @@ -211,6 +215,7 @@ BOOL CTextEditor::DeleteSelection() _nSelEnd = _nSelStart; _layout.ResetCaretBlink(); UpdateLayout(); + _layout.EnsureCaretVisible(_nSelEnd); _pTextStore->OnTextChange(_nSelStart, nSelOldEnd, _nSelStart); _pTextStore->OnSelectionChange(); @@ -276,7 +281,8 @@ void CTextEditor::UpdateLayout() const LONG width = max(_rcHost.right - _rcHost.left, 1L); _layout.Layout(GetTextBuffer(), GetTextLength(), &_lfCurrentFont, static_cast(width), dpi, dpi, static_cast(_rcContentPadding.left), static_cast(_rcContentPadding.top), - static_cast(_rcContentPadding.right), static_cast(_rcContentPadding.bottom)); + static_cast(_rcContentPadding.right), static_cast(_rcContentPadding.bottom), _singleLine); + _layout.EnsureCaretVisible(_nSelEnd); } void CTextEditor::NotifyLayoutChange() diff --git a/ui/src/tsf/TextEditor.h b/ui/src/tsf/TextEditor.h index 5e0f4099..4b6bdbae 100644 --- a/ui/src/tsf/TextEditor.h +++ b/ui/src/tsf/TextEditor.h @@ -62,6 +62,10 @@ class CTextEditor : public CTextContainer { _rcContentPadding = rcPadding; } + void SetSingleLine(BOOL singleLine) + { + _singleLine = singleLine; + } UINT GetSelectionStart() { @@ -134,6 +138,7 @@ class CTextEditor : public CTextContainer HWND _hwnd; RECT _rcHost = {}; RECT _rcContentPadding = {}; + BOOL _singleLine = FALSE; CTextLayout _layout; LOGFONT _lfCurrentFont; diff --git a/ui/src/tsf/TextLayout.cpp b/ui/src/tsf/TextLayout.cpp index 75537cb1..7a2f6f40 100644 --- a/ui/src/tsf/TextLayout.cpp +++ b/ui/src/tsf/TextLayout.cpp @@ -79,7 +79,7 @@ BOOL CTextLayout::EnsureTextFormat(const LOGFONT *plf, FLOAT dpiY) BOOL CTextLayout::Layout(const WCHAR *psz, UINT nCnt, const LOGFONT *plf, FLOAT layoutWidthPixels, FLOAT dpiX, FLOAT dpiY, FLOAT paddingLeftPixels, FLOAT paddingTopPixels, FLOAT paddingRightPixels, - FLOAT paddingBottomPixels) + FLOAT paddingBottomPixels, BOOL singleLine) { Clear(); @@ -94,6 +94,7 @@ BOOL CTextLayout::Layout(const WCHAR *psz, UINT nCnt, const LOGFONT *plf, FLOAT _paddingTopDips = PixelsToDipsY(kEditorPaddingPixels + paddingTopPixels); _paddingRightDips = PixelsToDipsX(kEditorPaddingPixels + paddingRightPixels); _paddingBottomDips = PixelsToDipsY(kEditorPaddingPixels + paddingBottomPixels); + _singleLine = singleLine; _layoutWidth = max(PixelsToDipsX(layoutWidthPixels) - _paddingLeftDips - _paddingRightDips, 1.0f); const WCHAR *layoutText = psz ? psz : L""; @@ -112,7 +113,7 @@ BOOL CTextLayout::Layout(const WCHAR *psz, UINT nCnt, const LOGFONT *plf, FLOAT return FALSE; } - _pTextLayout->SetWordWrapping(DWRITE_WORD_WRAPPING_WRAP); + _pTextLayout->SetWordWrapping(_singleLine ? DWRITE_WORD_WRAPPING_NO_WRAP : DWRITE_WORD_WRAPPING_WRAP); DWRITE_TEXT_METRICS textMetrics = {}; if (FAILED(_pTextLayout->GetMetrics(&textMetrics))) @@ -280,7 +281,9 @@ BOOL CTextLayout::Render(ID2D1HwndRenderTarget *pRenderTarget, const WCHAR *psz, for (UINT j = nSelStartInLine; j < nSelEndInLine; j++) { const D2D1_RECT_F &rc = line.prgCharInfo[j].rc; - pRenderTarget->FillRectangle(D2D1::RectF(rc.left, rc.top, rc.right, rc.bottom), pSelectionBrush); + const FLOAT scrollX = _singleLine ? _horizontalScrollDips : 0.0f; + pRenderTarget->FillRectangle(D2D1::RectF(rc.left - scrollX, rc.top, rc.right - scrollX, rc.bottom), + pSelectionBrush); } } } @@ -288,7 +291,9 @@ BOOL CTextLayout::Render(ID2D1HwndRenderTarget *pRenderTarget, const WCHAR *psz, if (_pTextLayout && nCnt > 0) { - pRenderTarget->DrawTextLayout(D2D1::Point2F(_paddingLeftDips, _paddingTopDips), _pTextLayout, pTextBrush, + pRenderTarget->DrawTextLayout(D2D1::Point2F(_paddingLeftDips - (_singleLine ? _horizontalScrollDips : 0.0f), + _paddingTopDips), + _pTextLayout, pTextBrush, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT); } @@ -318,7 +323,9 @@ BOOL CTextLayout::Render(ID2D1HwndRenderTarget *pRenderTarget, const WCHAR *psz, if (composition.da.crBk.type != TF_CT_NONE) { pCompositionBrush->SetColor(GetAttributeColor(&composition.da.crBk, GetSysColor(COLOR_WINDOW))); - pRenderTarget->FillRectangle(D2D1::RectF(rc.left, rc.top, rc.right, rc.bottom), pCompositionBrush); + const FLOAT scrollX = _singleLine ? _horizontalScrollDips : 0.0f; + pRenderTarget->FillRectangle(D2D1::RectF(rc.left - scrollX, rc.top, rc.right - scrollX, rc.bottom), + pCompositionBrush); } WCHAR ch = psz[line.nPos + k]; @@ -329,8 +336,9 @@ BOOL CTextLayout::Render(ID2D1HwndRenderTarget *pRenderTarget, const WCHAR *psz, } pCompositionBrush->SetColor(compositionTextColor); + const FLOAT scrollX = _singleLine ? _horizontalScrollDips : 0.0f; pRenderTarget->DrawTextW(&ch, 1, _pTextFormat, - D2D1::RectF(rc.left, rc.top, rc.right + PixelsToDipsX(2.0f), rc.bottom), + D2D1::RectF(rc.left - scrollX, rc.top, rc.right - scrollX + PixelsToDipsX(2.0f), rc.bottom), pCompositionBrush, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT, DWRITE_MEASURING_MODE_NATURAL); @@ -433,6 +441,21 @@ BOOL CTextLayout::RectFromCharPos(UINT nPos, RECT *prc) } BOOL CTextLayout::RectFromCharPosDips(UINT nPos, D2D1_RECT_F *prc) +{ + if (!RectFromCharPosDipsRaw(nPos, prc)) + { + return FALSE; + } + + if (_singleLine) + { + prc->left -= _horizontalScrollDips; + prc->right -= _horizontalScrollDips; + } + return TRUE; +} + +BOOL CTextLayout::RectFromCharPosDipsRaw(UINT nPos, D2D1_RECT_F *prc) { *prc = D2D1::RectF(); @@ -481,7 +504,7 @@ BOOL CTextLayout::RectFromCharPosDips(UINT nPos, D2D1_RECT_F *prc) UINT CTextLayout::CharPosFromPoint(POINT pt) { - const FLOAT dipX = PixelsToDipsX(static_cast(pt.x)); + const FLOAT dipX = PixelsToDipsX(static_cast(pt.x)) + (_singleLine ? _horizontalScrollDips : 0.0f); const FLOAT dipY = PixelsToDipsY(static_cast(pt.y)); for (UINT i = 0; i < _nLineCnt; i++) { @@ -515,7 +538,7 @@ UINT CTextLayout::InsertionIndexFromPoint(POINT pt) return 0; } - const FLOAT dipX = PixelsToDipsX(static_cast(pt.x)); + const FLOAT dipX = PixelsToDipsX(static_cast(pt.x)) + (_singleLine ? _horizontalScrollDips : 0.0f); const FLOAT dipY = PixelsToDipsY(static_cast(pt.y)); for (UINT i = 0; i < _nLineCnt; i++) @@ -576,7 +599,7 @@ UINT CTextLayout::InsertionIndexFromPoint(POINT pt) UINT CTextLayout::ExactCharPosFromPoint(POINT pt) { - const FLOAT dipX = PixelsToDipsX(static_cast(pt.x)); + const FLOAT dipX = PixelsToDipsX(static_cast(pt.x)) + (_singleLine ? _horizontalScrollDips : 0.0f); const FLOAT dipY = PixelsToDipsY(static_cast(pt.y)); for (UINT i = 0; i < _nLineCnt; i++) { @@ -609,6 +632,36 @@ UINT CTextLayout::FineFirstEndCharPosInLine(UINT uCurPos, BOOL bFirst) return static_cast(-1); } +void CTextLayout::EnsureCaretVisible(UINT nPos) +{ + if (!_singleLine) + { + _horizontalScrollDips = 0.0f; + return; + } + + D2D1_RECT_F rawCaret = {}; + if (!RectFromCharPosDipsRaw(nPos, &rawCaret)) + { + return; + } + + const FLOAT caretWidth = PixelsToDipsX(kCaretWidthPixels); + const FLOAT visibleLeft = _paddingLeftDips; + const FLOAT visibleRight = _paddingLeftDips + _layoutWidth; + const FLOAT displayLeft = rawCaret.left - _horizontalScrollDips; + const FLOAT displayRight = max(rawCaret.right, rawCaret.left + caretWidth) - _horizontalScrollDips; + + if (displayLeft < visibleLeft) + { + _horizontalScrollDips = max(rawCaret.left - visibleLeft, 0.0f); + } + else if (displayRight > visibleRight) + { + _horizontalScrollDips = max(max(rawCaret.right, rawCaret.left + caretWidth) - visibleRight, 0.0f); + } +} + void CTextLayout::Clear() { if (_pTextLayout) diff --git a/ui/src/tsf/TextLayout.h b/ui/src/tsf/TextLayout.h index b763e501..800fe508 100644 --- a/ui/src/tsf/TextLayout.h +++ b/ui/src/tsf/TextLayout.h @@ -67,6 +67,8 @@ class CTextLayout _paddingTopDips = 0.0f; _paddingRightDips = 0.0f; _paddingBottomDips = 0.0f; + _horizontalScrollDips = 0.0f; + _singleLine = FALSE; _rcCaret = D2D1::RectF(); } @@ -86,7 +88,7 @@ class CTextLayout BOOL Initialize(IDWriteFactory *pDWriteFactory); BOOL Layout(const WCHAR *psz, UINT nCnt, const LOGFONT *plf, FLOAT layoutWidthPixels, FLOAT dpiX, FLOAT dpiY, FLOAT paddingLeftPixels = 0.0f, FLOAT paddingTopPixels = 0.0f, FLOAT paddingRightPixels = 0.0f, - FLOAT paddingBottomPixels = 0.0f); + FLOAT paddingBottomPixels = 0.0f, BOOL singleLine = FALSE); BOOL Render(ID2D1HwndRenderTarget *pRenderTarget, const WCHAR *psz, UINT nCnt, UINT nSelStart, UINT nSelEnd, const COMPOSITIONRENDERINFO *pCompositionRenderInfo, UINT nCompositionRenderInfo); BOOL RectFromCharPos(UINT nPos, RECT *prc); @@ -94,6 +96,7 @@ class CTextLayout UINT InsertionIndexFromPoint(POINT pt); UINT ExactCharPosFromPoint(POINT pt); UINT FineFirstEndCharPosInLine(UINT uCurPos, BOOL bFirst); + void EnsureCaretVisible(UINT nPos); void ToggleCaretBlink(); void ResetCaretBlink(); void SetCaretVisible(BOOL fVisible) @@ -130,6 +133,7 @@ class CTextLayout private: BOOL EnsureTextFormat(const LOGFONT *plf, FLOAT dpiY); BOOL RectFromCharPosDips(UINT nPos, D2D1_RECT_F *prc); + BOOL RectFromCharPosDipsRaw(UINT nPos, D2D1_RECT_F *prc); FLOAT PixelsToDipsX(FLOAT value) const; FLOAT PixelsToDipsY(FLOAT value) const; LONG DipsToPixelsX(FLOAT value) const; @@ -152,6 +156,8 @@ class CTextLayout FLOAT _paddingTopDips; FLOAT _paddingRightDips; FLOAT _paddingBottomDips; + FLOAT _horizontalScrollDips; + BOOL _singleLine; LINEINFO *_prgLines; UINT _nLineCnt;