1 // Scintilla source code edit control
2 /** @file MarginView.cxx
3 ** Defines the appearance of the editor margin.
5 // Copyright 1998-2014 by Neil Hodgson <neilh@scintilla.org>
6 // The License.txt file describes the conditions under which this software may be distributed.
18 #include <string_view>
26 #include "ScintillaTypes.h"
27 #include "ScintillaMessages.h"
28 #include "ScintillaStructures.h"
32 #include "Debugging.h"
36 #include "CharacterCategoryMap.h"
38 #include "UniqueString.h"
39 #include "SplitVector.h"
40 #include "Partitioning.h"
41 #include "RunStyles.h"
42 #include "ContractionState.h"
43 #include "CellBuffer.h"
45 #include "Indicator.h"
46 #include "LineMarker.h"
48 #include "ViewStyle.h"
49 #include "CharClassify.h"
50 #include "Decoration.h"
51 #include "CaseFolder.h"
53 #include "UniConversion.h"
54 #include "Selection.h"
55 #include "PositionCache.h"
56 #include "EditModel.h"
57 #include "MarginView.h"
60 using namespace Scintilla
;
62 namespace Scintilla::Internal
{
64 void DrawWrapMarker(Surface
*surface
, PRectangle rcPlace
,
65 bool isEndMarker
, ColourRGBA wrapColour
) {
67 const XYPOSITION extraFinalPixel
= surface
->SupportsFeature(Supports::LineDrawsFinal
) ? 0.0f
: 1.0f
;
69 const PRectangle rcAligned
= PixelAlignOutside(rcPlace
, surface
->PixelDivisions());
71 const XYPOSITION widthStroke
= std::floor(rcAligned
.Width() / 6);
73 constexpr XYPOSITION xa
= 1; // gap before start
74 const XYPOSITION w
= rcAligned
.Width() - xa
- widthStroke
;
76 // isEndMarker -> x-mirrored symbol for start marker
78 const XYPOSITION x0
= isEndMarker
? rcAligned
.left
: rcAligned
.right
- widthStroke
;
79 const XYPOSITION y0
= rcAligned
.top
;
81 const XYPOSITION dy
= std::floor(rcAligned
.Height() / 5);
82 const XYPOSITION y
= std::floor(rcAligned
.Height() / 2) + dy
;
90 Point
At(XYPOSITION xRelative
, XYPOSITION yRelative
) const noexcept
{
91 return Point(xBase
+ xDir
* xRelative
+ halfWidth
, yBase
+ yDir
* yRelative
+ halfWidth
);
95 Relative rel
= { x0
, isEndMarker
? 1 : -1, y0
, 1, widthStroke
/ 2.0f
};
98 const Point head
[] = {
99 rel
.At(xa
+ dy
, y
- dy
),
101 rel
.At(xa
+ dy
+ extraFinalPixel
, y
+ dy
+ extraFinalPixel
)
103 surface
->PolyLine(head
, std::size(head
), Stroke(wrapColour
, widthStroke
));
106 const Point body
[] = {
109 rel
.At(xa
+ w
, y
- 2 * dy
),
110 rel
.At(xa
, y
- 2 * dy
),
112 surface
->PolyLine(body
, std::size(body
), Stroke(wrapColour
, widthStroke
));
115 MarginView::MarginView() noexcept
{
116 wrapMarkerPaddingRight
= 3;
117 customDrawWrapMarker
= nullptr;
120 void MarginView::DropGraphics() noexcept
{
121 pixmapSelMargin
.reset();
122 pixmapSelPattern
.reset();
123 pixmapSelPatternOffset1
.reset();
126 void MarginView::RefreshPixMaps(Surface
*surfaceWindow
, const ViewStyle
&vsDraw
) {
127 if (!pixmapSelPattern
) {
128 constexpr int patternSize
= 8;
129 pixmapSelPattern
= surfaceWindow
->AllocatePixMap(patternSize
, patternSize
);
130 pixmapSelPatternOffset1
= surfaceWindow
->AllocatePixMap(patternSize
, patternSize
);
131 // This complex procedure is to reproduce the checkerboard dithered pattern used by windows
132 // for scroll bars and Visual Studio for its selection margin. The colour of this pattern is half
133 // way between the chrome colour and the chrome highlight colour making a nice transition
134 // between the window chrome and the content area. And it works in low colour depths.
135 const PRectangle rcPattern
= PRectangle::FromInts(0, 0, patternSize
, patternSize
);
137 // Initialize default colours based on the chrome colour scheme. Typically the highlight is white.
138 ColourRGBA colourFMFill
= vsDraw
.selbar
;
139 ColourRGBA colourFMStripes
= vsDraw
.selbarlight
;
141 if (!(vsDraw
.selbarlight
== ColourRGBA(0xff, 0xff, 0xff))) {
142 // User has chosen an unusual chrome colour scheme so just use the highlight edge colour.
143 // (Typically, the highlight colour is white.)
144 colourFMFill
= vsDraw
.selbarlight
;
147 if (vsDraw
.foldmarginColour
) {
148 // override default fold margin colour
149 colourFMFill
= *vsDraw
.foldmarginColour
;
151 if (vsDraw
.foldmarginHighlightColour
) {
152 // override default fold margin highlight colour
153 colourFMStripes
= *vsDraw
.foldmarginHighlightColour
;
156 pixmapSelPattern
->FillRectangle(rcPattern
, colourFMFill
);
157 pixmapSelPatternOffset1
->FillRectangle(rcPattern
, colourFMStripes
);
158 for (int y
= 0; y
< patternSize
; y
++) {
159 for (int x
= y
% 2; x
< patternSize
; x
+= 2) {
160 const PRectangle rcPixel
= PRectangle::FromInts(x
, y
, x
+ 1, y
+ 1);
161 pixmapSelPattern
->FillRectangle(rcPixel
, colourFMStripes
);
162 pixmapSelPatternOffset1
->FillRectangle(rcPixel
, colourFMFill
);
165 pixmapSelPattern
->FlushDrawing();
166 pixmapSelPatternOffset1
->FlushDrawing();
172 MarkerOutline
SubstituteMarkerIfEmpty(MarkerOutline markerCheck
, MarkerOutline markerDefault
, const ViewStyle
&vs
) noexcept
{
173 if (vs
.markers
[static_cast<size_t>(markerCheck
)].markType
== MarkerSymbol::Empty
)
174 return markerDefault
;
178 constexpr MarkerOutline
TailFromNextLevel(FoldLevel levelNextNum
) noexcept
{
179 return (levelNextNum
> FoldLevel::Base
) ? MarkerOutline::FolderMidTail
: MarkerOutline::FolderTail
;
182 int FoldingMark(FoldLevel level
, FoldLevel levelNext
, bool firstSubLine
, bool lastSubLine
,
183 bool isExpanded
, bool needWhiteClosure
, MarkerOutline folderOpenMid
, MarkerOutline folderEnd
) noexcept
{
185 const FoldLevel levelNum
= LevelNumberPart(level
);
186 const FoldLevel levelNextNum
= LevelNumberPart(levelNext
);
188 if (LevelIsHeader(level
)) {
190 if (levelNum
< levelNextNum
) {
191 if (levelNum
== FoldLevel::Base
) {
192 return 1 << (isExpanded
? MarkerOutline::FolderOpen
: MarkerOutline::Folder
);
194 return 1 << (isExpanded
? folderOpenMid
: folderEnd
);
196 } else if (levelNum
> FoldLevel::Base
) {
197 return 1 << MarkerOutline::FolderSub
;
200 if (levelNum
< levelNextNum
) {
202 return 1 << MarkerOutline::FolderSub
;
203 } else if (levelNum
> FoldLevel::Base
) {
204 return 1 << MarkerOutline::FolderSub
;
206 } else if (levelNum
> FoldLevel::Base
) {
207 return 1 << MarkerOutline::FolderSub
;
210 } else if (LevelIsWhitespace(level
)) {
211 if (needWhiteClosure
) {
212 if (LevelIsWhitespace(levelNext
)) {
213 return 1 << MarkerOutline::FolderSub
;
215 return 1 << TailFromNextLevel(levelNextNum
);
217 } else if (levelNum
> FoldLevel::Base
) {
218 if (levelNextNum
< levelNum
) {
219 return 1 << TailFromNextLevel(levelNextNum
);
221 return 1 << MarkerOutline::FolderSub
;
224 } else if (levelNum
> FoldLevel::Base
) {
225 if (levelNextNum
< levelNum
) {
226 if (LevelIsWhitespace(levelNext
)) {
227 return 1 << MarkerOutline::FolderSub
;
228 } else if (lastSubLine
) {
229 return 1 << TailFromNextLevel(levelNextNum
);
231 return 1 << MarkerOutline::FolderSub
;
234 return 1 << MarkerOutline::FolderSub
;
238 // No folding mark on this line
244 void MarginView::PaintOneMargin(Surface
*surface
, PRectangle rc
, PRectangle rcOneMargin
, const MarginStyle
&marginStyle
,
245 const EditModel
&model
, const ViewStyle
&vs
) {
246 const Point ptOrigin
= model
.GetVisibleOriginInMain();
247 const Sci::Line lineStartPaint
= static_cast<Sci::Line
>(rcOneMargin
.top
+ ptOrigin
.y
) / vs
.lineHeight
;
248 Sci::Line visibleLine
= model
.TopLineOfMain() + lineStartPaint
;
249 XYPOSITION yposScreen
= lineStartPaint
* vs
.lineHeight
- ptOrigin
.y
;
250 // Work out whether the top line is whitespace located after a
251 // lessening of fold level which implies a 'fold tail' but which should not
252 // be displayed until the last of a sequence of whitespace.
253 bool needWhiteClosure
= false;
254 if (marginStyle
.ShowsFolding()) {
255 const FoldLevel level
= model
.pdoc
->GetFoldLevel(model
.pcs
->DocFromDisplay(visibleLine
));
256 if (LevelIsWhitespace(level
)) {
257 Sci::Line lineBack
= model
.pcs
->DocFromDisplay(visibleLine
);
258 FoldLevel levelPrev
= level
;
259 while ((lineBack
> 0) && LevelIsWhitespace(levelPrev
)) {
261 levelPrev
= model
.pdoc
->GetFoldLevel(lineBack
);
263 if (!LevelIsHeader(levelPrev
)) {
264 if (LevelNumber(level
) < LevelNumber(levelPrev
))
265 needWhiteClosure
= true;
270 // Old code does not know about new markers needed to distinguish all cases
271 const MarkerOutline folderOpenMid
= SubstituteMarkerIfEmpty(MarkerOutline::FolderOpenMid
,
272 MarkerOutline::FolderOpen
, vs
);
273 const MarkerOutline folderEnd
= SubstituteMarkerIfEmpty(MarkerOutline::FolderEnd
,
274 MarkerOutline::Folder
, vs
);
276 while ((visibleLine
< model
.pcs
->LinesDisplayed()) && yposScreen
< rc
.bottom
) {
278 PLATFORM_ASSERT(visibleLine
< model
.pcs
->LinesDisplayed());
279 const Sci::Line lineDoc
= model
.pcs
->DocFromDisplay(visibleLine
);
280 PLATFORM_ASSERT((lineDoc
== 0) || model
.pcs
->GetVisible(lineDoc
));
281 const Sci::Line firstVisibleLine
= model
.pcs
->DisplayFromDoc(lineDoc
);
282 const Sci::Line lastVisibleLine
= model
.pcs
->DisplayLastFromDoc(lineDoc
);
283 const bool firstSubLine
= visibleLine
== firstVisibleLine
;
284 const bool lastSubLine
= visibleLine
== lastVisibleLine
;
286 int marks
= firstSubLine
? model
.pdoc
->GetMark(lineDoc
) : 0;
288 bool headWithTail
= false;
290 if (marginStyle
.ShowsFolding()) {
291 // Decide which fold indicator should be displayed
292 const FoldLevel level
= model
.pdoc
->GetFoldLevel(lineDoc
);
293 const FoldLevel levelNext
= model
.pdoc
->GetFoldLevel(lineDoc
+ 1);
294 const FoldLevel levelNum
= LevelNumberPart(level
);
295 const FoldLevel levelNextNum
= LevelNumberPart(levelNext
);
296 const bool isExpanded
= model
.pcs
->GetExpanded(lineDoc
);
298 marks
|= FoldingMark(level
, levelNext
, firstSubLine
, lastSubLine
,
299 isExpanded
, needWhiteClosure
, folderOpenMid
, folderEnd
);
301 // Change needWhiteClosure and headWithTail if needed
302 if (LevelIsHeader(level
)) {
303 needWhiteClosure
= false;
304 const Sci::Line firstFollowupLine
= model
.pcs
->DocFromDisplay(model
.pcs
->DisplayFromDoc(lineDoc
+ 1));
305 const FoldLevel firstFollowupLineLevel
= model
.pdoc
->GetFoldLevel(firstFollowupLine
);
306 const FoldLevel secondFollowupLineLevelNum
= LevelNumberPart(model
.pdoc
->GetFoldLevel(firstFollowupLine
+ 1));
308 if (LevelIsWhitespace(firstFollowupLineLevel
) &&
309 (levelNum
> secondFollowupLineLevelNum
))
310 needWhiteClosure
= true;
312 if (highlightDelimiter
.IsFoldBlockHighlighted(firstFollowupLine
))
315 } else if (LevelIsWhitespace(level
)) {
316 if (needWhiteClosure
) {
317 needWhiteClosure
= LevelIsWhitespace(levelNext
);
319 } else if (levelNum
> FoldLevel::Base
) {
320 if (levelNextNum
< levelNum
) {
321 needWhiteClosure
= LevelIsWhitespace(levelNext
);
326 const PRectangle
rcMarker(
330 yposScreen
+ vs
.lineHeight
);
331 if (marginStyle
.style
== MarginType::Number
) {
335 sNumber
= std::to_string(lineDoc
+ 1);
337 if (FlagSet(model
.foldFlags
, (FoldFlag::LevelNumbers
| FoldFlag::LineState
))) {
338 char number
[100] = "";
339 if (FlagSet(model
.foldFlags
, FoldFlag::LevelNumbers
)) {
340 const FoldLevel lev
= model
.pdoc
->GetFoldLevel(lineDoc
);
341 sprintf(number
, "%c%c %03X %03X",
342 LevelIsHeader(lev
) ? 'H' : '_',
343 LevelIsWhitespace(lev
) ? 'W' : '_',
345 static_cast<int>(lev
) >> 16
348 const int state
= model
.pdoc
->GetLineState(lineDoc
);
349 sprintf(number
, "%0X", state
);
353 PRectangle rcNumber
= rcMarker
;
355 const XYPOSITION width
= surface
->WidthText(vs
.styles
[StyleLineNumber
].font
.get(), sNumber
);
356 const XYPOSITION xpos
= rcNumber
.right
- width
- vs
.marginNumberPadding
;
357 rcNumber
.left
= xpos
;
358 DrawTextNoClipPhase(surface
, rcNumber
, vs
.styles
[StyleLineNumber
],
359 rcNumber
.top
+ vs
.maxAscent
, sNumber
, DrawPhase::all
);
360 } else if (FlagSet(vs
.wrap
.visualFlags
, WrapVisualFlag::Margin
)) {
361 PRectangle rcWrapMarker
= rcMarker
;
362 rcWrapMarker
.right
-= wrapMarkerPaddingRight
;
363 rcWrapMarker
.left
= rcWrapMarker
.right
- vs
.styles
[StyleLineNumber
].aveCharWidth
;
364 if (!customDrawWrapMarker
) {
365 DrawWrapMarker(surface
, rcWrapMarker
, false, vs
.styles
[StyleLineNumber
].fore
);
367 customDrawWrapMarker(surface
, rcWrapMarker
, false, vs
.styles
[StyleLineNumber
].fore
);
370 } else if (marginStyle
.style
== MarginType::Text
|| marginStyle
.style
== MarginType::RText
) {
371 const StyledText stMargin
= model
.pdoc
->MarginStyledText(lineDoc
);
372 if (stMargin
.text
&& ValidStyledText(vs
, vs
.marginStyleOffset
, stMargin
)) {
374 surface
->FillRectangle(rcMarker
,
375 vs
.styles
[stMargin
.StyleAt(0) + vs
.marginStyleOffset
].back
);
376 PRectangle rcText
= rcMarker
;
377 if (marginStyle
.style
== MarginType::RText
) {
378 const int width
= WidestLineWidth(surface
, vs
, vs
.marginStyleOffset
, stMargin
);
379 rcText
.left
= rcText
.right
- width
- 3;
381 DrawStyledText(surface
, vs
, vs
.marginStyleOffset
, rcText
,
382 stMargin
, 0, stMargin
.length
, DrawPhase::all
);
384 // if we're displaying annotation lines, colour the margin to match the associated document line
385 const int annotationLines
= model
.pdoc
->AnnotationLines(lineDoc
);
386 if (annotationLines
&& (visibleLine
> lastVisibleLine
- annotationLines
)) {
387 surface
->FillRectangle(rcMarker
, vs
.styles
[stMargin
.StyleAt(0) + vs
.marginStyleOffset
].back
);
393 marks
&= marginStyle
.mask
;
396 for (int markBit
= 0; (markBit
< 32) && marks
; markBit
++) {
398 LineMarker::FoldPart part
= LineMarker::FoldPart::undefined
;
399 if (marginStyle
.ShowsFolding() && highlightDelimiter
.IsFoldBlockHighlighted(lineDoc
)) {
400 if (highlightDelimiter
.IsBodyOfFoldBlock(lineDoc
)) {
401 part
= LineMarker::FoldPart::body
;
402 } else if (highlightDelimiter
.IsHeadOfFoldBlock(lineDoc
)) {
404 part
= headWithTail
? LineMarker::FoldPart::headWithTail
: LineMarker::FoldPart::head
;
406 if (model
.pcs
->GetExpanded(lineDoc
) || headWithTail
) {
407 part
= LineMarker::FoldPart::body
;
409 part
= LineMarker::FoldPart::undefined
;
412 } else if (highlightDelimiter
.IsTailOfFoldBlock(lineDoc
)) {
413 part
= LineMarker::FoldPart::tail
;
416 vs
.markers
[markBit
].Draw(surface
, rcMarker
, vs
.styles
[StyleLineNumber
].font
.get(), part
, marginStyle
.style
);
423 yposScreen
+= vs
.lineHeight
;
427 void MarginView::PaintMargin(Surface
*surface
, Sci::Line topLine
, PRectangle rc
, PRectangle rcMargin
,
428 const EditModel
&model
, const ViewStyle
&vs
) {
430 PRectangle rcOneMargin
= rcMargin
;
431 rcOneMargin
.right
= rcMargin
.left
;
432 if (rcOneMargin
.bottom
< rc
.bottom
)
433 rcOneMargin
.bottom
= rc
.bottom
;
435 const Point ptOrigin
= model
.GetVisibleOriginInMain();
436 for (const MarginStyle
&marginStyle
: vs
.ms
) {
437 if (marginStyle
.width
> 0) {
439 rcOneMargin
.left
= rcOneMargin
.right
;
440 rcOneMargin
.right
= rcOneMargin
.left
+ marginStyle
.width
;
442 if (marginStyle
.style
!= MarginType::Number
) {
443 if (marginStyle
.ShowsFolding()) {
444 // Required because of special way brush is created for selection margin
445 // Ensure patterns line up when scrolling with separate margin view
446 // by choosing correctly aligned variant.
447 const bool invertPhase
= static_cast<int>(ptOrigin
.y
) & 1;
448 surface
->FillRectangle(rcOneMargin
,
449 invertPhase
? *pixmapSelPattern
: *pixmapSelPatternOffset1
);
452 switch (marginStyle
.style
) {
453 case MarginType::Back
:
454 colour
= vs
.styles
[StyleDefault
].back
;
456 case MarginType::Fore
:
457 colour
= vs
.styles
[StyleDefault
].fore
;
459 case MarginType::Colour
:
460 colour
= marginStyle
.back
;
463 colour
= vs
.styles
[StyleLineNumber
].back
;
466 surface
->FillRectangle(rcOneMargin
, colour
);
469 surface
->FillRectangle(rcOneMargin
, vs
.styles
[StyleLineNumber
].back
);
472 if (marginStyle
.ShowsFolding() && highlightDelimiter
.isEnabled
) {
473 const Sci::Line lastLine
= model
.pcs
->DocFromDisplay(topLine
+ model
.LinesOnScreen()) + 1;
474 model
.pdoc
->GetHighlightDelimiters(highlightDelimiter
,
475 model
.pdoc
->SciLineFromPosition(model
.sel
.MainCaret()), lastLine
);
478 PaintOneMargin(surface
, rc
, rcOneMargin
, marginStyle
, model
, vs
);
482 PRectangle rcBlankMargin
= rcMargin
;
483 rcBlankMargin
.left
= rcOneMargin
.right
;
484 surface
->FillRectangle(rcBlankMargin
, vs
.styles
[StyleDefault
].back
);