notepad-plus-plus/scintilla/src/LineMarker.cxx

592 lines
20 KiB
C++

// Scintilla source code edit control
/** @file LineMarker.cxx
** Defines the look of a line marker in the margin.
**/
// Copyright 1998-2011 by Neil Hodgson <neilh@scintilla.org>
// The License.txt file describes the conditions under which this software may be distributed.
#include <cstring>
#include <cmath>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
#include <map>
#include <optional>
#include <algorithm>
#include <iterator>
#include <memory>
#include "ScintillaTypes.h"
#include "Debugging.h"
#include "Geometry.h"
#include "Platform.h"
#include "XPM.h"
#include "LineMarker.h"
#include "UniConversion.h"
using namespace Scintilla;
using namespace Scintilla::Internal;
LineMarker::LineMarker(const LineMarker &other) {
// Defined to avoid pxpm and image being blindly copied, not as a complete copy constructor.
markType = other.markType;
fore = other.fore;
back = other.back;
backSelected = other.backSelected;
strokeWidth = other.strokeWidth;
layer = other.layer;
alpha = other.alpha;
if (other.pxpm)
pxpm = std::make_unique<XPM>(*other.pxpm);
else
pxpm = nullptr;
if (other.image)
image = std::make_unique<RGBAImage>(*other.image);
else
image = nullptr;
customDraw = other.customDraw;
}
LineMarker &LineMarker::operator=(const LineMarker &other) {
// Defined to avoid pxpm and image being blindly copied, not as a complete assignment operator.
if (this != &other) {
markType = other.markType;
fore = other.fore;
back = other.back;
backSelected = other.backSelected;
strokeWidth = other.strokeWidth;
layer = other.layer;
alpha = other.alpha;
if (other.pxpm)
pxpm = std::make_unique<XPM>(*other.pxpm);
else
pxpm = nullptr;
if (other.image)
image = std::make_unique<RGBAImage>(*other.image);
else
image = nullptr;
customDraw = other.customDraw;
}
return *this;
}
ColourRGBA LineMarker::BackWithAlpha() const noexcept {
return ColourRGBA(back, static_cast<int>(alpha));
}
void LineMarker::SetXPM(const char *textForm) {
pxpm = std::make_unique<XPM>(textForm);
markType = MarkerSymbol::Pixmap;
}
void LineMarker::SetXPM(const char *const *linesForm) {
pxpm = std::make_unique<XPM>(linesForm);
markType = MarkerSymbol::Pixmap;
}
void LineMarker::SetRGBAImage(Point sizeRGBAImage, float scale, const unsigned char *pixelsRGBAImage) {
image = std::make_unique<RGBAImage>(static_cast<int>(sizeRGBAImage.x), static_cast<int>(sizeRGBAImage.y), scale, pixelsRGBAImage);
markType = MarkerSymbol::RgbaImage;
}
namespace {
enum class Expansion { Minus, Plus };
enum class Shape { Square, Circle };
void DrawSymbol(Surface *surface, Shape shape, Expansion expansion, PRectangle rcSymbol, XYPOSITION widthStroke,
ColourRGBA colourFill, ColourRGBA colourFrame, ColourRGBA colourFrameRight, ColourRGBA colourExpansion) {
const FillStroke fillStroke(colourFill, colourFrame, widthStroke);
const PRectangle rcSymbolLeft = Side(rcSymbol, Edge::left, (rcSymbol.Width() + widthStroke) / 2.0f);
surface->SetClip(rcSymbolLeft);
if (shape == Shape::Square) {
// Hollowed square
surface->RectangleDraw(rcSymbol, fillStroke);
} else {
surface->Ellipse(rcSymbol, fillStroke);
}
surface->PopClip();
const FillStroke fillStrokeRight(colourFill, colourFrameRight, widthStroke);
const PRectangle rcSymbolRight = Side(rcSymbol, Edge::right, (rcSymbol.Width() - widthStroke) / 2.0f);
surface->SetClip(rcSymbolRight);
if (shape == Shape::Square) {
surface->RectangleDraw(rcSymbol, fillStrokeRight);
} else {
surface->Ellipse(rcSymbol, fillStrokeRight);
}
surface->PopClip();
const PRectangle rcPlusMinus = rcSymbol.Inset(widthStroke + 1.0f);
const XYPOSITION armWidth = (rcPlusMinus.Width() - widthStroke) / 2.0f;
const XYPOSITION top = rcPlusMinus.top + armWidth;
const PRectangle rcH = PRectangle(
rcPlusMinus.left, top,
rcPlusMinus.right, top + widthStroke);
surface->FillRectangle(rcH, colourExpansion);
if (expansion == Expansion::Plus) {
const XYPOSITION left = rcPlusMinus.left + armWidth;
const PRectangle rcV = PRectangle(
left, rcPlusMinus.top,
left + widthStroke, rcPlusMinus.bottom);
surface->FillRectangle(rcV, colourExpansion);
}
}
void DrawTail(Surface *surface, XYPOSITION leftLine, XYPOSITION rightTail, XYPOSITION centreY, XYPOSITION widthSymbolStroke, ColourRGBA fill) {
const XYPOSITION slopeLength = 2.0f + widthSymbolStroke;
const XYPOSITION strokeTop = centreY + slopeLength;
const XYPOSITION halfWidth = widthSymbolStroke / 2.0f;
const XYPOSITION strokeMiddle = strokeTop + halfWidth;
const Point lines[] = {
// Stick
Point(rightTail, strokeMiddle),
Point(leftLine + halfWidth + slopeLength, strokeMiddle),
// Slope
Point(leftLine + widthSymbolStroke / 2.0f, centreY + halfWidth),
};
surface->PolyLine(lines, std::size(lines), Stroke(fill, widthSymbolStroke));
}
}
void LineMarker::DrawFoldingMark(Surface *surface, const PRectangle &rcWhole, FoldPart part) const {
// Assume: edges of rcWhole are integers.
// Code can only really handle integer strokeWidth.
ColourRGBA colourHead = back;
ColourRGBA colourBody = back;
ColourRGBA colourTail = back;
switch (part) {
case FoldPart::head:
case FoldPart::headWithTail:
colourHead = backSelected;
colourTail = backSelected;
break;
case FoldPart::body:
colourHead = backSelected;
colourBody = backSelected;
break;
case FoldPart::tail:
colourBody = backSelected;
colourTail = backSelected;
break;
default:
// LineMarker::undefined
break;
}
const int pixelDivisions = surface->PixelDivisions();
// Folding symbols should have equal height and width to be either a circle or square.
// So find the minimum of width and height.
const XYPOSITION minDimension = std::floor(std::min(rcWhole.Width(), rcWhole.Height() - 2)) - 1;
// If strokeWidth would take up too much of area reduce to reasonable width.
const XYPOSITION widthStroke = PixelAlignFloor(std::min(strokeWidth, minDimension / 5.0f), pixelDivisions);
// To centre +/-, odd strokeWidth -> odd symbol width, even -> even
const XYPOSITION widthSymbol =
((std::lround(minDimension * pixelDivisions) % 2) == (std::lround(widthStroke * pixelDivisions) % 2)) ?
minDimension : minDimension - 1.0f / pixelDivisions;
const Point centre = PixelAlign(rcWhole.Centre(), pixelDivisions);
// Folder symbols and lines follow some rules to join up, fit the pixel grid,
// and avoid over-painting.
const XYPOSITION halfSymbol = std::round(widthSymbol / 2);
const Point topLeft(centre.x - halfSymbol, centre.y - halfSymbol);
const PRectangle rcSymbol(
topLeft.x, topLeft.y,
topLeft.x + widthSymbol, topLeft.y + widthSymbol);
const XYPOSITION leftLine = rcSymbol.Centre().x - widthStroke / 2.0f;
const XYPOSITION rightLine = leftLine + widthStroke;
// This is the vertical line through the whole area which is subdivided
// when there is a symbol on the line or the colour changes for highlighting.
const PRectangle rcVLine(leftLine, rcWhole.top, rightLine, rcWhole.bottom);
// Portions of rcVLine above and below the symbol.
const PRectangle rcAboveSymbol = Clamp(rcVLine, Edge::bottom, rcSymbol.top);
const PRectangle rcBelowSymbol = Clamp(rcVLine, Edge::top, rcSymbol.bottom);
// Projection to right.
const PRectangle rcStick(
rcVLine.right, centre.y + 1.0f - widthStroke,
rcWhole.right - 1, centre.y + 1.0f);
switch (markType) {
case MarkerSymbol::VLine:
surface->FillRectangle(rcVLine, colourBody);
break;
case MarkerSymbol::LCorner:
surface->FillRectangle(Clamp(rcVLine, Edge::bottom, centre.y + 1.0f), colourTail);
surface->FillRectangle(rcStick, colourTail);
break;
case MarkerSymbol::TCorner:
surface->FillRectangle(Clamp(rcVLine, Edge::bottom, centre.y + 1.0f), colourBody);
surface->FillRectangle(Clamp(rcVLine, Edge::top, centre.y + 1.0f), colourHead);
surface->FillRectangle(rcStick, colourTail);
break;
// CORNERCURVE cases divide slightly lower than CORNER to accommodate the curve
case MarkerSymbol::LCornerCurve:
surface->FillRectangle(Clamp(rcVLine, Edge::bottom, centre.y), colourTail);
DrawTail(surface, leftLine, rcWhole.right - 1.0f, centre.y - widthStroke,
widthStroke, colourTail);
break;
case MarkerSymbol::TCornerCurve:
surface->FillRectangle(Clamp(rcVLine, Edge::bottom, centre.y), colourBody);
surface->FillRectangle(Clamp(rcVLine, Edge::top, centre.y), colourHead);
DrawTail(surface, leftLine, rcWhole.right - 1.0f, centre.y - widthStroke,
widthStroke, colourTail);
break;
case MarkerSymbol::BoxPlus:
DrawSymbol(surface, Shape::Square, Expansion::Plus, rcSymbol, widthStroke,
fore, colourHead, colourHead, colourTail);
break;
case MarkerSymbol::BoxPlusConnected: {
const ColourRGBA colourBelow = (part == FoldPart::headWithTail) ? colourTail : colourBody;
surface->FillRectangle(rcBelowSymbol, colourBelow);
surface->FillRectangle(rcAboveSymbol, colourBody);
const ColourRGBA colourRight = (part == FoldPart::body) ? colourTail : colourHead;
DrawSymbol(surface, Shape::Square, Expansion::Plus, rcSymbol, widthStroke,
fore, colourHead, colourRight, colourTail);
}
break;
case MarkerSymbol::BoxMinus:
surface->FillRectangle(rcBelowSymbol, colourHead);
DrawSymbol(surface, Shape::Square, Expansion::Minus, rcSymbol, widthStroke,
fore, colourHead, colourHead, colourTail);
break;
case MarkerSymbol::BoxMinusConnected: {
surface->FillRectangle(rcBelowSymbol, colourHead);
surface->FillRectangle(rcAboveSymbol, colourBody);
const ColourRGBA colourRight = (part == FoldPart::body) ? colourTail : colourHead;
DrawSymbol(surface, Shape::Square, Expansion::Minus, rcSymbol, widthStroke,
fore, colourHead, colourRight, colourTail);
}
break;
case MarkerSymbol::CirclePlus:
DrawSymbol(surface, Shape::Circle, Expansion::Plus, rcSymbol, widthStroke,
fore, colourHead, colourHead, colourTail);
break;
case MarkerSymbol::CirclePlusConnected: {
const ColourRGBA colourBelow = (part == FoldPart::headWithTail) ? colourTail : colourBody;
surface->FillRectangle(rcBelowSymbol, colourBelow);
surface->FillRectangle(rcAboveSymbol, colourBody);
const ColourRGBA colourRight = (part == FoldPart::body) ? colourTail : colourHead;
DrawSymbol(surface, Shape::Circle, Expansion::Plus, rcSymbol, widthStroke,
fore, colourHead, colourRight, colourTail);
}
break;
case MarkerSymbol::CircleMinus:
surface->FillRectangle(rcBelowSymbol, colourHead);
DrawSymbol(surface, Shape::Circle, Expansion::Minus, rcSymbol, widthStroke,
fore, colourHead, colourHead, colourTail);
break;
case MarkerSymbol::CircleMinusConnected: {
surface->FillRectangle(rcBelowSymbol, colourHead);
surface->FillRectangle(rcAboveSymbol, colourBody);
const ColourRGBA colourRight = (part == FoldPart::body) ? colourTail : colourHead;
DrawSymbol(surface, Shape::Circle, Expansion::Minus, rcSymbol, widthStroke,
fore, colourHead, colourRight, colourTail);
}
break;
default:
break;
}
}
void LineMarker::AlignedPolygon(Surface *surface, const Point *pts, size_t npts) const {
const XYPOSITION move = strokeWidth / 2.0;
std::vector<Point> points;
std::transform(pts, pts + npts, std::back_inserter(points), [=](Point pt) noexcept ->Point {
return Point(pt.x + move, pt.y + move);
});
surface->Polygon(points.data(), std::size(points), FillStroke(back, fore, strokeWidth));
}
void LineMarker::Draw(Surface *surface, const PRectangle &rcWhole, const Font *fontForCharacter, FoldPart part, MarginType marginStyle) const {
// This is to satisfy the changed API - eventually the stroke width will be exposed to clients
if (customDraw) {
customDraw(surface, rcWhole, fontForCharacter, static_cast<int>(part), marginStyle, this);
return;
}
if ((markType == MarkerSymbol::Pixmap) && (pxpm)) {
pxpm->Draw(surface, rcWhole);
return;
}
if ((markType == MarkerSymbol::RgbaImage) && (image)) {
// Make rectangle just large enough to fit image centred on centre of rcWhole
PRectangle rcImage;
rcImage.top = ((rcWhole.top + rcWhole.bottom) - image->GetScaledHeight()) / 2;
rcImage.bottom = rcImage.top + image->GetScaledHeight();
rcImage.left = ((rcWhole.left + rcWhole.right) - image->GetScaledWidth()) / 2;
rcImage.right = rcImage.left + image->GetScaledWidth();
surface->DrawRGBAImage(rcImage, image->GetWidth(), image->GetHeight(), image->Pixels());
return;
}
if ((markType >= MarkerSymbol::VLine) && markType <= (MarkerSymbol::CircleMinusConnected)) {
DrawFoldingMark(surface, rcWhole, part);
return;
}
// Restrict most shapes a bit
const PRectangle rc(rcWhole.left, rcWhole.top + 1, rcWhole.right, rcWhole.bottom - 1);
// Ensure does not go beyond edge
const XYPOSITION minDim = std::min(rcWhole.Width(), rcWhole.Height() - 2) - 1;
const Point centre = rcWhole.Centre();
XYPOSITION centreX = std::floor(centre.x);
const XYPOSITION centreY = std::floor(centre.y);
const XYPOSITION dimOn2 = std::floor(minDim / 2);
const XYPOSITION dimOn4 = std::floor(minDim / 4);
const XYPOSITION armSize = dimOn2 - 2;
if (marginStyle == MarginType::Number || marginStyle == MarginType::Text || marginStyle == MarginType::RText) {
// On textual margins move marker to the left to try to avoid overlapping the text
centreX = rcWhole.left + dimOn2 + 1;
}
switch (markType) {
case MarkerSymbol::RoundRect: {
PRectangle rcRounded = rc;
rcRounded.left = rc.left + 1;
rcRounded.right = rc.right - 1;
surface->RoundedRectangle(rcRounded, FillStroke(back, fore, strokeWidth));
}
break;
case MarkerSymbol::Circle: {
const PRectangle rcCircle = PRectangle(
centreX - dimOn2,
centreY - dimOn2,
centreX + dimOn2,
centreY + dimOn2);
surface->Ellipse(rcCircle, FillStroke(back, fore, strokeWidth));
}
break;
case MarkerSymbol::Arrow: {
const Point pts[] = {
Point(centreX - dimOn4, centreY - dimOn2),
Point(centreX - dimOn4, centreY + dimOn2),
Point(centreX + dimOn2 - dimOn4, centreY),
};
AlignedPolygon(surface, pts, std::size(pts));
}
break;
case MarkerSymbol::ArrowDown: {
const Point pts[] = {
Point(centreX - dimOn2, centreY - dimOn4),
Point(centreX + dimOn2, centreY - dimOn4),
Point(centreX, centreY + dimOn2 - dimOn4),
};
AlignedPolygon(surface, pts, std::size(pts));
}
break;
case MarkerSymbol::Plus: {
const Point pts[] = {
Point(centreX - armSize, centreY - 1),
Point(centreX - 1, centreY - 1),
Point(centreX - 1, centreY - armSize),
Point(centreX + 1, centreY - armSize),
Point(centreX + 1, centreY - 1),
Point(centreX + armSize, centreY - 1),
Point(centreX + armSize, centreY + 1),
Point(centreX + 1, centreY + 1),
Point(centreX + 1, centreY + armSize),
Point(centreX - 1, centreY + armSize),
Point(centreX - 1, centreY + 1),
Point(centreX - armSize, centreY + 1),
};
AlignedPolygon(surface, pts, std::size(pts));
}
break;
case MarkerSymbol::Minus: {
const Point pts[] = {
Point(centreX - armSize, centreY - 1),
Point(centreX + armSize, centreY - 1),
Point(centreX + armSize, centreY + 1),
Point(centreX - armSize, centreY + 1),
};
AlignedPolygon(surface, pts, std::size(pts));
}
break;
case MarkerSymbol::SmallRect: {
PRectangle rcSmall;
rcSmall.left = rc.left + 1;
rcSmall.top = rc.top + 2;
rcSmall.right = rc.right - 1;
rcSmall.bottom = rc.bottom - 2;
surface->RectangleDraw(rcSmall, FillStroke(back, fore, strokeWidth));
}
break;
case MarkerSymbol::Empty:
case MarkerSymbol::Background:
case MarkerSymbol::Underline:
case MarkerSymbol::Available:
// An invisible marker so don't draw anything
break;
case MarkerSymbol::DotDotDot: {
XYPOSITION right = static_cast<XYPOSITION>(centreX - 6);
for (int b = 0; b < 3; b++) {
const PRectangle rcBlob(right, rc.bottom - 4, right + 2, rc.bottom - 2);
surface->FillRectangle(rcBlob, fore);
right += 5.0f;
}
}
break;
case MarkerSymbol::Arrows: {
XYPOSITION right = centreX - 4.0f + strokeWidth / 2.0f;
const XYPOSITION midY = centreY + strokeWidth / 2.0f;
const XYPOSITION armLength = std::round(dimOn2 - strokeWidth);
for (int b = 0; b < 3; b++) {
const Point pts[] = {
Point(right - armLength, midY - armLength),
Point(right, midY),
Point(right - armLength, midY + armLength)
};
surface->PolyLine(pts, std::size(pts), Stroke(fore, strokeWidth));
right += strokeWidth + 3.0f;
}
}
break;
case MarkerSymbol::ShortArrow: {
const Point pts[] = {
Point(centreX, centreY + dimOn2),
Point(centreX + dimOn2, centreY),
Point(centreX, centreY - dimOn2),
Point(centreX, centreY - dimOn4),
Point(centreX - dimOn4, centreY - dimOn4),
Point(centreX - dimOn4, centreY + dimOn4),
Point(centreX, centreY + dimOn4),
Point(centreX, centreY + dimOn2),
};
AlignedPolygon(surface, pts, std::size(pts));
}
break;
case MarkerSymbol::FullRect:
surface->FillRectangle(rcWhole, back);
break;
case MarkerSymbol::LeftRect: {
PRectangle rcLeft = rcWhole;
rcLeft.right = rcLeft.left + 4;
surface->FillRectangle(rcLeft, back);
}
break;
case MarkerSymbol::Bar: {
PRectangle rcBar = rcWhole;
const XYPOSITION widthBar = std::floor(rcWhole.Width() / 3.0);
rcBar.left = centreX - std::floor(widthBar / 2.0);
rcBar.right = rcBar.left + widthBar;
surface->SetClip(rcWhole);
switch (part) {
case LineMarker::FoldPart::headWithTail:
surface->RectangleDraw(rcBar, FillStroke(back, fore, strokeWidth));
break;
case LineMarker::FoldPart::head:
rcBar.bottom += 5;
surface->RectangleDraw(rcBar, FillStroke(back, fore, strokeWidth));
break;
case LineMarker::FoldPart::tail:
rcBar.top -= 5;
surface->RectangleDraw(rcBar, FillStroke(back, fore, strokeWidth));
break;
case LineMarker::FoldPart::body:
rcBar.top -= 5;
rcBar.bottom += 5;
surface->RectangleDraw(rcBar, FillStroke(back, fore, strokeWidth));
break;
default:
break;
}
surface->PopClip();
}
break;
case MarkerSymbol::Bookmark: {
const XYPOSITION halfHeight = std::floor(minDim / 3);
const Point pts[] = {
Point(rcWhole.left, centreY - halfHeight),
Point(rcWhole.right - strokeWidth - 2, centreY - halfHeight),
Point(rcWhole.right - strokeWidth - 2 - halfHeight, centreY),
Point(rcWhole.right - strokeWidth - 2, centreY + halfHeight),
Point(rcWhole.left, centreY + halfHeight),
};
AlignedPolygon(surface, pts, std::size(pts));
}
break;
case MarkerSymbol::VerticalBookmark: {
const XYPOSITION halfWidth = std::floor(minDim / 3);
const Point pts[] = {
Point(centreX - halfWidth, centreY - dimOn2),
Point(centreX + halfWidth, centreY - dimOn2),
Point(centreX + halfWidth, centreY + dimOn2),
Point(centreX, centreY + dimOn2 - halfWidth),
Point(centreX - halfWidth, centreY + dimOn2),
};
AlignedPolygon(surface, pts, std::size(pts));
}
break;
default:
if (markType >= MarkerSymbol::Character) {
char character[UTF8MaxBytes + 1] {};
const int uch = static_cast<int>(markType) - static_cast<int>(MarkerSymbol::Character);
UTF8FromUTF32Character(uch, character);
const XYPOSITION width = surface->WidthTextUTF8(fontForCharacter, character);
PRectangle rcText = rc;
rcText.left += (rc.Width() - width) / 2;
rcText.right = rcText.left + width;
surface->DrawTextNoClipUTF8(rcText, fontForCharacter, rcText.bottom - 2,
character, fore, back);
} else {
// treat as MarkerSymbol::FullRect
surface->FillRectangle(rcWhole, back);
}
break;
}
}