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