1// Scintilla source code edit control
2/** @file MarginView.cxx
3 ** Defines the appearance of the editor margin.
4 **/
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.
7
8#include <cstddef>
9#include <cstdlib>
10#include <cstdint>
11#include <cassert>
12#include <cstring>
13#include <cstdio>
14#include <cmath>
15
16#include <stdexcept>
17#include <string>
18#include <string_view>
19#include <vector>
20#include <map>
21#include <set>
22#include <optional>
23#include <algorithm>
24#include <memory>
25
26#include "ScintillaTypes.h"
27#include "ScintillaMessages.h"
28#include "ScintillaStructures.h"
29#include "ILoader.h"
30#include "ILexer.h"
31
32#include "Debugging.h"
33#include "Geometry.h"
34#include "Platform.h"
35
36#include "CharacterCategoryMap.h"
37#include "Position.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"
44#include "KeyMap.h"
45#include "Indicator.h"
46#include "LineMarker.h"
47#include "Style.h"
48#include "ViewStyle.h"
49#include "CharClassify.h"
50#include "Decoration.h"
51#include "CaseFolder.h"
52#include "Document.h"
53#include "UniConversion.h"
54#include "Selection.h"
55#include "PositionCache.h"
56#include "EditModel.h"
57#include "MarginView.h"
58#include "EditView.h"
59
60using namespace Scintilla;
61
62namespace Scintilla::Internal {
63
64void DrawWrapMarker(Surface *surface, PRectangle rcPlace,
65 bool isEndMarker, ColourRGBA wrapColour) {
66
67 const XYPOSITION extraFinalPixel = surface->SupportsFeature(Supports::LineDrawsFinal) ? 0.0f : 1.0f;
68
69 const PRectangle rcAligned = PixelAlignOutside(rcPlace, surface->PixelDivisions());
70
71 const XYPOSITION widthStroke = std::floor(rcAligned.Width() / 6);
72
73 constexpr XYPOSITION xa = 1; // gap before start
74 const XYPOSITION w = rcAligned.Width() - xa - widthStroke;
75
76 // isEndMarker -> x-mirrored symbol for start marker
77
78 const XYPOSITION x0 = isEndMarker ? rcAligned.left : rcAligned.right - widthStroke;
79 const XYPOSITION y0 = rcAligned.top;
80
81 const XYPOSITION dy = std::floor(rcAligned.Height() / 5);
82 const XYPOSITION y = std::floor(rcAligned.Height() / 2) + dy;
83
84 struct Relative {
85 XYPOSITION xBase;
86 int xDir;
87 XYPOSITION yBase;
88 int yDir;
89 XYPOSITION halfWidth;
90 Point At(XYPOSITION xRelative, XYPOSITION yRelative) const noexcept {
91 return Point(xBase + xDir * xRelative + halfWidth, yBase + yDir * yRelative + halfWidth);
92 }
93 };
94
95 Relative rel = { x0, isEndMarker ? 1 : -1, y0, 1, widthStroke / 2.0f };
96
97 // arrow head
98 const Point head[] = {
99 rel.At(xa + dy, y - dy),
100 rel.At(xa, y),
101 rel.At(xa + dy + extraFinalPixel, y + dy + extraFinalPixel)
102 };
103 surface->PolyLine(head, std::size(head), Stroke(wrapColour, widthStroke));
104
105 // arrow body
106 const Point body[] = {
107 rel.At(xa, y),
108 rel.At(xa + w, y),
109 rel.At(xa + w, y - 2 * dy),
110 rel.At(xa, y - 2 * dy),
111 };
112 surface->PolyLine(body, std::size(body), Stroke(wrapColour, widthStroke));
113}
114
115MarginView::MarginView() noexcept {
116 wrapMarkerPaddingRight = 3;
117 customDrawWrapMarker = nullptr;
118}
119
120void MarginView::DropGraphics() noexcept {
121 pixmapSelMargin.reset();
122 pixmapSelPattern.reset();
123 pixmapSelPatternOffset1.reset();
124}
125
126void 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);
136
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;
140
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;
145 }
146
147 if (vsDraw.foldmarginColour) {
148 // override default fold margin colour
149 colourFMFill = *vsDraw.foldmarginColour;
150 }
151 if (vsDraw.foldmarginHighlightColour) {
152 // override default fold margin highlight colour
153 colourFMStripes = *vsDraw.foldmarginHighlightColour;
154 }
155
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);
163 }
164 }
165 pixmapSelPattern->FlushDrawing();
166 pixmapSelPatternOffset1->FlushDrawing();
167 }
168}
169
170namespace {
171
172MarkerOutline SubstituteMarkerIfEmpty(MarkerOutline markerCheck, MarkerOutline markerDefault, const ViewStyle &vs) noexcept {
173 if (vs.markers[static_cast<size_t>(markerCheck)].markType == MarkerSymbol::Empty)
174 return markerDefault;
175 return markerCheck;
176}
177
178constexpr MarkerOutline TailFromNextLevel(FoldLevel levelNextNum) noexcept {
179 return (levelNextNum > FoldLevel::Base) ? MarkerOutline::FolderMidTail : MarkerOutline::FolderTail;
180}
181
182int FoldingMark(FoldLevel level, FoldLevel levelNext, bool firstSubLine, bool lastSubLine,
183 bool isExpanded, bool needWhiteClosure, MarkerOutline folderOpenMid, MarkerOutline folderEnd) noexcept {
184
185 const FoldLevel levelNum = LevelNumberPart(level);
186 const FoldLevel levelNextNum = LevelNumberPart(levelNext);
187
188 if (LevelIsHeader(level)) {
189 if (firstSubLine) {
190 if (levelNum < levelNextNum) {
191 if (levelNum == FoldLevel::Base) {
192 return 1 << (isExpanded ? MarkerOutline::FolderOpen : MarkerOutline::Folder);
193 } else {
194 return 1 << (isExpanded ? folderOpenMid : folderEnd);
195 }
196 } else if (levelNum > FoldLevel::Base) {
197 return 1 << MarkerOutline::FolderSub;
198 }
199 } else {
200 if (levelNum < levelNextNum) {
201 if (isExpanded) {
202 return 1 << MarkerOutline::FolderSub;
203 } else if (levelNum > FoldLevel::Base) {
204 return 1 << MarkerOutline::FolderSub;
205 }
206 } else if (levelNum > FoldLevel::Base) {
207 return 1 << MarkerOutline::FolderSub;
208 }
209 }
210 } else if (LevelIsWhitespace(level)) {
211 if (needWhiteClosure) {
212 if (LevelIsWhitespace(levelNext)) {
213 return 1 << MarkerOutline::FolderSub;
214 } else {
215 return 1 << TailFromNextLevel(levelNextNum);
216 }
217 } else if (levelNum > FoldLevel::Base) {
218 if (levelNextNum < levelNum) {
219 return 1 << TailFromNextLevel(levelNextNum);
220 } else {
221 return 1 << MarkerOutline::FolderSub;
222 }
223 }
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);
230 } else {
231 return 1 << MarkerOutline::FolderSub;
232 }
233 } else {
234 return 1 << MarkerOutline::FolderSub;
235 }
236 }
237
238 // No folding mark on this line
239 return 0;
240}
241
242}
243
244void 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)) {
260 lineBack--;
261 levelPrev = model.pdoc->GetFoldLevel(lineBack);
262 }
263 if (!LevelIsHeader(levelPrev)) {
264 if (LevelNumber(level) < LevelNumber(levelPrev))
265 needWhiteClosure = true;
266 }
267 }
268 }
269
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);
275
276 while ((visibleLine < model.pcs->LinesDisplayed()) && yposScreen < rc.bottom) {
277
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;
285
286 int marks = firstSubLine ? model.pdoc->GetMark(lineDoc) : 0;
287
288 bool headWithTail = false;
289
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);
297
298 marks |= FoldingMark(level, levelNext, firstSubLine, lastSubLine,
299 isExpanded, needWhiteClosure, folderOpenMid, folderEnd);
300
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));
307 if (!isExpanded) {
308 if (LevelIsWhitespace(firstFollowupLineLevel) &&
309 (levelNum > secondFollowupLineLevelNum))
310 needWhiteClosure = true;
311
312 if (highlightDelimiter.IsFoldBlockHighlighted(firstFollowupLine))
313 headWithTail = true;
314 }
315 } else if (LevelIsWhitespace(level)) {
316 if (needWhiteClosure) {
317 needWhiteClosure = LevelIsWhitespace(levelNext);
318 }
319 } else if (levelNum > FoldLevel::Base) {
320 if (levelNextNum < levelNum) {
321 needWhiteClosure = LevelIsWhitespace(levelNext);
322 }
323 }
324 }
325
326 const PRectangle rcMarker(
327 rcOneMargin.left,
328 yposScreen,
329 rcOneMargin.right,
330 yposScreen + vs.lineHeight);
331 if (marginStyle.style == MarginType::Number) {
332 if (firstSubLine) {
333 std::string sNumber;
334 if (lineDoc >= 0) {
335 sNumber = std::to_string(lineDoc + 1);
336 }
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' : '_',
344 LevelNumber(lev),
345 static_cast<int>(lev) >> 16
346 );
347 } else {
348 const int state = model.pdoc->GetLineState(lineDoc);
349 sprintf(number, "%0X", state);
350 }
351 sNumber = number;
352 }
353 PRectangle rcNumber = rcMarker;
354 // Right justify
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);
366 } else {
367 customDrawWrapMarker(surface, rcWrapMarker, false, vs.styles[StyleLineNumber].fore);
368 }
369 }
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)) {
373 if (firstSubLine) {
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;
380 }
381 DrawStyledText(surface, vs, vs.marginStyleOffset, rcText,
382 stMargin, 0, stMargin.length, DrawPhase::all);
383 } else {
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);
388 }
389 }
390 }
391 }
392
393 marks &= marginStyle.mask;
394
395 if (marks) {
396 for (int markBit = 0; (markBit < 32) && marks; markBit++) {
397 if (marks & 1) {
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)) {
403 if (firstSubLine) {
404 part = headWithTail ? LineMarker::FoldPart::headWithTail : LineMarker::FoldPart::head;
405 } else {
406 if (model.pcs->GetExpanded(lineDoc) || headWithTail) {
407 part = LineMarker::FoldPart::body;
408 } else {
409 part = LineMarker::FoldPart::undefined;
410 }
411 }
412 } else if (highlightDelimiter.IsTailOfFoldBlock(lineDoc)) {
413 part = LineMarker::FoldPart::tail;
414 }
415 }
416 vs.markers[markBit].Draw(surface, rcMarker, vs.styles[StyleLineNumber].font.get(), part, marginStyle.style);
417 }
418 marks >>= 1;
419 }
420 }
421
422 visibleLine++;
423 yposScreen += vs.lineHeight;
424 }
425}
426
427void MarginView::PaintMargin(Surface *surface, Sci::Line topLine, PRectangle rc, PRectangle rcMargin,
428 const EditModel &model, const ViewStyle &vs) {
429
430 PRectangle rcOneMargin = rcMargin;
431 rcOneMargin.right = rcMargin.left;
432 if (rcOneMargin.bottom < rc.bottom)
433 rcOneMargin.bottom = rc.bottom;
434
435 const Point ptOrigin = model.GetVisibleOriginInMain();
436 for (const MarginStyle &marginStyle : vs.ms) {
437 if (marginStyle.width > 0) {
438
439 rcOneMargin.left = rcOneMargin.right;
440 rcOneMargin.right = rcOneMargin.left + marginStyle.width;
441
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);
450 } else {
451 ColourRGBA colour;
452 switch (marginStyle.style) {
453 case MarginType::Back:
454 colour = vs.styles[StyleDefault].back;
455 break;
456 case MarginType::Fore:
457 colour = vs.styles[StyleDefault].fore;
458 break;
459 case MarginType::Colour:
460 colour = marginStyle.back;
461 break;
462 default:
463 colour = vs.styles[StyleLineNumber].back;
464 break;
465 }
466 surface->FillRectangle(rcOneMargin, colour);
467 }
468 } else {
469 surface->FillRectangle(rcOneMargin, vs.styles[StyleLineNumber].back);
470 }
471
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);
476 }
477
478 PaintOneMargin(surface, rc, rcOneMargin, marginStyle, model, vs);
479 }
480 }
481
482 PRectangle rcBlankMargin = rcMargin;
483 rcBlankMargin.left = rcOneMargin.right;
484 surface->FillRectangle(rcBlankMargin, vs.styles[StyleDefault].back);
485}
486
487}
488
489