| 1 | // Scintilla source code edit control | 
| 2 | /** @file CallTip.cxx | 
| 3 |  ** Code for displaying call tips. | 
| 4 |  **/ | 
| 5 | // Copyright 1998-2001 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 <cassert> | 
| 11 | #include <cstring> | 
| 12 | #include <cstdio> | 
| 13 | #include <cmath> | 
| 14 |  | 
| 15 | #include <stdexcept> | 
| 16 | #include <string> | 
| 17 | #include <string_view> | 
| 18 | #include <vector> | 
| 19 | #include <optional> | 
| 20 | #include <algorithm> | 
| 21 | #include <memory> | 
| 22 |  | 
| 23 | #include "ScintillaTypes.h" | 
| 24 | #include "ScintillaMessages.h" | 
| 25 |  | 
| 26 | #include "Debugging.h" | 
| 27 | #include "Geometry.h" | 
| 28 | #include "Platform.h" | 
| 29 |  | 
| 30 | #include "Position.h" | 
| 31 | #include "CallTip.h" | 
| 32 |  | 
| 33 | using namespace Scintilla; | 
| 34 | using namespace Scintilla::Internal; | 
| 35 |  | 
| 36 | size_t Chunk::Length() const noexcept { | 
| 37 | 	return end - start; | 
| 38 | } | 
| 39 |  | 
| 40 | CallTip::CallTip() noexcept { | 
| 41 | 	wCallTip = {}; | 
| 42 | 	inCallTipMode = false; | 
| 43 | 	posStartCallTip = 0; | 
| 44 | 	rectUp = PRectangle(0,0,0,0); | 
| 45 | 	rectDown = PRectangle(0,0,0,0); | 
| 46 | 	lineHeight = 1; | 
| 47 | 	offsetMain = 0; | 
| 48 | 	tabSize = 0; | 
| 49 | 	above = false; | 
| 50 | 	useStyleCallTip = false;    // for backwards compatibility | 
| 51 |  | 
| 52 | 	insetX = 5; | 
| 53 | 	widthArrow = 14; | 
| 54 | 	borderHeight = 2; // Extra line for border and an empty line at top and bottom. | 
| 55 | 	verticalOffset = 1; | 
| 56 |  | 
| 57 | #ifdef __APPLE__ | 
| 58 | 	// proper apple colours for the default | 
| 59 | 	colourBG = ColourRGBA(0xff, 0xff, 0xc6); | 
| 60 | 	colourUnSel = ColourRGBA(0, 0, 0); | 
| 61 | #else | 
| 62 | 	colourBG = ColourRGBA(0xff, 0xff, 0xff); | 
| 63 | 	colourUnSel = ColourRGBA(0x80, 0x80, 0x80); | 
| 64 | #endif | 
| 65 | 	colourSel = ColourRGBA(0, 0, 0x80); | 
| 66 | 	colourShade = ColourRGBA(0, 0, 0); | 
| 67 | 	colourLight = ColourRGBA(0xc0, 0xc0, 0xc0); | 
| 68 | 	codePage = 0; | 
| 69 | 	clickPlace = 0; | 
| 70 | } | 
| 71 |  | 
| 72 | CallTip::~CallTip() { | 
| 73 | 	wCallTip.Destroy(); | 
| 74 | } | 
| 75 |  | 
| 76 | // We ignore tabs unless a tab width has been set. | 
| 77 | bool CallTip::IsTabCharacter(char ch) const noexcept { | 
| 78 | 	return (tabSize > 0) && (ch == '\t'); | 
| 79 | } | 
| 80 |  | 
| 81 | int CallTip::NextTabPos(int x) const noexcept { | 
| 82 | 	if (tabSize > 0) {              // paranoia... not called unless this is true | 
| 83 | 		x -= insetX;                // position relative to text | 
| 84 | 		x = (x + tabSize) / tabSize;  // tab "number" | 
| 85 | 		return tabSize*x + insetX;  // position of next tab | 
| 86 | 	} else { | 
| 87 | 		return x + 1;                 // arbitrary | 
| 88 | 	} | 
| 89 | } | 
| 90 |  | 
| 91 | namespace { | 
| 92 |  | 
| 93 | // Although this test includes 0, we should never see a \0 character. | 
| 94 | constexpr bool IsArrowCharacter(char ch) noexcept { | 
| 95 | 	return (ch == 0) || (ch == '\001') || (ch == '\002'); | 
| 96 | } | 
| 97 |  | 
| 98 | void DrawArrow(Surface *surface, const PRectangle &rc, bool upArrow, ColourRGBA colourBG, ColourRGBA colourUnSel) { | 
| 99 | 	surface->FillRectangle(rc, colourBG); | 
| 100 | 	const PRectangle rcClientInner = Clamp(rc.Inset(1), Edge::right, rc.right - 2); | 
| 101 | 	surface->FillRectangle(rcClientInner, colourUnSel); | 
| 102 |  | 
| 103 | 	const XYPOSITION width = std::floor(rcClientInner.Width()); | 
| 104 | 	const XYPOSITION halfWidth = std::floor(width / 2) - 1; | 
| 105 | 	const XYPOSITION quarterWidth = std::floor(halfWidth / 2); | 
| 106 | 	const XYPOSITION centreX = rcClientInner.left + width / 2; | 
| 107 | 	const XYPOSITION centreY = std::floor((rcClientInner.top + rcClientInner.bottom) / 2); | 
| 108 |  | 
| 109 | 	constexpr XYPOSITION pixelMove = 0.0f; | 
| 110 | 	if (upArrow) {      // Up arrow | 
| 111 | 		Point pts[] = { | 
| 112 | 			Point(centreX - halfWidth + pixelMove, centreY + quarterWidth + 0.5f), | 
| 113 | 			Point(centreX + halfWidth + pixelMove, centreY + quarterWidth + 0.5f), | 
| 114 | 			Point(centreX + pixelMove, centreY - halfWidth + quarterWidth + 0.5f), | 
| 115 | 		}; | 
| 116 | 		surface->Polygon(pts, std::size(pts), FillStroke(colourBG)); | 
| 117 | 	} else {            // Down arrow | 
| 118 | 		Point pts[] = { | 
| 119 | 			Point(centreX - halfWidth + pixelMove, centreY - quarterWidth + 0.5f), | 
| 120 | 			Point(centreX + halfWidth + pixelMove, centreY - quarterWidth + 0.5f), | 
| 121 | 			Point(centreX + pixelMove, centreY + halfWidth - quarterWidth + 0.5f), | 
| 122 | 		}; | 
| 123 | 		surface->Polygon(pts, std::size(pts), FillStroke(colourBG)); | 
| 124 | 	} | 
| 125 | } | 
| 126 |  | 
| 127 | } | 
| 128 |  | 
| 129 | // Draw a section of the call tip that does not include \n in one colour. | 
| 130 | // The text may include tabs or arrow characters. | 
| 131 | int CallTip::DrawChunk(Surface *surface, int x, std::string_view sv, | 
| 132 | 	int ytext, PRectangle rcClient, bool asHighlight, bool draw) { | 
| 133 |  | 
| 134 | 	if (sv.empty()) { | 
| 135 | 		return x; | 
| 136 | 	} | 
| 137 |  | 
| 138 | 	// Divide the text into sections that are all text, or that are | 
| 139 | 	// single arrows or single tab characters (if tabSize > 0). | 
| 140 | 	// Start with single element 0 to simplify append checks. | 
| 141 | 	std::vector<size_t> ends(1); | 
| 142 | 	for (size_t i=0; i<sv.length(); i++) { | 
| 143 | 		if (IsArrowCharacter(sv[i]) || IsTabCharacter(sv[i])) { | 
| 144 | 			if (ends.back() != i) | 
| 145 | 				ends.push_back(i); | 
| 146 | 			ends.push_back(i+1); | 
| 147 | 		} | 
| 148 | 	} | 
| 149 | 	if (ends.back() != sv.length()) | 
| 150 | 		ends.push_back(sv.length()); | 
| 151 | 	ends.erase(ends.begin());	// Remove initial 0. | 
| 152 |  | 
| 153 | 	size_t startSeg = 0; | 
| 154 | 	for (const size_t endSeg : ends) { | 
| 155 | 		assert(endSeg > 0); | 
| 156 | 		int xEnd; | 
| 157 | 		if (IsArrowCharacter(sv[startSeg])) { | 
| 158 | 			xEnd = x + widthArrow; | 
| 159 | 			const bool upArrow = sv[startSeg] == '\001'; | 
| 160 | 			rcClient.left = static_cast<XYPOSITION>(x); | 
| 161 | 			rcClient.right = static_cast<XYPOSITION>(xEnd); | 
| 162 | 			if (draw) { | 
| 163 | 				DrawArrow(surface, rcClient, upArrow, colourBG, colourUnSel); | 
| 164 | 			} | 
| 165 | 			offsetMain = xEnd; | 
| 166 | 			if (upArrow) { | 
| 167 | 				rectUp = rcClient; | 
| 168 | 			} else { | 
| 169 | 				rectDown = rcClient; | 
| 170 | 			} | 
| 171 | 		} else if (IsTabCharacter(sv[startSeg])) { | 
| 172 | 			xEnd = NextTabPos(x); | 
| 173 | 		} else { | 
| 174 | 			const std::string_view segText = sv.substr(startSeg, endSeg - startSeg); | 
| 175 | 			xEnd = x + static_cast<int>(std::lround(surface->WidthText(font.get(), segText))); | 
| 176 | 			if (draw) { | 
| 177 | 				rcClient.left = static_cast<XYPOSITION>(x); | 
| 178 | 				rcClient.right = static_cast<XYPOSITION>(xEnd); | 
| 179 | 				surface->DrawTextTransparent(rcClient, font.get(), static_cast<XYPOSITION>(ytext), | 
| 180 | 									segText, asHighlight ? colourSel : colourUnSel); | 
| 181 | 			} | 
| 182 | 		} | 
| 183 | 		x = xEnd; | 
| 184 | 		startSeg = endSeg; | 
| 185 | 	} | 
| 186 | 	return x; | 
| 187 | } | 
| 188 |  | 
| 189 | int CallTip::PaintContents(Surface *surfaceWindow, bool draw) { | 
| 190 | 	const PRectangle rcClientPos = wCallTip.GetClientPosition(); | 
| 191 | 	const PRectangle rcClientSize(0.0f, 0.0f, rcClientPos.right - rcClientPos.left, | 
| 192 | 	                        rcClientPos.bottom - rcClientPos.top); | 
| 193 | 	PRectangle rcClient(1.0f, 1.0f, rcClientSize.right - 1, rcClientSize.bottom - 1); | 
| 194 |  | 
| 195 | 	// To make a nice small call tip window, it is only sized to fit most normal characters without accents | 
| 196 | 	const int ascent = static_cast<int>(std::round(surfaceWindow->Ascent(font.get()) - surfaceWindow->InternalLeading(font.get()))); | 
| 197 |  | 
| 198 | 	// For each line... | 
| 199 | 	// Draw the definition in three parts: before highlight, highlighted, after highlight | 
| 200 | 	int ytext = static_cast<int>(rcClient.top) + ascent + 1; | 
| 201 | 	rcClient.bottom = ytext + surfaceWindow->Descent(font.get()) + 1; | 
| 202 | 	std::string_view remaining(val); | 
| 203 | 	int maxWidth = 0; | 
| 204 | 	size_t lineStart = 0; | 
| 205 | 	while (!remaining.empty()) { | 
| 206 | 		const std::string_view chunkVal = remaining.substr(0, remaining.find_first_of('\n')); | 
| 207 | 		remaining.remove_prefix(chunkVal.length()); | 
| 208 | 		if (!remaining.empty()) { | 
| 209 | 			remaining.remove_prefix(1);	// Skip \n | 
| 210 | 		} | 
| 211 |  | 
| 212 | 		const Chunk chunkLine(lineStart, lineStart + chunkVal.length()); | 
| 213 | 		Chunk chunkHighlight( | 
| 214 | 			std::clamp(highlight.start, chunkLine.start, chunkLine.end), | 
| 215 | 			std::clamp(highlight.end, chunkLine.start, chunkLine.end) | 
| 216 | 		); | 
| 217 | 		chunkHighlight.start -= lineStart; | 
| 218 | 		chunkHighlight.end -= lineStart; | 
| 219 |  | 
| 220 | 		rcClient.top = static_cast<XYPOSITION>(ytext - ascent - 1); | 
| 221 |  | 
| 222 | 		int x = insetX;     // start each line at this inset | 
| 223 |  | 
| 224 | 		x = DrawChunk(surfaceWindow, x, | 
| 225 | 			chunkVal.substr(0, chunkHighlight.start), | 
| 226 | 			ytext, rcClient, false, draw); | 
| 227 | 		x = DrawChunk(surfaceWindow, x, | 
| 228 | 			chunkVal.substr(chunkHighlight.start, chunkHighlight.Length()), | 
| 229 | 			ytext, rcClient, true, draw); | 
| 230 | 		x = DrawChunk(surfaceWindow, x, | 
| 231 | 			chunkVal.substr(chunkHighlight.end), | 
| 232 | 			ytext, rcClient, false, draw); | 
| 233 |  | 
| 234 | 		ytext += lineHeight; | 
| 235 | 		rcClient.bottom += lineHeight; | 
| 236 | 		maxWidth = std::max(maxWidth, x); | 
| 237 | 		lineStart += chunkVal.length() + 1; | 
| 238 | 	} | 
| 239 | 	return maxWidth; | 
| 240 | } | 
| 241 |  | 
| 242 | void CallTip::PaintCT(Surface *surfaceWindow) { | 
| 243 | 	if (val.empty()) | 
| 244 | 		return; | 
| 245 | 	const PRectangle rcClientPos = wCallTip.GetClientPosition(); | 
| 246 | 	const PRectangle rcClientSize(0.0f, 0.0f, rcClientPos.right - rcClientPos.left, | 
| 247 | 	                        rcClientPos.bottom - rcClientPos.top); | 
| 248 | 	const PRectangle rcClient(1.0f, 1.0f, rcClientSize.right - 1, rcClientSize.bottom - 1); | 
| 249 |  | 
| 250 | 	surfaceWindow->FillRectangle(rcClient, colourBG); | 
| 251 |  | 
| 252 | 	offsetMain = insetX;    // initial alignment assuming no arrows | 
| 253 | 	PaintContents(surfaceWindow, true); | 
| 254 |  | 
| 255 | #if !defined(__APPLE__) && !PLAT_CURSES | 
| 256 | 	// OSX doesn't put borders on "help tags" | 
| 257 | 	// Draw a raised border around the edges of the window | 
| 258 | 	constexpr XYPOSITION border = 1.0f; | 
| 259 | 	surfaceWindow->FillRectangle(Side(rcClientSize, Edge::left, border), colourLight); | 
| 260 | 	surfaceWindow->FillRectangle(Side(rcClientSize, Edge::right, border), colourShade); | 
| 261 | 	surfaceWindow->FillRectangle(Side(rcClientSize, Edge::bottom, border), colourShade); | 
| 262 | 	surfaceWindow->FillRectangle(Side(rcClientSize, Edge::top, border), colourLight); | 
| 263 | #endif | 
| 264 | } | 
| 265 |  | 
| 266 | void CallTip::MouseClick(Point pt) noexcept { | 
| 267 | 	clickPlace = 0; | 
| 268 | 	if (rectUp.Contains(pt)) | 
| 269 | 		clickPlace = 1; | 
| 270 | 	if (rectDown.Contains(pt)) | 
| 271 | 		clickPlace = 2; | 
| 272 | } | 
| 273 |  | 
| 274 | PRectangle CallTip::CallTipStart(Sci::Position pos, Point pt, int textHeight, const char *defn, | 
| 275 |                                  const char *faceName, int size, | 
| 276 |                                  int codePage_, CharacterSet characterSet, | 
| 277 |                                  Technology technology, | 
| 278 |                                  const char *localeName, | 
| 279 |                                  const Window &wParent) { | 
| 280 | 	clickPlace = 0; | 
| 281 | 	val = defn; | 
| 282 | 	codePage = codePage_; | 
| 283 | 	std::unique_ptr<Surface> surfaceMeasure = Surface::Allocate(technology); | 
| 284 | 	surfaceMeasure->Init(wParent.GetID()); | 
| 285 | 	surfaceMeasure->SetMode(SurfaceMode(codePage, false)); | 
| 286 | 	highlight = Chunk(); | 
| 287 | 	inCallTipMode = true; | 
| 288 | 	posStartCallTip = pos; | 
| 289 | 	const XYPOSITION deviceHeight = static_cast<XYPOSITION>(surfaceMeasure->DeviceHeightFont(size)); | 
| 290 | 	const FontParameters fp(faceName, deviceHeight / FontSizeMultiplier, FontWeight::Normal, | 
| 291 | 		false, FontQuality::QualityDefault, technology, characterSet, localeName); | 
| 292 | 	font = Font::Allocate(fp); | 
| 293 | 	// Look for multiple lines in the text | 
| 294 | 	// Only support \n here - simply means container must avoid \r! | 
| 295 | 	const int numLines = 1 + static_cast<int>(std::count(val.begin(), val.end(), '\n')); | 
| 296 | 	rectUp = PRectangle(0,0,0,0); | 
| 297 | 	rectDown = PRectangle(0,0,0,0); | 
| 298 | 	offsetMain = insetX;            // changed to right edge of any arrows | 
| 299 | 	lineHeight = static_cast<int>(std::lround(surfaceMeasure->Height(font.get()))); | 
| 300 | #if !PLAT_CURSES | 
| 301 | 	widthArrow = lineHeight * 9 / 10; | 
| 302 | #endif | 
| 303 | 	const int width = PaintContents(surfaceMeasure.get(), false) + insetX; | 
| 304 |  | 
| 305 | 	// The returned | 
| 306 | 	// rectangle is aligned to the right edge of the last arrow encountered in | 
| 307 | 	// the tip text, else to the tip text left edge. | 
| 308 | 	const int height = lineHeight * numLines - static_cast<int>(surfaceMeasure->InternalLeading(font.get())) + borderHeight * 2; | 
| 309 | 	if (above) { | 
| 310 | 		return PRectangle(pt.x - offsetMain, pt.y - verticalOffset - height, pt.x + width - offsetMain, pt.y - verticalOffset); | 
| 311 | 	} else { | 
| 312 | 		return PRectangle(pt.x - offsetMain, pt.y + verticalOffset + textHeight, pt.x + width - offsetMain, pt.y + verticalOffset + textHeight + height); | 
| 313 | 	} | 
| 314 | } | 
| 315 |  | 
| 316 | void CallTip::CallTipCancel() noexcept { | 
| 317 | 	inCallTipMode = false; | 
| 318 | 	if (wCallTip.Created()) { | 
| 319 | 		wCallTip.Destroy(); | 
| 320 | 	} | 
| 321 | } | 
| 322 |  | 
| 323 | void CallTip::SetHighlight(size_t start, size_t end) { | 
| 324 | 	// Avoid flashing by checking something has really changed | 
| 325 | 	if ((start != highlight.start) || (end != highlight.end)) { | 
| 326 | 		highlight.start = start; | 
| 327 | 		highlight.end = (end > start) ? end : start; | 
| 328 | 		if (wCallTip.Created()) { | 
| 329 | 			wCallTip.InvalidateAll(); | 
| 330 | 		} | 
| 331 | 	} | 
| 332 | } | 
| 333 |  | 
| 334 | // Set the tab size (sizes > 0 enable the use of tabs). This also enables the | 
| 335 | // use of the StyleCallTip. | 
| 336 | void CallTip::SetTabSize(int tabSz) noexcept { | 
| 337 | 	tabSize = tabSz; | 
| 338 | 	useStyleCallTip = true; | 
| 339 | } | 
| 340 |  | 
| 341 | // Set the calltip position, below the text by default or if above is false | 
| 342 | // else above the text. | 
| 343 | void CallTip::SetPosition(bool aboveText) noexcept { | 
| 344 | 	above = aboveText; | 
| 345 | } | 
| 346 |  | 
| 347 | bool CallTip::UseStyleCallTip() const noexcept { | 
| 348 | 	return useStyleCallTip; | 
| 349 | } | 
| 350 |  | 
| 351 | // It might be better to have two access functions for this and to use | 
| 352 | // them for all settings of colours. | 
| 353 | void CallTip::SetForeBack(const ColourRGBA &fore, const ColourRGBA &back) noexcept { | 
| 354 | 	colourBG = back; | 
| 355 | 	colourUnSel = fore; | 
| 356 | } | 
| 357 |  |