Skip to content

Commit 900d0c3

Browse files
Selection for wide glyphs (#905)
1 parent 5f07f58 commit 900d0c3

5 files changed

Lines changed: 344 additions & 164 deletions

File tree

src/cascadia/TerminalCore/Terminal.cpp

Lines changed: 0 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -457,50 +457,6 @@ void Terminal::SetBackgroundCallback(std::function<void(const uint32_t)> pfn) no
457457
_pfnBackgroundColorChanged = pfn;
458458
}
459459

460-
// Method Description:
461-
// - Checks if selection is active
462-
// Return Value:
463-
// - bool representing if selection is active. Used to decide copy/paste on right click
464-
const bool Terminal::IsSelectionActive() const noexcept
465-
{
466-
return _selectionActive;
467-
}
468-
469-
// Method Description:
470-
// - Record the position of the beginning of a selection
471-
// Arguments:
472-
// - position: the (x,y) coordinate on the visible viewport
473-
void Terminal::SetSelectionAnchor(const COORD position)
474-
{
475-
_selectionAnchor = position;
476-
477-
// include _scrollOffset here to ensure this maps to the right spot of the original viewport
478-
THROW_IF_FAILED(ShortSub(_selectionAnchor.Y, gsl::narrow<SHORT>(_scrollOffset), &_selectionAnchor.Y));
479-
480-
// copy value of ViewStartIndex to support scrolling
481-
// and update on new buffer output (used in _GetSelectionRects())
482-
_selectionAnchor_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());
483-
484-
_selectionActive = true;
485-
SetEndSelectionPosition(position);
486-
}
487-
488-
// Method Description:
489-
// - Record the position of the end of a selection
490-
// Arguments:
491-
// - position: the (x,y) coordinate on the visible viewport
492-
void Terminal::SetEndSelectionPosition(const COORD position)
493-
{
494-
_endSelectionPosition = position;
495-
496-
// include _scrollOffset here to ensure this maps to the right spot of the original viewport
497-
THROW_IF_FAILED(ShortSub(_endSelectionPosition.Y, gsl::narrow<SHORT>(_scrollOffset), &_endSelectionPosition.Y));
498-
499-
// copy value of ViewStartIndex to support scrolling
500-
// and update on new buffer output (used in _GetSelectionRects())
501-
_endSelectionPosition_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());
502-
}
503-
504460
void Terminal::_InitializeColorTable()
505461
{
506462
gsl::span<COLORREF> tableView = { &_colorTable[0], gsl::narrow<ptrdiff_t>(_colorTable.size()) };
@@ -512,113 +468,6 @@ void Terminal::_InitializeColorTable()
512468
Utils::SetColorTableAlpha(tableView, 0xff);
513469
}
514470

515-
// Method Description:
516-
// - Helper to determine the selected region of the buffer. Used for rendering.
517-
// Return Value:
518-
// - A vector of rectangles representing the regions to select, line by line. They are absolute coordinates relative to the buffer origin.
519-
std::vector<SMALL_RECT> Terminal::_GetSelectionRects() const
520-
{
521-
std::vector<SMALL_RECT> selectionArea;
522-
523-
if (!_selectionActive)
524-
{
525-
return selectionArea;
526-
}
527-
528-
// create these new anchors for comparison and rendering
529-
COORD selectionAnchorWithOffset;
530-
COORD endSelectionPositionWithOffset;
531-
532-
// Add anchor offset here to update properly on new buffer output
533-
THROW_IF_FAILED(ShortAdd(_selectionAnchor.Y, _selectionAnchor_YOffset, &selectionAnchorWithOffset.Y));
534-
THROW_IF_FAILED(ShortAdd(_endSelectionPosition.Y, _endSelectionPosition_YOffset, &endSelectionPositionWithOffset.Y));
535-
536-
// clamp X values to be within buffer bounds
537-
const auto bufferWidth = _buffer->GetSize().RightInclusive();
538-
selectionAnchorWithOffset.X = std::clamp(_selectionAnchor.X, static_cast<SHORT>(0), bufferWidth);
539-
endSelectionPositionWithOffset.X = std::clamp(_endSelectionPosition.X, static_cast<SHORT>(0), bufferWidth);
540-
541-
// NOTE: (0,0) is top-left so vertical comparison is inverted
542-
const COORD& higherCoord = (selectionAnchorWithOffset.Y <= endSelectionPositionWithOffset.Y) ?
543-
selectionAnchorWithOffset :
544-
endSelectionPositionWithOffset;
545-
const COORD& lowerCoord = (selectionAnchorWithOffset.Y > endSelectionPositionWithOffset.Y) ?
546-
selectionAnchorWithOffset :
547-
endSelectionPositionWithOffset;
548-
549-
selectionArea.reserve(lowerCoord.Y - higherCoord.Y + 1);
550-
for (auto row = higherCoord.Y; row <= lowerCoord.Y; row++)
551-
{
552-
SMALL_RECT selectionRow;
553-
554-
selectionRow.Top = row;
555-
selectionRow.Bottom = row;
556-
557-
if (_boxSelection || higherCoord.Y == lowerCoord.Y)
558-
{
559-
selectionRow.Left = std::min(higherCoord.X, lowerCoord.X);
560-
selectionRow.Right = std::max(higherCoord.X, lowerCoord.X);
561-
}
562-
else
563-
{
564-
selectionRow.Left = (row == higherCoord.Y) ? higherCoord.X : 0;
565-
selectionRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : bufferWidth;
566-
}
567-
568-
selectionArea.emplace_back(selectionRow);
569-
}
570-
return selectionArea;
571-
}
572-
573-
// Method Description:
574-
// - enable/disable box selection (ALT + selection)
575-
// Arguments:
576-
// - isEnabled: new value for _boxSelection
577-
void Terminal::SetBoxSelection(const bool isEnabled) noexcept
578-
{
579-
_boxSelection = isEnabled;
580-
}
581-
582-
// Method Description:
583-
// - clear selection data and disable rendering it
584-
void Terminal::ClearSelection() noexcept
585-
{
586-
_selectionActive = false;
587-
_selectionAnchor = { 0, 0 };
588-
_endSelectionPosition = { 0, 0 };
589-
_selectionAnchor_YOffset = 0;
590-
_endSelectionPosition_YOffset = 0;
591-
592-
_buffer->GetRenderTarget().TriggerSelection();
593-
}
594-
595-
// Method Description:
596-
// - get wstring text from highlighted portion of text buffer
597-
// Arguments:
598-
// - trimTrailingWhitespace: enable removing any whitespace from copied selection
599-
// and get text to appear on separate lines.
600-
// Return Value:
601-
// - wstring text from buffer. If extended to multiple lines, each line is separated by \r\n
602-
const std::wstring Terminal::RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace) const
603-
{
604-
std::function<COLORREF(TextAttribute&)> GetForegroundColor = std::bind(&Terminal::GetForegroundColor, this, std::placeholders::_1);
605-
std::function<COLORREF(TextAttribute&)> GetBackgroundColor = std::bind(&Terminal::GetBackgroundColor, this, std::placeholders::_1);
606-
607-
auto data = _buffer->GetTextForClipboard(!_boxSelection,
608-
trimTrailingWhitespace,
609-
_GetSelectionRects(),
610-
GetForegroundColor,
611-
GetBackgroundColor);
612-
613-
std::wstring result;
614-
for (const auto& text : data.text)
615-
{
616-
result += text;
617-
}
618-
619-
return result;
620-
}
621-
622471
// Method Description:
623472
// - Sets the visibility of the text cursor.
624473
// Arguments:

src/cascadia/TerminalCore/Terminal.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ class Microsoft::Terminal::Core::Terminal final :
118118
bool IsCursorBlinkingAllowed() const noexcept;
119119

120120
#pragma region TextSelection
121+
// These methods are defined in TerminalSelection.cpp
121122
const bool IsSelectionActive() const noexcept;
122123
void SetSelectionAnchor(const COORD position);
123124
void SetEndSelectionPosition(const COORD position);
@@ -188,5 +189,10 @@ class Microsoft::Terminal::Core::Terminal final :
188189

189190
void _NotifyScrollEvent();
190191

192+
#pragma region TextSelection
193+
// These methods are defined in TerminalSelection.cpp
191194
std::vector<SMALL_RECT> _GetSelectionRects() const;
195+
const SHORT _ExpandWideGlyphSelectionLeft(const SHORT xPos, const SHORT yPos) const noexcept;
196+
const SHORT _ExpandWideGlyphSelectionRight(const SHORT xPos, const SHORT yPos) const noexcept;
197+
#pragma endregion
192198
};
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
#include "pch.h"
5+
#include "Terminal.hpp"
6+
7+
using namespace Microsoft::Terminal::Core;
8+
9+
// Method Description:
10+
// - Helper to determine the selected region of the buffer. Used for rendering.
11+
// Return Value:
12+
// - A vector of rectangles representing the regions to select, line by line. They are absolute coordinates relative to the buffer origin.
13+
std::vector<SMALL_RECT> Terminal::_GetSelectionRects() const
14+
{
15+
std::vector<SMALL_RECT> selectionArea;
16+
17+
if (!_selectionActive)
18+
{
19+
return selectionArea;
20+
}
21+
22+
// create these new anchors for comparison and rendering
23+
COORD selectionAnchorWithOffset;
24+
COORD endSelectionPositionWithOffset;
25+
26+
// Add anchor offset here to update properly on new buffer output
27+
THROW_IF_FAILED(ShortAdd(_selectionAnchor.Y, _selectionAnchor_YOffset, &selectionAnchorWithOffset.Y));
28+
THROW_IF_FAILED(ShortAdd(_endSelectionPosition.Y, _endSelectionPosition_YOffset, &endSelectionPositionWithOffset.Y));
29+
30+
// clamp X values to be within buffer bounds
31+
const auto bufferWidth = _buffer->GetSize().RightInclusive();
32+
selectionAnchorWithOffset.X = std::clamp(_selectionAnchor.X, static_cast<SHORT>(0), bufferWidth);
33+
endSelectionPositionWithOffset.X = std::clamp(_endSelectionPosition.X, static_cast<SHORT>(0), bufferWidth);
34+
35+
// NOTE: (0,0) is top-left so vertical comparison is inverted
36+
const COORD& higherCoord = (selectionAnchorWithOffset.Y <= endSelectionPositionWithOffset.Y) ?
37+
selectionAnchorWithOffset :
38+
endSelectionPositionWithOffset;
39+
const COORD& lowerCoord = (selectionAnchorWithOffset.Y > endSelectionPositionWithOffset.Y) ?
40+
selectionAnchorWithOffset :
41+
endSelectionPositionWithOffset;
42+
43+
selectionArea.reserve(lowerCoord.Y - higherCoord.Y + 1);
44+
for (auto row = higherCoord.Y; row <= lowerCoord.Y; row++)
45+
{
46+
SMALL_RECT selectionRow;
47+
48+
selectionRow.Top = row;
49+
selectionRow.Bottom = row;
50+
51+
if (_boxSelection || higherCoord.Y == lowerCoord.Y)
52+
{
53+
selectionRow.Left = std::min(higherCoord.X, lowerCoord.X);
54+
selectionRow.Right = std::max(higherCoord.X, lowerCoord.X);
55+
}
56+
else
57+
{
58+
selectionRow.Left = (row == higherCoord.Y) ? higherCoord.X : 0;
59+
selectionRow.Right = (row == lowerCoord.Y) ? lowerCoord.X : bufferWidth;
60+
}
61+
62+
selectionRow.Left = _ExpandWideGlyphSelectionLeft(selectionRow.Left, row);
63+
selectionRow.Right = _ExpandWideGlyphSelectionRight(selectionRow.Right, row);
64+
65+
selectionArea.emplace_back(selectionRow);
66+
}
67+
return selectionArea;
68+
}
69+
70+
// Method Description:
71+
// - Expands the selection left-wards to cover a wide glyph, if necessary
72+
// Arguments:
73+
// - position: the (x,y) coordinate on the visible viewport
74+
// Return Value:
75+
// - updated x position to encapsulate the wide glyph
76+
const SHORT Terminal::_ExpandWideGlyphSelectionLeft(const SHORT xPos, const SHORT yPos) const noexcept
77+
{
78+
// don't change the value if at/outside the boundary
79+
if (xPos <= 0 || xPos >= _buffer->GetSize().RightInclusive())
80+
{
81+
return xPos;
82+
}
83+
84+
COORD position{ xPos, yPos };
85+
const auto attr = _buffer->GetCellDataAt(position)->DbcsAttr();
86+
if (attr.IsTrailing())
87+
{
88+
// move off by highlighting the lead half too.
89+
// alters position.X
90+
_mutableViewport.DecrementInBounds(position);
91+
}
92+
return position.X;
93+
}
94+
95+
// Method Description:
96+
// - Expands the selection right-wards to cover a wide glyph, if necessary
97+
// Arguments:
98+
// - position: the (x,y) coordinate on the visible viewport
99+
// Return Value:
100+
// - updated x position to encapsulate the wide glyph
101+
const SHORT Terminal::_ExpandWideGlyphSelectionRight(const SHORT xPos, const SHORT yPos) const noexcept
102+
{
103+
// don't change the value if at/outside the boundary
104+
if (xPos <= 0 || xPos >= _buffer->GetSize().RightInclusive())
105+
{
106+
return xPos;
107+
}
108+
109+
COORD position{ xPos, yPos };
110+
const auto attr = _buffer->GetCellDataAt(position)->DbcsAttr();
111+
if (attr.IsLeading())
112+
{
113+
// move off by highlighting the trailing half too.
114+
// alters position.X
115+
_mutableViewport.IncrementInBounds(position);
116+
}
117+
return position.X;
118+
}
119+
120+
// Method Description:
121+
// - Checks if selection is active
122+
// Return Value:
123+
// - bool representing if selection is active. Used to decide copy/paste on right click
124+
const bool Terminal::IsSelectionActive() const noexcept
125+
{
126+
return _selectionActive;
127+
}
128+
129+
// Method Description:
130+
// - Record the position of the beginning of a selection
131+
// Arguments:
132+
// - position: the (x,y) coordinate on the visible viewport
133+
void Terminal::SetSelectionAnchor(const COORD position)
134+
{
135+
_selectionAnchor = position;
136+
137+
// include _scrollOffset here to ensure this maps to the right spot of the original viewport
138+
THROW_IF_FAILED(ShortSub(_selectionAnchor.Y, gsl::narrow<SHORT>(_scrollOffset), &_selectionAnchor.Y));
139+
140+
// copy value of ViewStartIndex to support scrolling
141+
// and update on new buffer output (used in _GetSelectionRects())
142+
_selectionAnchor_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());
143+
144+
_selectionActive = true;
145+
SetEndSelectionPosition(position);
146+
}
147+
148+
// Method Description:
149+
// - Record the position of the end of a selection
150+
// Arguments:
151+
// - position: the (x,y) coordinate on the visible viewport
152+
void Terminal::SetEndSelectionPosition(const COORD position)
153+
{
154+
_endSelectionPosition = position;
155+
156+
// include _scrollOffset here to ensure this maps to the right spot of the original viewport
157+
THROW_IF_FAILED(ShortSub(_endSelectionPosition.Y, gsl::narrow<SHORT>(_scrollOffset), &_endSelectionPosition.Y));
158+
159+
// copy value of ViewStartIndex to support scrolling
160+
// and update on new buffer output (used in _GetSelectionRects())
161+
_endSelectionPosition_YOffset = gsl::narrow<SHORT>(_ViewStartIndex());
162+
}
163+
164+
// Method Description:
165+
// - enable/disable box selection (ALT + selection)
166+
// Arguments:
167+
// - isEnabled: new value for _boxSelection
168+
void Terminal::SetBoxSelection(const bool isEnabled) noexcept
169+
{
170+
_boxSelection = isEnabled;
171+
}
172+
173+
// Method Description:
174+
// - clear selection data and disable rendering it
175+
void Terminal::ClearSelection() noexcept
176+
{
177+
_selectionActive = false;
178+
_selectionAnchor = { 0, 0 };
179+
_endSelectionPosition = { 0, 0 };
180+
_selectionAnchor_YOffset = 0;
181+
_endSelectionPosition_YOffset = 0;
182+
183+
_buffer->GetRenderTarget().TriggerSelection();
184+
}
185+
186+
// Method Description:
187+
// - get wstring text from highlighted portion of text buffer
188+
// Arguments:
189+
// - trimTrailingWhitespace: enable removing any whitespace from copied selection
190+
// and get text to appear on separate lines.
191+
// Return Value:
192+
// - wstring text from buffer. If extended to multiple lines, each line is separated by \r\n
193+
const std::wstring Terminal::RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace) const
194+
{
195+
std::function<COLORREF(TextAttribute&)> GetForegroundColor = std::bind(&Terminal::GetForegroundColor, this, std::placeholders::_1);
196+
std::function<COLORREF(TextAttribute&)> GetBackgroundColor = std::bind(&Terminal::GetBackgroundColor, this, std::placeholders::_1);
197+
198+
auto data = _buffer->GetTextForClipboard(!_boxSelection,
199+
trimTrailingWhitespace,
200+
_GetSelectionRects(),
201+
GetForegroundColor,
202+
GetBackgroundColor);
203+
204+
std::wstring result;
205+
for (const auto& text : data.text)
206+
{
207+
result += text;
208+
}
209+
210+
return result;
211+
}

0 commit comments

Comments
 (0)