-
Notifications
You must be signed in to change notification settings - Fork 8.4k
/
paint.cpp
987 lines (825 loc) · 38.8 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
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "gdirenderer.hpp"
#include "../inc/unicode.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Render;
// This is an excerpt of GDI's FontHasWesternScript() as
// used by InternalTextOut() which is part of ExtTextOutW().
bool GdiEngine::FontHasWesternScript(HDC hdc)
{
WORD glyphs[4];
return (GetGlyphIndicesW(hdc, L"dMr\"", 4, glyphs, GGI_MARK_NONEXISTING_GLYPHS) == 4) &&
(glyphs[0] != 0xFFFF && glyphs[1] != 0xFFFF && glyphs[2] != 0xFFFF && glyphs[3] != 0xFFFF);
}
// 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.to_win32_rect();
#if DBG
_debugContext = GetDC(_debugWindow);
#endif
_lastFontType = FontType::Undefined;
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.width && 0 == _szInvalidScroll.height);
// 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 auto identityTransform = cursorInvertTransform == IDENTITY_XFORM;
if (!identityTransform)
{
LOG_HR_IF(E_FAIL, !SetWorldTransform(_hdcMemoryContext, &cursorInvertTransform));
LOG_HR_IF(E_FAIL, !SetWorldTransform(_psInvalidData.hdc, &cursorInvertTransform));
}
for (const auto& 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.
const auto coordFontSize = _GetFontSize();
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.width == 0 || coordFontSize.height == 0);
til::size szGutter;
szGutter.width = _szMemorySurface.width % coordFontSize.width;
szGutter.height = _szMemorySurface.height % coordFontSize.height;
RECT rcScrollLimit{};
RETURN_IF_FAILED(LongSub(_szMemorySurface.width, szGutter.width, &rcScrollLimit.right));
RETURN_IF_FAILED(LongSub(_szMemorySurface.height, szGutter.height, &rcScrollLimit.bottom));
// Scroll real window and memory buffer in-sync.
LOG_LAST_ERROR_IF(!ScrollWindowEx(_hwndTargetWindow,
_szInvalidScroll.width,
_szInvalidScroll.height,
&rcScrollLimit,
&rcScrollLimit,
nullptr,
nullptr,
0));
til::rect rcUpdate;
LOG_HR_IF(E_FAIL, !(ScrollDC(_hdcMemoryContext, _szInvalidScroll.width, _szInvalidScroll.height, &rcScrollLimit, &rcScrollLimit, nullptr, rcUpdate.as_win32_rect())));
LOG_IF_FAILED(_InvalidCombine(&rcUpdate));
// update invalid rect for the remainder of paint functions
_psInvalidData.rcPaint = _rcInvalid.to_win32_rect();
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)));
const auto 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.width == szClient.width && _szMemorySurface.height == szClient.height);
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.width, szClient.height));
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.width, _szMemorySurface.height, _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.width, szClient.height);
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());
const auto pt = _GetInvalidRectPoint();
const auto sz = _GetInvalidRectSize();
LOG_HR_IF(E_FAIL, !(BitBlt(_psInvalidData.hdc, pt.x, pt.y, sz.width, sz.height, _hdcMemoryContext, pt.x, pt.y, SRCCOPY)));
WHEN_DBG(_DebugBltAll());
_rcInvalid = {};
_fInvalidRectUsed = false;
_szInvalidScroll = {};
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(const std::span<const Cluster> clusters,
const til::point 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);
const auto ptDraw = coord * _GetFontSize();
const auto pPolyTextLine = &_pPolyText[_cPolyText];
auto& polyString = _polyStrings.emplace_back();
polyString.reserve(cchLine);
const auto coordFontSize = _GetFontSize();
auto& polyWidth = _polyWidths.emplace_back();
polyWidth.reserve(cchLine);
// If we have a soft font, we only use the character's lower 7 bits.
const auto softFontCharMask = _lastFontType == FontType::Soft ? L'\x7F' : ~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);
const auto text = cluster.GetText();
polyString += text;
polyString.back() &= softFontCharMask;
polyWidth.push_back(gsl::narrow<int>(cluster.GetColumns()) * coordFontSize.width);
cchCharWidths += polyWidth.back();
polyWidth.append(text.size() - 1, 0);
}
// Detect and convert for raster font...
if (!_isTrueTypeFont)
{
// dispatch conversion into our codepage
// Find out the bytes required
const auto 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
const auto 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.
const auto 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.
const auto 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.height >> 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>(polyString.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 + (til::CoordType)cchCharWidths;
pPolyTextLine->rcl.bottom = pPolyTextLine->y + coordFontSize.height - bottomOffset;
pPolyTextLine->pdx = polyWidth.data();
if (trimLeft)
{
pPolyTextLine->rcl.left += coordFontSize.width;
}
_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
{
auto hr = S_OK;
if (_cPolyText > 0)
{
for (size_t i = 0; i != _cPolyText; ++i)
{
const auto& t = _pPolyText[i];
// The following if/else replicates the essentials of how ExtTextOutW() without ETO_IGNORELANGUAGE works.
// See InternalTextOut().
//
// Unlike the original, we don't check for `GetTextCharacterExtra(hdc) != 0`,
// because we don't ever call SetTextCharacterExtra() anyways.
//
// GH#12294:
// Additionally we set ss.fOverrideDirection to TRUE, because we need to present RTL
// text in logical order in order to be compatible with applications like `vim -H`.
if (_fontHasWesternScript && ScriptIsComplex(t.lpstr, t.n, SIC_COMPLEX) == S_FALSE)
{
if (!ExtTextOutW(_hdcMemoryContext, t.x, t.y, t.uiFlags | ETO_IGNORELANGUAGE, &t.rcl, t.lpstr, t.n, t.pdx))
{
hr = E_FAIL;
break;
}
}
else
{
SCRIPT_STATE ss{};
ss.fOverrideDirection = TRUE;
SCRIPT_STRING_ANALYSIS ssa;
hr = ScriptStringAnalyse(_hdcMemoryContext, t.lpstr, t.n, 0, -1, SSA_GLYPHS | SSA_FALLBACK, 0, nullptr, &ss, t.pdx, nullptr, nullptr, &ssa);
if (FAILED(hr))
{
break;
}
hr = ScriptStringOut(ssa, t.x, t.y, t.uiFlags, &t.rcl, 0, 0, FALSE);
std::ignore = ScriptStringFree(&ssa);
if (FAILED(hr))
{
break;
}
}
}
_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
// - gridlineColor - The color to use for drawing the gridlines.
// - underlineColor - The color to use for drawing the underlines.
// - 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 GridLineSet lines, const COLORREF gridlineColor, const COLORREF underlineColor, const size_t cchLine, const til::point coordTarget) noexcept
try
{
LOG_IF_FAILED(_FlushBufferLines());
// Convert the target from characters to pixels.
const auto ptTarget = coordTarget * _GetFontSize();
// Create a brush with the gridline color, and apply it.
wil::unique_hbrush hbr(CreateSolidBrush(gridlineColor));
RETURN_HR_IF_NULL(E_FAIL, hbr.get());
const auto prevBrush = wil::SelectObject(_hdcMemoryContext, hbr.get());
RETURN_HR_IF_NULL(E_FAIL, prevBrush.get());
// Get the font size so we know the size of the rectangle lines we'll be inscribing.
const auto fontWidth = _GetFontSize().width;
const auto fontHeight = _GetFontSize().height;
const auto widthOfAllCells = fontWidth * gsl::narrow_cast<til::CoordType>(cchLine);
const auto DrawLine = [=](const til::CoordType x, const til::CoordType y, const til::CoordType w, const til::CoordType h) {
return PatBlt(_hdcMemoryContext, x, y, w, h, PATCOPY);
};
const auto DrawStrokedLine = [&](const til::CoordType x, const til::CoordType y, const til::CoordType w) {
RETURN_HR_IF(E_FAIL, !MoveToEx(_hdcMemoryContext, x, y, nullptr));
RETURN_HR_IF(E_FAIL, !LineTo(_hdcMemoryContext, x + w, y));
return S_OK;
};
const auto DrawCurlyLine = [&](const til::CoordType begX, const til::CoordType y, const til::CoordType width) {
const auto period = _lineMetrics.curlyLinePeriod;
const auto halfPeriod = period / 2;
const auto controlPointOffset = _lineMetrics.curlyLineControlPointOffset;
// To ensure proper continuity of the wavy line between cells of different line color
// this code starts/ends the line earlier/later than it should and then clips it.
// Clipping in GDI is expensive, but it was the easiest approach.
// I've noticed that subtracting -1px prevents missing pixels when GDI draws. They still
// occur at certain (small) font sizes, but I couldn't figure out how to prevent those.
const auto lineStart = ((begX - 1) / period) * period;
const auto lineEnd = begX + width;
IntersectClipRect(_hdcMemoryContext, begX, ptTarget.y, begX + width, ptTarget.y + fontHeight);
const auto restoreRegion = wil::scope_exit([&]() {
// Luckily no one else uses clip regions. They're weird to use.
SelectClipRgn(_hdcMemoryContext, nullptr);
});
// You can assume that each cell has roughly 5 POINTs on average. 128 POINTs is 1KiB.
til::small_vector<POINT, 128> points;
// This is the start point of the Bézier curve.
points.emplace_back(lineStart, y);
for (auto x = lineStart; x < lineEnd; x += period)
{
points.emplace_back(x + halfPeriod, y - controlPointOffset);
points.emplace_back(x + halfPeriod, y + controlPointOffset);
points.emplace_back(x + period, y);
}
const auto cpt = gsl::narrow_cast<DWORD>(points.size());
return PolyBezier(_hdcMemoryContext, points.data(), cpt);
};
if (lines.test(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.test(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.test(GridLines::Top))
{
const auto y = ptTarget.y;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.gridlineWidth));
}
if (lines.test(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.test(GridLines::Strikethrough))
{
const auto y = ptTarget.y + _lineMetrics.strikethroughOffset;
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, y, widthOfAllCells, _lineMetrics.strikethroughWidth));
}
DWORD underlinePenType = PS_SOLID;
if (lines.test(GridLines::DottedUnderline))
{
underlinePenType = PS_DOT;
}
else if (lines.test(GridLines::DashedUnderline))
{
underlinePenType = PS_DASH;
}
DWORD underlineWidth = _lineMetrics.underlineWidth;
if (lines.any(GridLines::DoubleUnderline, GridLines::CurlyUnderline))
{
underlineWidth = _lineMetrics.doubleUnderlineWidth;
}
const LOGBRUSH brushProp{ .lbStyle = BS_SOLID, .lbColor = underlineColor };
wil::unique_hpen hpen(ExtCreatePen(underlinePenType | PS_GEOMETRIC | PS_ENDCAP_FLAT, underlineWidth, &brushProp, 0, nullptr));
// Apply the pen.
const auto prevPen = wil::SelectObject(_hdcMemoryContext, hpen.get());
RETURN_HR_IF_NULL(E_FAIL, prevPen.get());
if (lines.test(GridLines::Underline))
{
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineCenter, widthOfAllCells);
}
else if (lines.test(GridLines::DoubleUnderline))
{
RETURN_IF_FAILED(DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.doubleUnderlinePosTop, widthOfAllCells));
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.doubleUnderlinePosBottom, widthOfAllCells);
}
else if (lines.test(GridLines::CurlyUnderline))
{
return DrawCurlyLine(ptTarget.x, ptTarget.y + _lineMetrics.curlyLineCenter, widthOfAllCells);
}
else if (lines.test(GridLines::DottedUnderline))
{
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineCenter, widthOfAllCells);
}
else if (lines.test(GridLines::DashedUnderline))
{
return DrawStrokedLine(ptTarget.x, ptTarget.y + _lineMetrics.underlineCenter, widthOfAllCells);
}
return S_OK;
}
CATCH_RETURN();
[[nodiscard]] HRESULT GdiEngine::PaintImageSlice(const ImageSlice& imageSlice,
const til::CoordType targetRow,
const til::CoordType viewportLeft) noexcept
try
{
LOG_IF_FAILED(_FlushBufferLines());
LOG_IF_FAILED(ResetLineTransform());
const auto& imagePixels = imageSlice.Pixels();
if (_imageMask.size() < imagePixels.size())
{
_imageMask.resize(imagePixels.size());
}
const auto srcCellSize = imageSlice.CellSize();
const auto dstCellSize = _GetFontSize();
const auto srcWidth = imageSlice.PixelWidth();
const auto srcHeight = srcCellSize.height;
const auto dstWidth = srcWidth * dstCellSize.width / srcCellSize.width;
const auto dstHeight = srcHeight * dstCellSize.height / srcCellSize.height;
const auto x = (imageSlice.ColumnOffset() - viewportLeft) * dstCellSize.width;
const auto y = targetRow * dstCellSize.height;
auto bitmapInfo = BITMAPINFO{
.bmiHeader = {
.biSize = sizeof(BITMAPINFOHEADER),
.biWidth = srcWidth,
.biHeight = -srcHeight,
.biPlanes = 1,
.biBitCount = 32,
.biCompression = BI_RGB,
}
};
auto allOpaque = true;
auto allTransparent = true;
for (size_t i = 0; i < imagePixels.size(); i++)
{
const auto opaque = til::at(imagePixels, i).rgbReserved != 0;
allOpaque &= opaque;
allTransparent &= !opaque;
til::at(_imageMask, i) = (opaque ? 0 : 0xFFFFFF);
}
if (allOpaque)
{
StretchDIBits(_hdcMemoryContext, x, y, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, imagePixels.data(), &bitmapInfo, DIB_RGB_COLORS, SRCCOPY);
}
else if (!allTransparent)
{
StretchDIBits(_hdcMemoryContext, x, y, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, _imageMask.data(), &bitmapInfo, DIB_RGB_COLORS, SRCAND);
StretchDIBits(_hdcMemoryContext, x, y, dstWidth, dstHeight, 0, 0, srcWidth, srcHeight, imagePixels.data(), &bitmapInfo, DIB_RGB_COLORS, SRCPAINT);
}
return S_OK;
}
CATCH_RETURN();
// 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());
const auto coordFontSize = _GetFontSize();
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_INVALID_STATE), coordFontSize.width == 0 || coordFontSize.height == 0);
// First set up a block cursor the size of the font.
RECT rcBoundaries;
rcBoundaries.left = options.coordCursor.x * coordFontSize.width;
rcBoundaries.top = options.coordCursor.y * coordFontSize.height;
rcBoundaries.right = rcBoundaries.left + coordFontSize.width;
rcBoundaries.bottom = rcBoundaries.top + coordFontSize.height;
// If we're double-width cursor, make it an extra font wider.
if (options.fIsDoubleWidth)
{
rcBoundaries.right = rcBoundaries.right + coordFontSize.width;
}
// Make a set of RECTs to paint.
cursorInvertRects.clear();
auto 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
auto 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.height, ulHeight, 100); // divide by 100 because percent.
// Reduce the height of the top to be relative to the bottom by the height we want.
rcInvert.top = rcInvert.bottom - ulHeight;
cursorInvertRects.push_back(rcInvert);
}
break;
case CursorType::VerticalBar:
LONG proposedWidth;
proposedWidth = rcInvert.left + options.cursorPixelWidth;
// 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:
rcInvert.top = rcInvert.bottom + -1;
cursorInvertRects.push_back(rcInvert);
break;
case CursorType::DoubleUnderscore:
{
RECT top, bottom;
top = bottom = rcBoundaries;
bottom.top = bottom.bottom + -1;
top.top = top.bottom + -3;
top.bottom = top.top + 1;
cursorInvertRects.push_back(top);
cursorInvertRects.push_back(bottom);
}
break;
case CursorType::EmptyBox:
{
RECT top, left, right, bottom;
top = left = right = bottom = rcBoundaries;
top.bottom = top.top + 1;
bottom.top = bottom.bottom + -1;
left.right = left.left + 1;
right.left = right.right + -1;
top.left = top.left + 1;
bottom.left = bottom.left + 1;
top.right = top.right + -1;
bottom.right = bottom.right + -1;
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)
{
auto hCursorBrush = CreateSolidBrush(options.cursorColor);
for (auto 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 (auto r : cursorInvertRects)
{
// Make sure the cursor is always readable (see gh-3647)
const auto PrevObject = SelectObject(_hdcMemoryContext, GetStockObject(LTGRAY_BRUSH));
const auto Result = PatBlt(_hdcMemoryContext, r.left, r.top, r.right - r.left, r.bottom - r.top, PATINVERT);
SelectObject(_hdcMemoryContext, PrevObject);
RETURN_HR_IF(E_FAIL, !Result);
}
}
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 til::rect& rect) noexcept
{
LOG_IF_FAILED(_FlushBufferLines());
const auto pixelRect = rect.scale_up(_GetFontSize()).to_win32_rect();
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.width, _szMemorySurface.height, _hdcMemoryContext, 0, 0, SRCCOPY);
Sleep(100);
}
}
#endif