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
Length() const32 size_t Chunk::Length() const noexcept {
33 return end - start;
34 }
35
CallTip()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
~CallTip()68 CallTip::~CallTip() {
69 font.Release();
70 wCallTip.Destroy();
71 }
72
73 // We ignore tabs unless a tab width has been set.
IsTabCharacter(char ch) const74 bool CallTip::IsTabCharacter(char ch) const noexcept {
75 return (tabSize > 0) && (ch == '\t');
76 }
77
NextTabPos(int x) const78 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.
IsArrowCharacter(char ch)91 constexpr bool IsArrowCharacter(char ch) noexcept {
92 return (ch == 0) || (ch == '\001') || (ch == '\002');
93 }
94
DrawArrow(Scintilla::Surface * surface,const PRectangle & rc,bool upArrow,ColourDesired colourBG,ColourDesired colourUnSel)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.
DrawChunk(Surface * surface,int x,std::string_view sv,int ytext,PRectangle rcClient,bool asHighlight,bool draw)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
PaintContents(Surface * surfaceWindow,bool draw)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
PaintCT(Surface * surfaceWindow)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
MouseClick(Point pt)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
CallTipStart(Sci::Position pos,Point pt,int textHeight,const char * defn,const char * faceName,int size,int codePage_,int characterSet,int technology,const Window & wParent)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
CallTipCancel()309 void CallTip::CallTipCancel() {
310 inCallTipMode = false;
311 if (wCallTip.Created()) {
312 wCallTip.Destroy();
313 }
314 }
315
SetHighlight(size_t start,size_t end)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.
SetTabSize(int tabSz)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.
SetPosition(bool aboveText)336 void CallTip::SetPosition(bool aboveText) noexcept {
337 above = aboveText;
338 }
339
UseStyleCallTip() const340 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.
SetForeBack(const ColourDesired & fore,const ColourDesired & back)346 void CallTip::SetForeBack(const ColourDesired &fore, const ColourDesired &back) noexcept {
347 colourBG = back;
348 colourUnSel = fore;
349 }
350