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
33using namespace Scintilla;
34using namespace Scintilla::Internal;
35
36size_t Chunk::Length() const noexcept {
37 return end - start;
38}
39
40CallTip::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
72CallTip::~CallTip() {
73 wCallTip.Destroy();
74}
75
76// We ignore tabs unless a tab width has been set.
77bool CallTip::IsTabCharacter(char ch) const noexcept {
78 return (tabSize > 0) && (ch == '\t');
79}
80
81int 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
91namespace {
92
93// Although this test includes 0, we should never see a \0 character.
94constexpr bool IsArrowCharacter(char ch) noexcept {
95 return (ch == 0) || (ch == '\001') || (ch == '\002');
96}
97
98void 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.
131int 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
189int 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
242void 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
266void 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
274PRectangle 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
316void CallTip::CallTipCancel() noexcept {
317 inCallTipMode = false;
318 if (wCallTip.Created()) {
319 wCallTip.Destroy();
320 }
321}
322
323void 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.
336void 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.
343void CallTip::SetPosition(bool aboveText) noexcept {
344 above = aboveText;
345}
346
347bool 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.
353void CallTip::SetForeBack(const ColourRGBA &fore, const ColourRGBA &back) noexcept {
354 colourBG = back;
355 colourUnSel = fore;
356}
357