1// Scintilla source code edit control
2/** @file LineMarker.cxx
3 ** Defines the look of a line marker in the margin.
4 **/
5// Copyright 1998-2011 by Neil Hodgson <neilh@scintilla.org>
6// The License.txt file describes the conditions under which this software may be distributed.
7
8#include <cstring>
9#include <cmath>
10
11#include <stdexcept>
12#include <string>
13#include <string_view>
14#include <vector>
15#include <map>
16#include <optional>
17#include <algorithm>
18#include <iterator>
19#include <memory>
20
21#include "ScintillaTypes.h"
22
23#include "Debugging.h"
24#include "Geometry.h"
25
26#include "Platform.h"
27
28#include "XPM.h"
29#include "LineMarker.h"
30#include "UniConversion.h"
31
32using namespace Scintilla;
33using namespace Scintilla::Internal;
34
35LineMarker::LineMarker(const LineMarker &other) {
36 // Defined to avoid pxpm and image being blindly copied, not as a complete copy constructor.
37 markType = other.markType;
38 fore = other.fore;
39 back = other.back;
40 backSelected = other.backSelected;
41 strokeWidth = other.strokeWidth;
42 layer = other.layer;
43 alpha = other.alpha;
44 if (other.pxpm)
45 pxpm = std::make_unique<XPM>(*other.pxpm);
46 else
47 pxpm = nullptr;
48 if (other.image)
49 image = std::make_unique<RGBAImage>(*other.image);
50 else
51 image = nullptr;
52 customDraw = other.customDraw;
53}
54
55LineMarker &LineMarker::operator=(const LineMarker &other) {
56 // Defined to avoid pxpm and image being blindly copied, not as a complete assignment operator.
57 if (this != &other) {
58 markType = other.markType;
59 fore = other.fore;
60 back = other.back;
61 backSelected = other.backSelected;
62 strokeWidth = other.strokeWidth;
63 layer = other.layer;
64 alpha = other.alpha;
65 if (other.pxpm)
66 pxpm = std::make_unique<XPM>(*other.pxpm);
67 else
68 pxpm = nullptr;
69 if (other.image)
70 image = std::make_unique<RGBAImage>(*other.image);
71 else
72 image = nullptr;
73 customDraw = other.customDraw;
74 }
75 return *this;
76}
77
78ColourRGBA LineMarker::BackWithAlpha() const noexcept {
79 return ColourRGBA(back, static_cast<int>(alpha));
80}
81
82void LineMarker::SetXPM(const char *textForm) {
83 pxpm = std::make_unique<XPM>(textForm);
84 markType = MarkerSymbol::Pixmap;
85}
86
87void LineMarker::SetXPM(const char *const *linesForm) {
88 pxpm = std::make_unique<XPM>(linesForm);
89 markType = MarkerSymbol::Pixmap;
90}
91
92void LineMarker::SetRGBAImage(Point sizeRGBAImage, float scale, const unsigned char *pixelsRGBAImage) {
93 image = std::make_unique<RGBAImage>(static_cast<int>(sizeRGBAImage.x), static_cast<int>(sizeRGBAImage.y), scale, pixelsRGBAImage);
94 markType = MarkerSymbol::RgbaImage;
95}
96
97namespace {
98
99enum class Expansion { Minus, Plus };
100enum class Shape { Square, Circle };
101
102void DrawSymbol(Surface *surface, Shape shape, Expansion expansion, PRectangle rcSymbol, XYPOSITION widthStroke,
103 ColourRGBA colourFill, ColourRGBA colourFrame, ColourRGBA colourFrameRight, ColourRGBA colourExpansion) {
104
105 const FillStroke fillStroke(colourFill, colourFrame, widthStroke);
106 const PRectangle rcSymbolLeft = Side(rcSymbol, Edge::left, (rcSymbol.Width() + widthStroke) / 2.0f);
107 surface->SetClip(rcSymbolLeft);
108 if (shape == Shape::Square) {
109 // Hollowed square
110 surface->RectangleDraw(rcSymbol, fillStroke);
111 } else {
112 surface->Ellipse(rcSymbol, fillStroke);
113 }
114 surface->PopClip();
115
116 const FillStroke fillStrokeRight(colourFill, colourFrameRight, widthStroke);
117 const PRectangle rcSymbolRight = Side(rcSymbol, Edge::right, (rcSymbol.Width() - widthStroke) / 2.0f);
118 surface->SetClip(rcSymbolRight);
119 if (shape == Shape::Square) {
120 surface->RectangleDraw(rcSymbol, fillStrokeRight);
121 } else {
122 surface->Ellipse(rcSymbol, fillStrokeRight);
123 }
124 surface->PopClip();
125
126 const PRectangle rcPlusMinus = rcSymbol.Inset(widthStroke + 1.0f);
127 const XYPOSITION armWidth = (rcPlusMinus.Width() - widthStroke) / 2.0f;
128 const XYPOSITION top = rcPlusMinus.top + armWidth;
129 const PRectangle rcH = PRectangle(
130 rcPlusMinus.left, top,
131 rcPlusMinus.right, top + widthStroke);
132 surface->FillRectangle(rcH, colourExpansion);
133 if (expansion == Expansion::Plus) {
134 const XYPOSITION left = rcPlusMinus.left + armWidth;
135 const PRectangle rcV = PRectangle(
136 left, rcPlusMinus.top,
137 left + widthStroke, rcPlusMinus.bottom);
138 surface->FillRectangle(rcV, colourExpansion);
139 }
140}
141
142void DrawTail(Surface *surface, XYPOSITION leftLine, XYPOSITION rightTail, XYPOSITION centreY, XYPOSITION widthSymbolStroke, ColourRGBA fill) {
143 const XYPOSITION slopeLength = 2.0f + widthSymbolStroke;
144 const XYPOSITION strokeTop = centreY + slopeLength;
145 const XYPOSITION halfWidth = widthSymbolStroke / 2.0f;
146 const XYPOSITION strokeMiddle = strokeTop + halfWidth;
147 Point lines[] = {
148 // Stick
149 Point(rightTail, strokeMiddle),
150 Point(leftLine + halfWidth + slopeLength, strokeMiddle),
151 // Slope
152 Point(leftLine + widthSymbolStroke / 2.0f, centreY + halfWidth),
153 };
154 surface->PolyLine(lines, std::size(lines), Stroke(fill, widthSymbolStroke));
155}
156
157}
158
159void LineMarker::DrawFoldingMark(Surface *surface, const PRectangle &rcWhole, FoldPart part) const {
160 // Assume: edges of rcWhole are integers.
161 // Code can only really handle integer strokeWidth.
162
163 ColourRGBA colourHead = back;
164 ColourRGBA colourBody = back;
165 ColourRGBA colourTail = back;
166
167 switch (part) {
168 case FoldPart::head:
169 case FoldPart::headWithTail:
170 colourHead = backSelected;
171 colourTail = backSelected;
172 break;
173 case FoldPart::body:
174 colourHead = backSelected;
175 colourBody = backSelected;
176 break;
177 case FoldPart::tail:
178 colourBody = backSelected;
179 colourTail = backSelected;
180 break;
181 default:
182 // LineMarker::undefined
183 break;
184 }
185
186 const int pixelDivisions = surface->PixelDivisions();
187
188 // Folding symbols should have equal height and width to be either a circle or square.
189 // So find the minimum of width and height.
190 const XYPOSITION minDimension = std::floor(std::min(rcWhole.Width(), rcWhole.Height() - 2)) - 1;
191
192 // If strokeWidth would take up too much of area reduce to reasonable width.
193 const XYPOSITION widthStroke = PixelAlignFloor(std::min(strokeWidth, minDimension / 5.0f), pixelDivisions);
194
195 // To centre +/-, odd strokeWidth -> odd symbol width, even -> even
196 const XYPOSITION widthSymbol =
197 ((std::lround(minDimension * pixelDivisions) % 2) == (std::lround(widthStroke * pixelDivisions) % 2)) ?
198 minDimension : minDimension - 1.0f / pixelDivisions;
199
200 const Point centre = PixelAlign(rcWhole.Centre(), pixelDivisions);
201
202 // Folder symbols and lines follow some rules to join up, fit the pixel grid,
203 // and avoid over-painting.
204
205 const XYPOSITION halfSymbol = std::round(widthSymbol / 2);
206 const Point topLeft(centre.x - halfSymbol, centre.y - halfSymbol);
207 const PRectangle rcSymbol(
208 topLeft.x, topLeft.y,
209 topLeft.x + widthSymbol, topLeft.y + widthSymbol);
210 const XYPOSITION leftLine = rcSymbol.Centre().x - widthStroke / 2.0f;
211 const XYPOSITION rightLine = leftLine + widthStroke;
212
213 // This is the vertical line through the whole area which is subdivided
214 // when there is a symbol on the line or the colour changes for highlighting.
215 const PRectangle rcVLine(leftLine, rcWhole.top, rightLine, rcWhole.bottom);
216
217 // Portions of rcVLine above and below the symbol.
218 const PRectangle rcAboveSymbol = Clamp(rcVLine, Edge::bottom, rcSymbol.top);
219 const PRectangle rcBelowSymbol = Clamp(rcVLine, Edge::top, rcSymbol.bottom);
220
221 // Projection to right.
222 const PRectangle rcStick(
223 rcVLine.right, centre.y + 1.0f - widthStroke,
224 rcWhole.right - 1, centre.y + 1.0f);
225
226 switch (markType) {
227
228 case MarkerSymbol::VLine:
229 surface->FillRectangle(rcVLine, colourBody);
230 break;
231
232 case MarkerSymbol::LCorner:
233 surface->FillRectangle(Clamp(rcVLine, Edge::bottom, centre.y + 1.0f), colourTail);
234 surface->FillRectangle(rcStick, colourTail);
235 break;
236
237 case MarkerSymbol::TCorner:
238 surface->FillRectangle(Clamp(rcVLine, Edge::bottom, centre.y + 1.0f), colourBody);
239 surface->FillRectangle(Clamp(rcVLine, Edge::top, centre.y + 1.0f), colourHead);
240 surface->FillRectangle(rcStick, colourTail);
241 break;
242
243 // CORNERCURVE cases divide slightly lower than CORNER to accommodate the curve
244 case MarkerSymbol::LCornerCurve:
245 surface->FillRectangle(Clamp(rcVLine, Edge::bottom, centre.y), colourTail);
246 DrawTail(surface, leftLine, rcWhole.right - 1.0f, centre.y - widthStroke,
247 widthStroke, colourTail);
248 break;
249
250 case MarkerSymbol::TCornerCurve:
251 surface->FillRectangle(Clamp(rcVLine, Edge::bottom, centre.y), colourBody);
252 surface->FillRectangle(Clamp(rcVLine, Edge::top, centre.y), colourHead);
253 DrawTail(surface, leftLine, rcWhole.right - 1.0f, centre.y - widthStroke,
254 widthStroke, colourTail);
255 break;
256
257 case MarkerSymbol::BoxPlus:
258 DrawSymbol(surface, Shape::Square, Expansion::Plus, rcSymbol, widthStroke,
259 fore, colourHead, colourHead, colourTail);
260 break;
261
262 case MarkerSymbol::BoxPlusConnected: {
263 const ColourRGBA colourBelow = (part == FoldPart::headWithTail) ? colourTail : colourBody;
264 surface->FillRectangle(rcBelowSymbol, colourBelow);
265 surface->FillRectangle(rcAboveSymbol, colourBody);
266
267 const ColourRGBA colourRight = (part == FoldPart::body) ? colourTail : colourHead;
268 DrawSymbol(surface, Shape::Square, Expansion::Plus, rcSymbol, widthStroke,
269 fore, colourHead, colourRight, colourTail);
270 }
271 break;
272
273 case MarkerSymbol::BoxMinus:
274 surface->FillRectangle(rcBelowSymbol, colourHead);
275 DrawSymbol(surface, Shape::Square, Expansion::Minus, rcSymbol, widthStroke,
276 fore, colourHead, colourHead, colourTail);
277 break;
278
279 case MarkerSymbol::BoxMinusConnected: {
280 surface->FillRectangle(rcBelowSymbol, colourHead);
281 surface->FillRectangle(rcAboveSymbol, colourBody);
282
283 const ColourRGBA colourRight = (part == FoldPart::body) ? colourTail : colourHead;
284 DrawSymbol(surface, Shape::Square, Expansion::Minus, rcSymbol, widthStroke,
285 fore, colourHead, colourRight, colourTail);
286 }
287 break;
288
289 case MarkerSymbol::CirclePlus:
290 DrawSymbol(surface, Shape::Circle, Expansion::Plus, rcSymbol, widthStroke,
291 fore, colourHead, colourHead, colourTail);
292 break;
293
294 case MarkerSymbol::CirclePlusConnected: {
295 const ColourRGBA colourBelow = (part == FoldPart::headWithTail) ? colourTail : colourBody;
296 surface->FillRectangle(rcBelowSymbol, colourBelow);
297 surface->FillRectangle(rcAboveSymbol, colourBody);
298
299 const ColourRGBA colourRight = (part == FoldPart::body) ? colourTail : colourHead;
300 DrawSymbol(surface, Shape::Circle, Expansion::Plus, rcSymbol, widthStroke,
301 fore, colourHead, colourRight, colourTail);
302 }
303 break;
304
305 case MarkerSymbol::CircleMinus:
306 surface->FillRectangle(rcBelowSymbol, colourHead);
307 DrawSymbol(surface, Shape::Circle, Expansion::Minus, rcSymbol, widthStroke,
308 fore, colourHead, colourHead, colourTail);
309 break;
310
311 case MarkerSymbol::CircleMinusConnected: {
312 surface->FillRectangle(rcBelowSymbol, colourHead);
313 surface->FillRectangle(rcAboveSymbol, colourBody);
314 const ColourRGBA colourRight = (part == FoldPart::body) ? colourTail : colourHead;
315 DrawSymbol(surface, Shape::Circle, Expansion::Minus, rcSymbol, widthStroke,
316 fore, colourHead, colourRight, colourTail);
317 }
318 break;
319
320 default:
321 break;
322
323 }
324}
325
326void LineMarker::AlignedPolygon(Surface *surface, const Point *pts, size_t npts) const {
327 const XYPOSITION move = strokeWidth / 2.0;
328 std::vector<Point> points;
329 std::transform(pts, pts + npts, std::back_inserter(points), [=](Point pt) noexcept ->Point {
330 return Point(pt.x + move, pt.y + move);
331 });
332 surface->Polygon(points.data(), std::size(points), FillStroke(back, fore, strokeWidth));
333}
334
335void LineMarker::Draw(Surface *surface, const PRectangle &rcWhole, const Font *fontForCharacter, FoldPart part, MarginType marginStyle) const {
336 // This is to satisfy the changed API - eventually the stroke width will be exposed to clients
337
338 if (customDraw) {
339 customDraw(surface, rcWhole, fontForCharacter, static_cast<int>(part), marginStyle, this);
340 return;
341 }
342
343 if ((markType == MarkerSymbol::Pixmap) && (pxpm)) {
344 pxpm->Draw(surface, rcWhole);
345 return;
346 }
347 if ((markType == MarkerSymbol::RgbaImage) && (image)) {
348 // Make rectangle just large enough to fit image centred on centre of rcWhole
349 PRectangle rcImage;
350 rcImage.top = ((rcWhole.top + rcWhole.bottom) - image->GetScaledHeight()) / 2;
351 rcImage.bottom = rcImage.top + image->GetScaledHeight();
352 rcImage.left = ((rcWhole.left + rcWhole.right) - image->GetScaledWidth()) / 2;
353 rcImage.right = rcImage.left + image->GetScaledWidth();
354 surface->DrawRGBAImage(rcImage, image->GetWidth(), image->GetHeight(), image->Pixels());
355 return;
356 }
357
358 if ((markType >= MarkerSymbol::VLine) && markType <= (MarkerSymbol::CircleMinusConnected)) {
359 DrawFoldingMark(surface, rcWhole, part);
360 return;
361 }
362
363 // Restrict most shapes a bit
364 const PRectangle rc(rcWhole.left, rcWhole.top + 1, rcWhole.right, rcWhole.bottom - 1);
365 // Ensure does not go beyond edge
366 const XYPOSITION minDim = std::min(rcWhole.Width(), rcWhole.Height() - 2) - 1;
367
368 const Point centre = rcWhole.Centre();
369 XYPOSITION centreX = std::floor(centre.x);
370 const XYPOSITION centreY = std::floor(centre.y);
371 const XYPOSITION dimOn2 = std::floor(minDim / 2);
372 const XYPOSITION dimOn4 = std::floor(minDim / 4);
373 const XYPOSITION armSize = dimOn2 - 2;
374 if (marginStyle == MarginType::Number || marginStyle == MarginType::Text || marginStyle == MarginType::RText) {
375 // On textual margins move marker to the left to try to avoid overlapping the text
376 centreX = rcWhole.left + dimOn2 + 1;
377 }
378
379 switch (markType) {
380 case MarkerSymbol::RoundRect: {
381 PRectangle rcRounded = rc;
382 rcRounded.left = rc.left + 1;
383 rcRounded.right = rc.right - 1;
384 surface->RoundedRectangle(rcRounded, FillStroke(back, fore, strokeWidth));
385 }
386 break;
387
388 case MarkerSymbol::Circle: {
389 const PRectangle rcCircle = PRectangle(
390 centreX - dimOn2,
391 centreY - dimOn2,
392 centreX + dimOn2,
393 centreY + dimOn2);
394 surface->Ellipse(rcCircle, FillStroke(back, fore, strokeWidth));
395 }
396 break;
397
398 case MarkerSymbol::Arrow: {
399 Point pts[] = {
400 Point(centreX - dimOn4, centreY - dimOn2),
401 Point(centreX - dimOn4, centreY + dimOn2),
402 Point(centreX + dimOn2 - dimOn4, centreY),
403 };
404 AlignedPolygon(surface, pts, std::size(pts));
405 }
406 break;
407
408 case MarkerSymbol::ArrowDown: {
409 Point pts[] = {
410 Point(centreX - dimOn2, centreY - dimOn4),
411 Point(centreX + dimOn2, centreY - dimOn4),
412 Point(centreX, centreY + dimOn2 - dimOn4),
413 };
414 AlignedPolygon(surface, pts, std::size(pts));
415 }
416 break;
417
418 case MarkerSymbol::Plus: {
419 Point pts[] = {
420 Point(centreX - armSize, centreY - 1),
421 Point(centreX - 1, centreY - 1),
422 Point(centreX - 1, centreY - armSize),
423 Point(centreX + 1, centreY - armSize),
424 Point(centreX + 1, centreY - 1),
425 Point(centreX + armSize, centreY - 1),
426 Point(centreX + armSize, centreY + 1),
427 Point(centreX + 1, centreY + 1),
428 Point(centreX + 1, centreY + armSize),
429 Point(centreX - 1, centreY + armSize),
430 Point(centreX - 1, centreY + 1),
431 Point(centreX - armSize, centreY + 1),
432 };
433 AlignedPolygon(surface, pts, std::size(pts));
434 }
435 break;
436
437 case MarkerSymbol::Minus: {
438 Point pts[] = {
439 Point(centreX - armSize, centreY - 1),
440 Point(centreX + armSize, centreY - 1),
441 Point(centreX + armSize, centreY + 1),
442 Point(centreX - armSize, centreY + 1),
443 };
444 AlignedPolygon(surface, pts, std::size(pts));
445 }
446 break;
447
448 case MarkerSymbol::SmallRect: {
449 PRectangle rcSmall;
450 rcSmall.left = rc.left + 1;
451 rcSmall.top = rc.top + 2;
452 rcSmall.right = rc.right - 1;
453 rcSmall.bottom = rc.bottom - 2;
454 surface->RectangleDraw(rcSmall, FillStroke(back, fore, strokeWidth));
455 }
456 break;
457
458 case MarkerSymbol::Empty:
459 case MarkerSymbol::Background:
460 case MarkerSymbol::Underline:
461 case MarkerSymbol::Available:
462 // An invisible marker so don't draw anything
463 break;
464
465 case MarkerSymbol::DotDotDot: {
466 XYPOSITION right = static_cast<XYPOSITION>(centreX - 6);
467 for (int b = 0; b < 3; b++) {
468 const PRectangle rcBlob(right, rc.bottom - 4, right + 2, rc.bottom - 2);
469 surface->FillRectangle(rcBlob, fore);
470 right += 5.0f;
471 }
472 }
473 break;
474
475 case MarkerSymbol::Arrows: {
476 XYPOSITION right = centreX - 4.0f + strokeWidth / 2.0f;
477 const XYPOSITION midY = centreY + strokeWidth / 2.0f;
478 const XYPOSITION armLength = std::round(dimOn2 - strokeWidth);
479 for (int b = 0; b < 3; b++) {
480 const Point pts[] = {
481 Point(right - armLength, midY - armLength),
482 Point(right, midY),
483 Point(right - armLength, midY + armLength)
484 };
485 surface->PolyLine(pts, std::size(pts), Stroke(fore, strokeWidth));
486 right += strokeWidth + 3.0f;
487 }
488 }
489 break;
490
491 case MarkerSymbol::ShortArrow: {
492 Point pts[] = {
493 Point(centreX, centreY + dimOn2),
494 Point(centreX + dimOn2, centreY),
495 Point(centreX, centreY - dimOn2),
496 Point(centreX, centreY - dimOn4),
497 Point(centreX - dimOn4, centreY - dimOn4),
498 Point(centreX - dimOn4, centreY + dimOn4),
499 Point(centreX, centreY + dimOn4),
500 Point(centreX, centreY + dimOn2),
501 };
502 AlignedPolygon(surface, pts, std::size(pts));
503 }
504 break;
505
506 case MarkerSymbol::FullRect:
507 surface->FillRectangle(rcWhole, back);
508 break;
509
510 case MarkerSymbol::LeftRect: {
511 PRectangle rcLeft = rcWhole;
512 rcLeft.right = rcLeft.left + 4;
513 surface->FillRectangle(rcLeft, back);
514 }
515 break;
516
517 case MarkerSymbol::Bookmark: {
518 const XYPOSITION halfHeight = std::floor(minDim / 3);
519 Point pts[] = {
520 Point(rcWhole.left, centreY - halfHeight),
521 Point(rcWhole.right - strokeWidth - 2, centreY - halfHeight),
522 Point(rcWhole.right - strokeWidth - 2 - halfHeight, centreY),
523 Point(rcWhole.right - strokeWidth - 2, centreY + halfHeight),
524 Point(rcWhole.left, centreY + halfHeight),
525 };
526 AlignedPolygon(surface, pts, std::size(pts));
527 }
528 break;
529
530 case MarkerSymbol::VerticalBookmark: {
531 const XYPOSITION halfWidth = std::floor(minDim / 3);
532 Point pts[] = {
533 Point(centreX - halfWidth, centreY - dimOn2),
534 Point(centreX + halfWidth, centreY - dimOn2),
535 Point(centreX + halfWidth, centreY + dimOn2),
536 Point(centreX, centreY + dimOn2 - halfWidth),
537 Point(centreX - halfWidth, centreY + dimOn2),
538 };
539 AlignedPolygon(surface, pts, std::size(pts));
540 }
541 break;
542
543 default:
544 if (markType >= MarkerSymbol::Character) {
545 char character[UTF8MaxBytes + 1] {};
546 const int uch = static_cast<int>(markType) - static_cast<int>(MarkerSymbol::Character);
547 UTF8FromUTF32Character(uch, character);
548 const XYPOSITION width = surface->WidthTextUTF8(fontForCharacter, character);
549 PRectangle rcText = rc;
550 rcText.left += (rc.Width() - width) / 2;
551 rcText.right = rcText.left + width;
552 surface->DrawTextNoClipUTF8(rcText, fontForCharacter, rcText.bottom - 2,
553 character, fore, back);
554 } else {
555 // treat as MarkerSymbol::FullRect
556 surface->FillRectangle(rcWhole, back);
557 }
558 break;
559 }
560}
561