-
Notifications
You must be signed in to change notification settings - Fork 8.1k
/
paint.cpp
803 lines (662 loc) · 31.2 KB
/
paint.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include <vector>
#include "gdirenderer.hpp"
#include "../inc/unicode.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Render;
// Routine Description:
// - Prepares internal structures for a painting operation.
// Arguments:
// - <none>
// Return Value:
// - S_OK if we started to paint. S_FALSE if we didn't need to paint. HRESULT error code if painting didn't start successfully.
[[nodiscard]] HRESULT GdiEngine::StartPaint() noexcept
{
// If we have no handle, we don't need to paint. Return quickly.
RETURN_HR_IF(S_FALSE, !_IsWindowValid());
// If we're already painting, we don't need to paint. Return quickly.
RETURN_HR_IF(S_FALSE, _fPaintStarted);
// If the window we're painting on is invisible, we don't need to paint. Return quickly.
// If the title changed, we will need to try and paint this frame. This will
// make sure the window's title is updated, even if the window isn't visible.
RETURN_HR_IF(S_FALSE, (!IsWindowVisible(_hwndTargetWindow) && !_titleChanged));
// At the beginning of a new frame, we have 0 lines ready for painting in PolyTextOut
_cPolyText = 0;
// Prepare our in-memory bitmap for double-buffered composition.
RETURN_IF_FAILED(_PrepareMemoryBitmap(_hwndTargetWindow));
// We must use Get and Release DC because BeginPaint/EndPaint can only be called in response to a WM_PAINT message (and may hang otherwise)
// We'll still use the PAINTSTRUCT for information because it's convenient.
_psInvalidData.hdc = GetDC(_hwndTargetWindow);
RETURN_HR_IF_NULL(E_FAIL, _psInvalidData.hdc);
// We need the advanced graphics mode in order to set a transform.
SetGraphicsMode(_psInvalidData.hdc, GM_ADVANCED);
// Signal that we're starting to paint.
_fPaintStarted = true;
_psInvalidData.fErase = TRUE;
_psInvalidData.rcPaint = _rcInvalid;
#if DBG
_debugContext = GetDC(_debugWindow);
#endif
return S_OK;
}
// Routine Description:
// - Scrolls the existing data on the in-memory frame by the scroll region
// deltas we have collectively received through the Invalidate methods
// since the last time this was called.
// Arguments:
// - <none>
// Return Value:
// - S_OK, suitable GDI HRESULT error, error from Win32 windowing, or safemath error.
[[nodiscard]] HRESULT GdiEngine::ScrollFrame() noexcept
{
// If we don't have any scrolling to do, return early.
RETURN_HR_IF(S_OK, 0 == _szInvalidScroll.cx && 0 == _szInvalidScroll.cy);
// If we have an inverted cursor, we have to see if we have to clean it before we scroll to prevent
// left behind cursor copies in the scrolled region.
if (cursorInvertRects.size() > 0)
{
// We first need to apply the transform that was active at the time the cursor
// was rendered otherwise we won't be clearing the right area of the display.
// We don't need to do this if it was an identity transform though.
const bool identityTransform = cursorInvertTransform == IDENTITY_XFORM;
if (!identityTransform)
{
LOG_HR_IF(E_FAIL, !SetWorldTransform(_hdcMemoryContext, &cursorInvertTransform));
LOG_HR_IF(E_FAIL, !SetWorldTransform(_psInvalidData.hdc, &cursorInvertTransform));
}
for (RECT r : cursorInvertRects)
{
// Clean both the in-memory and actual window context.
LOG_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r)));
LOG_HR_IF(E_FAIL, !(InvertRect(_psInvalidData.hdc, &r)));
}
// If we've applied a transform, then we need to reset it.
if (!identityTransform)
{
LOG_HR_IF(E_FAIL, !ModifyWorldTransform(_hdcMemoryContext, nullptr, MWT_IDENTITY));
LOG_HR_IF(E_FAIL, !ModifyWorldTransform(_psInvalidData.hdc, nullptr, MWT_IDENTITY));
}
cursorInvertRects.clear();
}
// We have to limit the region that can be scrolled to not include the gutters.
// Gutters are defined as sub-character width pixels at the bottom or right of the screen.
COORD const coordFontSize = _GetFontSize();
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0);
SIZE szGutter;
szGutter.cx = _szMemorySurface.cx % coordFontSize.X;
szGutter.cy = _szMemorySurface.cy % coordFontSize.Y;
RECT rcScrollLimit = { 0 };
RETURN_IF_FAILED(LongSub(_szMemorySurface.cx, szGutter.cx, &rcScrollLimit.right));
RETURN_IF_FAILED(LongSub(_szMemorySurface.cy, szGutter.cy, &rcScrollLimit.bottom));
// Scroll real window and memory buffer in-sync.
LOG_LAST_ERROR_IF(!ScrollWindowEx(_hwndTargetWindow,
_szInvalidScroll.cx,
_szInvalidScroll.cy,
&rcScrollLimit,
&rcScrollLimit,
nullptr,
nullptr,
0));
RECT rcUpdate = { 0 };
LOG_HR_IF(E_FAIL, !(ScrollDC(_hdcMemoryContext, _szInvalidScroll.cx, _szInvalidScroll.cy, &rcScrollLimit, &rcScrollLimit, nullptr, &rcUpdate)));
LOG_IF_FAILED(_InvalidCombine(&rcUpdate));
// update invalid rect for the remainder of paint functions
_psInvalidData.rcPaint = _rcInvalid;
return S_OK;
}
// Routine Description:
// - BeginPaint helper to prepare the in-memory bitmap for double-buffering
// Arguments:
// - hwnd - Window handle to use for the DC properties when creating a memory DC and for checking the client area size.
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::_PrepareMemoryBitmap(const HWND hwnd) noexcept
{
RECT rcClient;
RETURN_HR_IF(E_FAIL, !(GetClientRect(hwnd, &rcClient)));
SIZE const szClient = _GetRectSize(&rcClient);
// Only do work if the existing memory surface is a different size from the client area.
// Return quickly if they're the same.
RETURN_HR_IF(S_OK, _szMemorySurface.cx == szClient.cx && _szMemorySurface.cy == szClient.cy);
wil::unique_hdc hdcRealWindow(GetDC(_hwndTargetWindow));
RETURN_HR_IF_NULL(E_FAIL, hdcRealWindow.get());
// If we already had a bitmap, Blt the old one onto the new one and clean up the old one.
if (nullptr != _hbitmapMemorySurface)
{
// Make a temporary DC for us to Blt with.
wil::unique_hdc hdcTemp(CreateCompatibleDC(hdcRealWindow.get()));
RETURN_HR_IF_NULL(E_FAIL, hdcTemp.get());
// Make the new bitmap we'll use going forward with the new size.
wil::unique_hbitmap hbitmapNew(CreateCompatibleBitmap(hdcRealWindow.get(), szClient.cx, szClient.cy));
RETURN_HR_IF_NULL(E_FAIL, hbitmapNew.get());
// Select it into the DC, but hold onto the junky one pixel bitmap (made by default) to give back when we need to Delete.
wil::unique_hbitmap hbitmapOnePixelJunk(SelectBitmap(hdcTemp.get(), hbitmapNew.get()));
RETURN_HR_IF_NULL(E_FAIL, hbitmapOnePixelJunk.get());
hbitmapNew.release(); // if SelectBitmap worked, GDI took ownership. Detach from smart object.
// Blt from the DC/bitmap we're already holding onto into the new one.
RETURN_HR_IF(E_FAIL, !(BitBlt(hdcTemp.get(), 0, 0, _szMemorySurface.cx, _szMemorySurface.cy, _hdcMemoryContext, 0, 0, SRCCOPY)));
// Put the junky bitmap back into the temp DC and get our new one out.
hbitmapNew.reset(SelectBitmap(hdcTemp.get(), hbitmapOnePixelJunk.get()));
RETURN_HR_IF_NULL(E_FAIL, hbitmapNew.get());
hbitmapOnePixelJunk.release(); // if SelectBitmap worked, GDI took ownership. Detach from smart object.
// Move our new bitmap into the long-standing DC we're holding onto.
wil::unique_hbitmap hbitmapOld(SelectBitmap(_hdcMemoryContext, hbitmapNew.get()));
RETURN_HR_IF_NULL(E_FAIL, hbitmapOld.get());
// Now save a pointer to our new bitmap into the class state.
_hbitmapMemorySurface = hbitmapNew.release(); // and prevent it from being freed now that GDI is holding onto it as well.
}
else
{
_hbitmapMemorySurface = CreateCompatibleBitmap(hdcRealWindow.get(), szClient.cx, szClient.cy);
RETURN_HR_IF_NULL(E_FAIL, _hbitmapMemorySurface);
wil::unique_hbitmap hOldBitmap(SelectBitmap(_hdcMemoryContext, _hbitmapMemorySurface)); // DC has a default junk bitmap, take it and delete it.
RETURN_HR_IF_NULL(E_FAIL, hOldBitmap.get());
}
// Save the new client size.
_szMemorySurface = szClient;
return S_OK;
}
// Routine Description:
// - EndPaint helper to perform the final BitBlt copy from the memory bitmap onto the final window bitmap (double-buffering.) Also cleans up structures used while painting.
// Arguments:
// - <none>
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::EndPaint() noexcept
{
// If we try to end a paint that wasn't started, it's invalid. Return.
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), !(_fPaintStarted));
LOG_IF_FAILED(_FlushBufferLines());
POINT const pt = _GetInvalidRectPoint();
SIZE const sz = _GetInvalidRectSize();
LOG_HR_IF(E_FAIL, !(BitBlt(_psInvalidData.hdc, pt.x, pt.y, sz.cx, sz.cy, _hdcMemoryContext, pt.x, pt.y, SRCCOPY)));
WHEN_DBG(_DebugBltAll());
_rcInvalid = { 0 };
_fInvalidRectUsed = false;
_szInvalidScroll = { 0 };
LOG_HR_IF(E_FAIL, !(GdiFlush()));
LOG_HR_IF(E_FAIL, !(ReleaseDC(_hwndTargetWindow, _psInvalidData.hdc)));
_psInvalidData.hdc = nullptr;
_fPaintStarted = false;
#if DBG
ReleaseDC(_debugWindow, _debugContext);
_debugContext = nullptr;
#endif
return S_OK;
}
// Routine Description:
// - Used to perform longer running presentation steps outside the lock so the other threads can continue.
// - Not currently used by GdiEngine.
// Arguments:
// - <none>
// Return Value:
// - S_FALSE since we do nothing.
[[nodiscard]] HRESULT GdiEngine::Present() noexcept
{
return S_FALSE;
}
// Routine Description:
// - Fills the given rectangle with the background color on the drawing context.
// Arguments:
// - prc - Rectangle to fill with color
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::_PaintBackgroundColor(const RECT* const prc) noexcept
{
wil::unique_hbrush hbr(GetStockBrush(DC_BRUSH));
RETURN_HR_IF_NULL(E_FAIL, hbr.get());
WHEN_DBG(_PaintDebugRect(prc));
LOG_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, prc, hbr.get())));
WHEN_DBG(_DoDebugBlt(prc));
return S_OK;
}
// Routine Description:
// - Paints the background of the invalid area of the frame.
// Arguments:
// - <none>
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::PaintBackground() noexcept
{
// We need to clear the cursorInvertRects at the start of a paint cycle so
// we don't inadvertently retain the invert region from the last paint after
// the cursor is hidden. If we don't, the ScrollFrame method may attempt to
// clean up a cursor that is no longer there, and instead leave a bunch of
// "ghost" cursor instances on the screen.
cursorInvertRects.clear();
if (_psInvalidData.fErase)
{
RETURN_IF_FAILED(_PaintBackgroundColor(&_psInvalidData.rcPaint));
}
return S_OK;
}
// Routine Description:
// - Draws one line of the buffer to the screen.
// - This will now be cached in a PolyText buffer and flushed periodically instead of drawing every individual segment. Note this means that the PolyText buffer must be flushed before some operations (changing the brush color, drawing lines on top of the characters, inverting for cursor/selection, etc.)
// Arguments:
// - clusters - text to be written and columns expected per cluster
// - coord - character coordinate target to render within viewport
// - trimLeft - This specifies whether to trim one character width off the left side of the output. Used for drawing the right-half only of a double-wide character.
// Return Value:
// - S_OK or suitable GDI HRESULT error.
// - HISTORICAL NOTES:
// ETO_OPAQUE will paint the background color before painting the text.
// ETO_CLIPPED required for ClearType fonts. Cleartype rendering can escape bounding rectangle unless clipped.
// Unclipped rectangles results in ClearType cutting off the right edge of the previous character when adding chars
// and in leaving behind artifacts when backspace/removing chars.
// This mainly applies to ClearType fonts like Lucida Console at small font sizes (10pt) or bolded.
// See: Win7: 390673, 447839 and then superseded by http://osgvsowi/638274 when FE/non-FE rendering condensed.
//#define CONSOLE_EXTTEXTOUT_FLAGS ETO_OPAQUE | ETO_CLIPPED
//#define MAX_POLY_LINES 80
[[nodiscard]] HRESULT GdiEngine::PaintBufferLine(gsl::span<const Cluster> const clusters,
const COORD coord,
const bool trimLeft,
const bool /*lineWrapped*/) noexcept
{
try
{
const auto cchLine = clusters.size();
// Exit early if there are no lines to draw.
RETURN_HR_IF(S_OK, 0 == cchLine);
POINT ptDraw = { 0 };
RETURN_IF_FAILED(_ScaleByFont(&coord, &ptDraw));
const auto pPolyTextLine = &_pPolyText[_cPolyText];
auto& polyString = _polyStrings.emplace_back(cchLine, UNICODE_NULL);
COORD const coordFontSize = _GetFontSize();
auto& polyWidth = _polyWidths.emplace_back(cchLine, 0);
// Sum up the total widths the entire line/run is expected to take while
// copying the pixel widths into a structure to direct GDI how many pixels to use per character.
size_t cchCharWidths = 0;
// Convert data from clusters into the text array and the widths array.
for (size_t i = 0; i < cchLine; i++)
{
const auto& cluster = til::at(clusters, i);
// Our GDI renderer hasn't and isn't going to handle things above U+FFFF or sequences.
// So replace anything complicated with a replacement character for drawing purposes.
polyString[i] = cluster.GetTextAsSingle();
polyWidth[i] = gsl::narrow<int>(cluster.GetColumns()) * coordFontSize.X;
cchCharWidths += polyWidth[i];
}
// Detect and convert for raster font...
if (!_isTrueTypeFont)
{
// dispatch conversion into our codepage
// Find out the bytes required
int const cbRequired = WideCharToMultiByte(_fontCodepage, 0, polyString.data(), (int)cchLine, nullptr, 0, nullptr, nullptr);
if (cbRequired != 0)
{
// Allocate buffer for MultiByte
auto psConverted = std::make_unique<char[]>(cbRequired);
// Attempt conversion to current codepage
int const cbConverted = WideCharToMultiByte(_fontCodepage, 0, polyString.data(), (int)cchLine, psConverted.get(), cbRequired, nullptr, nullptr);
// If successful...
if (cbConverted != 0)
{
// Now we have to convert back to Unicode but using the system ANSI codepage. Find buffer size first.
int const cchRequired = MultiByteToWideChar(CP_ACP, 0, psConverted.get(), cbRequired, nullptr, 0);
if (cchRequired != 0)
{
std::pmr::wstring polyConvert(cchRequired, UNICODE_NULL, &_pool);
// Then do the actual conversion.
int const cchConverted = MultiByteToWideChar(CP_ACP, 0, psConverted.get(), cbRequired, polyConvert.data(), cchRequired);
if (cchConverted != 0)
{
// If all successful, use this instead.
polyString.swap(polyConvert);
}
}
}
}
}
// If the line rendition is double height, we need to adjust the top or bottom
// of the clipping rect to clip half the height of the rendered characters.
const auto halfHeight = coordFontSize.Y >> 1;
const auto topOffset = _currentLineRendition == LineRendition::DoubleHeightBottom ? halfHeight : 0;
const auto bottomOffset = _currentLineRendition == LineRendition::DoubleHeightTop ? halfHeight : 0;
pPolyTextLine->lpstr = polyString.data();
pPolyTextLine->n = gsl::narrow<UINT>(clusters.size());
pPolyTextLine->x = ptDraw.x;
pPolyTextLine->y = ptDraw.y;
pPolyTextLine->uiFlags = ETO_OPAQUE | ETO_CLIPPED;
pPolyTextLine->rcl.left = pPolyTextLine->x;
pPolyTextLine->rcl.top = pPolyTextLine->y + topOffset;
pPolyTextLine->rcl.right = pPolyTextLine->rcl.left + (SHORT)cchCharWidths;
pPolyTextLine->rcl.bottom = pPolyTextLine->y + coordFontSize.Y - bottomOffset;
pPolyTextLine->pdx = polyWidth.data();
if (trimLeft)
{
pPolyTextLine->rcl.left += coordFontSize.X;
}
_cPolyText++;
if (_cPolyText >= s_cPolyTextCache)
{
LOG_IF_FAILED(_FlushBufferLines());
}
return S_OK;
}
CATCH_RETURN();
}
// Routine Description:
// - Flushes any buffer lines in the PolyTextOut cache by drawing them and freeing the strings.
// - See also: PaintBufferLine
// Arguments:
// - <none>
// Return Value:
// - S_OK or E_FAIL if GDI failed.
[[nodiscard]] HRESULT GdiEngine::_FlushBufferLines() noexcept
{
HRESULT hr = S_OK;
if (_cPolyText > 0)
{
if (!PolyTextOutW(_hdcMemoryContext, _pPolyText, (UINT)_cPolyText))
{
hr = E_FAIL;
}
_polyStrings.clear();
_polyWidths.clear();
ZeroMemory(_pPolyText, sizeof(_pPolyText));
_cPolyText = 0;
}
RETURN_HR(hr);
}
// Routine Description:
// - Draws up to one line worth of grid lines on top of characters.
// Arguments:
// - lines - Enum defining which edges of the rectangle to draw
// - color - The color to use for drawing the edges.
// - cchLine - How many characters we should draw the grid lines along (left to right in a row)
// - coordTarget - The starting X/Y position of the first character to draw on.
// Return Value:
// - S_OK or suitable GDI HRESULT error or E_FAIL for GDI errors in functions that don't reliably return a specific error code.
[[nodiscard]] HRESULT GdiEngine::PaintBufferGridLines(const GridLines lines, const COLORREF color, const size_t cchLine, const COORD coordTarget) noexcept
{
LOG_IF_FAILED(_FlushBufferLines());
// Convert the target from characters to pixels.
POINT ptTarget;
RETURN_IF_FAILED(_ScaleByFont(&coordTarget, &ptTarget));
// Set the brush color as requested and save the previous brush to restore at the end.
wil::unique_hbrush hbr(CreateSolidBrush(color));
RETURN_HR_IF_NULL(E_FAIL, hbr.get());
wil::unique_hbrush hbrPrev(SelectBrush(_hdcMemoryContext, hbr.get()));
RETURN_HR_IF_NULL(E_FAIL, hbrPrev.get());
hbr.release(); // If SelectBrush was successful, GDI owns the brush. Release for now.
// On exit, be sure we try to put the brush back how it was originally.
auto restoreBrushOnExit = wil::scope_exit([&] { hbr.reset(SelectBrush(_hdcMemoryContext, hbrPrev.get())); });
// Get the font size so we know the size of the rectangle lines we'll be inscribing.
const auto fontWidth = _GetFontSize().X;
const auto fontHeight = _GetFontSize().Y;
const auto widthOfAllCells = fontWidth * gsl::narrow_cast<unsigned>(cchLine);
const auto DrawLine = [=](const auto x, const auto y, const auto w, const auto h) {
return PatBlt(_hdcMemoryContext, x, y, w, h, PATCOPY);
};
if (lines & GridLines::Left)
{
auto x = ptTarget.x;
for (size_t i = 0; i < cchLine; i++, x += fontWidth)
{
RETURN_HR_IF(E_FAIL, !DrawLine(x, ptTarget.y, _lineMetrics.gridlineWidth, fontHeight));
}
}
if (lines & GridLines::Right)
{
// NOTE: We have to subtract the stroke width from the cell width
// to ensure the x coordinate remains inside the clipping rectangle.
auto x = ptTarget.x + fontWidth - _lineMetrics.gridlineWidth;
for (size_t i = 0; i < cchLine; i++, x += fontWidth)
{
RETURN_HR_IF(E_FAIL, !DrawLine(x, ptTarget.y, _lineMetrics.gridlineWidth, fontHeight));
}
}
if (lines & GridLines::Top)
{
const auto y = ptTarget.y;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth));
}
if (lines & GridLines::Bottom)
{
// NOTE: We have to subtract the stroke width from the cell height
// to ensure the y coordinate remains inside the clipping rectangle.
const auto y = ptTarget.y + fontHeight - _lineMetrics.gridlineWidth;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth));
}
if (lines & (GridLines::Underline | GridLines::DoubleUnderline))
{
const auto y = ptTarget.y + _lineMetrics.underlineOffset;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.underlineWidth));
if (lines & GridLines::DoubleUnderline)
{
const auto y2 = ptTarget.y + _lineMetrics.underlineOffset2;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y2, widthOfAllCells, _lineMetrics.underlineWidth));
}
}
if (lines & GridLines::Strikethrough)
{
const auto y = ptTarget.y + _lineMetrics.strikethroughOffset;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth));
}
return S_OK;
}
// Routine Description:
// - Draws the cursor on the screen
// Arguments:
// - options - Parameters that affect the way that the cursor is drawn
// Return Value:
// - S_OK, suitable GDI HRESULT error, or safemath error, or E_FAIL in a GDI error where a specific error isn't set.
[[nodiscard]] HRESULT GdiEngine::PaintCursor(const CursorOptions& options) noexcept
{
// if the cursor is off, do nothing - it should not be visible.
if (!options.isOn)
{
return S_FALSE;
}
LOG_IF_FAILED(_FlushBufferLines());
COORD const coordFontSize = _GetFontSize();
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.X == 0 || coordFontSize.Y == 0);
// First set up a block cursor the size of the font.
RECT rcBoundaries;
RETURN_IF_FAILED(LongMult(options.coordCursor.X, coordFontSize.X, &rcBoundaries.left));
RETURN_IF_FAILED(LongMult(options.coordCursor.Y, coordFontSize.Y, &rcBoundaries.top));
RETURN_IF_FAILED(LongAdd(rcBoundaries.left, coordFontSize.X, &rcBoundaries.right));
RETURN_IF_FAILED(LongAdd(rcBoundaries.top, coordFontSize.Y, &rcBoundaries.bottom));
// If we're double-width cursor, make it an extra font wider.
if (options.fIsDoubleWidth)
{
RETURN_IF_FAILED(LongAdd(rcBoundaries.right, coordFontSize.X, &rcBoundaries.right));
}
// Make a set of RECTs to paint.
cursorInvertRects.clear();
RECT rcInvert = rcBoundaries;
// depending on the cursorType, add rects to that set
switch (options.cursorType)
{
case CursorType::Legacy:
{
// Now adjust the cursor height
// enforce min/max cursor height
ULONG ulHeight = options.ulCursorHeightPercent;
ulHeight = std::max(ulHeight, s_ulMinCursorHeightPercent); // No smaller than 25%
ulHeight = std::min(ulHeight, s_ulMaxCursorHeightPercent); // No larger than 100%
ulHeight = MulDiv(coordFontSize.Y, ulHeight, 100); // divide by 100 because percent.
// Reduce the height of the top to be relative to the bottom by the height we want.
RETURN_IF_FAILED(LongSub(rcInvert.bottom, ulHeight, &rcInvert.top));
cursorInvertRects.push_back(rcInvert);
}
break;
case CursorType::VerticalBar:
LONG proposedWidth;
RETURN_IF_FAILED(LongAdd(rcInvert.left, options.cursorPixelWidth, &proposedWidth));
// It can't be wider than one cell or we'll have problems in invalidation, so restrict here.
// It's either the left + the proposed width from the ease of access setting, or
// it's the right edge of the block cursor as a maximum.
rcInvert.right = std::min(rcInvert.right, proposedWidth);
cursorInvertRects.push_back(rcInvert);
break;
case CursorType::Underscore:
RETURN_IF_FAILED(LongAdd(rcInvert.bottom, -1, &rcInvert.top));
cursorInvertRects.push_back(rcInvert);
break;
case CursorType::DoubleUnderscore:
{
RECT top, bottom;
top = bottom = rcBoundaries;
RETURN_IF_FAILED(LongAdd(bottom.bottom, -1, &bottom.top));
RETURN_IF_FAILED(LongAdd(top.bottom, -3, &top.top));
RETURN_IF_FAILED(LongAdd(top.top, 1, &top.bottom));
cursorInvertRects.push_back(top);
cursorInvertRects.push_back(bottom);
}
break;
case CursorType::EmptyBox:
{
RECT top, left, right, bottom;
top = left = right = bottom = rcBoundaries;
RETURN_IF_FAILED(LongAdd(top.top, 1, &top.bottom));
RETURN_IF_FAILED(LongAdd(bottom.bottom, -1, &bottom.top));
RETURN_IF_FAILED(LongAdd(left.left, 1, &left.right));
RETURN_IF_FAILED(LongAdd(right.right, -1, &right.left));
RETURN_IF_FAILED(LongAdd(top.left, 1, &top.left));
RETURN_IF_FAILED(LongAdd(bottom.left, 1, &bottom.left));
RETURN_IF_FAILED(LongAdd(top.right, -1, &top.right));
RETURN_IF_FAILED(LongAdd(bottom.right, -1, &bottom.right));
cursorInvertRects.push_back(top);
cursorInvertRects.push_back(left);
cursorInvertRects.push_back(right);
cursorInvertRects.push_back(bottom);
}
break;
case CursorType::FullBox:
cursorInvertRects.push_back(rcInvert);
break;
default:
return E_NOTIMPL;
}
// Prepare the appropriate line transform for the current row.
LOG_IF_FAILED(PrepareLineTransform(options.lineRendition, 0, options.viewportLeft));
auto resetLineTransform = wil::scope_exit([&]() {
LOG_IF_FAILED(ResetLineTransform());
});
// Either invert all the RECTs, or paint them.
if (options.fUseColor)
{
HBRUSH hCursorBrush = CreateSolidBrush(options.cursorColor);
for (RECT r : cursorInvertRects)
{
RETURN_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, &r, hCursorBrush)));
}
DeleteObject(hCursorBrush);
// Clear out the inverted rects, so that we don't re-invert them next frame.
cursorInvertRects.clear();
}
else
{
// Save the current line transform in case we need to reapply these
// inverted rects to hide the cursor in the ScrollFrame method.
cursorInvertTransform = _currentLineTransform;
for (RECT r : cursorInvertRects)
{
RETURN_HR_IF(E_FAIL, !(InvertRect(_hdcMemoryContext, &r)));
}
}
return S_OK;
}
// Routine Description:
// - Inverts the selected region on the current screen buffer.
// - Reads the selected area, selection mode, and active screen buffer
// from the global properties and dispatches a GDI invert on the selected text area.
// Arguments:
// - rect - Rectangle to invert or highlight to make the selection area
// Return Value:
// - S_OK or suitable GDI HRESULT error.
[[nodiscard]] HRESULT GdiEngine::PaintSelection(const SMALL_RECT rect) noexcept
{
LOG_IF_FAILED(_FlushBufferLines());
RECT pixelRect = { 0 };
RETURN_IF_FAILED(_ScaleByFont(&rect, &pixelRect));
RETURN_HR_IF(E_FAIL, !InvertRect(_hdcMemoryContext, &pixelRect));
return S_OK;
}
#ifdef DBG
void GdiEngine::_CreateDebugWindow()
{
if (_fDebug)
{
const auto className = L"ConsoleGdiDebugWindow";
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_OWNDC;
wc.lpfnWndProc = DefWindowProcW;
wc.hInstance = nullptr;
wc.lpszClassName = className;
THROW_LAST_ERROR_IF(0 == RegisterClassExW(&wc));
_debugWindow = CreateWindowExW(0,
className,
L"ConhostGdiDebugWindow",
0,
0,
0,
0,
0,
nullptr,
nullptr,
nullptr,
nullptr);
THROW_LAST_ERROR_IF_NULL(_debugWindow);
ShowWindow(_debugWindow, SW_SHOWNORMAL);
}
}
// Routine Description:
// - Will fill a given rectangle with a gray shade to help identify which portion of the screen is being debugged.
// - Will attempt immediate BLT so you can see it.
// - NOTE: You must set _fDebug flag for this to operate using a debugger.
// - NOTE: This only works in Debug (DBG) builds.
// Arguments:
// - prc - Pointer to rectangle to fill
// Return Value:
// - <none>
void GdiEngine::_PaintDebugRect(const RECT* const prc) const
{
if (_fDebug)
{
if (!IsRectEmpty(prc))
{
wil::unique_hbrush hbr(GetStockBrush(GRAY_BRUSH));
if (nullptr != LOG_HR_IF_NULL(E_FAIL, hbr.get()))
{
LOG_HR_IF(E_FAIL, !(FillRect(_hdcMemoryContext, prc, hbr.get())));
_DoDebugBlt(prc);
}
}
}
}
// Routine Description:
// - Will immediately Blt the given rectangle to the screen for aid in debugging when it is tough to see
// what is occurring with the in-memory DC.
// - This will pause the thread for 200ms when called to give you an opportunity to see the paint.
// - NOTE: You must set _fDebug flag for this to operate using a debugger.
// - NOTE: This only works in Debug (DBG) builds.
// Arguments:
// - prc - Pointer to region to immediately Blt to the real screen DC.
// Return Value:
// - <none>
void GdiEngine::_DoDebugBlt(const RECT* const prc) const
{
if (_fDebug)
{
if (!IsRectEmpty(prc))
{
LOG_HR_IF(E_FAIL, !(BitBlt(_debugContext, prc->left, prc->top, prc->right - prc->left, prc->bottom - prc->top, _hdcMemoryContext, prc->left, prc->top, SRCCOPY)));
Sleep(100);
}
}
}
void GdiEngine::_DebugBltAll() const
{
if (_fDebug)
{
BitBlt(_debugContext, 0, 0, _szMemorySurface.cx, _szMemorySurface.cy, _hdcMemoryContext, 0, 0, SRCCOPY);
Sleep(100);
}
}
#endif