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