1 /** 2 Markdown support 3 4 This is a port of https://github.com/juliettef/imgui_markdown which is under the zlib license! 5 6 Copyright (c) 2019 Juliette Foucaut and Doug Binks 7 8 This software is provided 'as-is', without any express or implied 9 warranty. In no event will the authors be held liable for any damages 10 arising from the use of this software. 11 12 Permission is granted to anyone to use this software for any purpose, 13 including commercial applications, and to alter it and redistribute it 14 freely, subject to the following restrictions: 15 16 1. The origin of this software must not be misrepresented; you must not 17 claim that you wrote the original software. If you use this software 18 in a product, an acknowledgement in the product documentation would be 19 appreciated but is not required. 20 2. Altered source versions must be plainly marked as such, and must not be 21 misrepresented as being the original software. 22 3. This notice may not be removed or altered from any source distribution. 23 */ 24 module creator.widgets.markdown; 25 import creator.widgets.dummy; 26 import bindbc.imgui; 27 28 struct MarkdownLinkCallbackData { 29 string text; 30 string link; 31 void* userData; 32 bool isImage; 33 } 34 35 struct MarkdownTooltipCallbackData { 36 MarkdownLinkCallbackData linkData; 37 string linkIcon; 38 } 39 40 struct MarkdownImageData { 41 bool isValid = false; // if true, will draw the image 42 bool useLinkCallback = false; // if true, linkCallback will be called when image is clicked 43 ImTextureID userTextureId = null; // see ImGui::Image 44 ImVec2 size = ImVec2( 100.0f, 100.0f ); // see ImGui::Image 45 ImVec2 uv0 = ImVec2( 0, 0 ); // see ImGui::Image 46 ImVec2 uv1 = ImVec2( 1, 1 ); // see ImGui::Image 47 ImVec4 tint_col = ImVec4( 1, 1, 1, 1 ); // see ImGui::Image 48 ImVec4 border_col = ImVec4( 0, 0, 0, 0 ); // see ImGui::Image 49 } 50 51 enum MarkdownFormatType { 52 NormalText, 53 Heading, 54 UnorderedList, 55 Link, 56 Emphasis, 57 } 58 59 struct MarkdownFormatInfo { 60 MarkdownFormatType type = MarkdownFormatType.NormalText; 61 int level = 0; // Set for headings: 1 for H1, 2 for H2 etc. 62 bool itemHovered = false; // Currently only set for links when mouse hovered, only valid when start_ == false 63 MarkdownConfig config; 64 } 65 66 struct MarkdownHeadingFormat 67 { 68 float scale = 1; // ImGui font 69 bool separator = true; // if true, an underlined separator is drawn after the header 70 } 71 72 // Configuration struct for Markdown 73 // - linkCallback is called when a link is clicked on 74 // - linkIcon is a string which encode a "Link" icon, if available in the current font (e.g. linkIcon = ICON_FA_LINK with FontAwesome + IconFontCppHeaders https://github.com/juliettef/IconFontCppHeaders) 75 // - headingFormats controls the format of heading H1 to H3, those above H3 use H3 format 76 struct MarkdownConfig 77 { 78 enum NUMHEADINGS = 3; 79 80 incMarkdownLinkCallback linkCallback; 81 incMarkdownTooltipCallback tooltipCallback; 82 incMarkdownImageCallback imageCallback; 83 string linkIcon = ""; // icon displayd in link tooltip 84 MarkdownHeadingFormat[NUMHEADINGS] headingFormats; 85 void* userData; 86 incMarkdownFormatCallback formatCallback = &markdownFmtDefault; 87 } 88 89 /** 90 Global callback for opening links 91 */ 92 alias incMarkdownLinkCallback = void function(MarkdownLinkCallbackData data); 93 94 /** 95 Global callback for opening image links 96 */ 97 alias incMarkdownImageCallback = MarkdownImageData function(MarkdownLinkCallbackData data) ; 98 99 /** 100 Global callback for displaying tooltips 101 */ 102 alias incMarkdownTooltipCallback = void function(MarkdownTooltipCallbackData data); 103 104 /** 105 Global callback for displaying tooltips 106 */ 107 alias incMarkdownFormatCallback = void function(ref MarkdownFormatInfo info, bool start); 108 109 private { 110 void markdownFmtDefault(ref MarkdownFormatInfo info, bool start) { 111 switch(info.type) { 112 case MarkdownFormatType.Heading: 113 MarkdownHeadingFormat fmt; 114 115 if (info.level > MarkdownConfig.NUMHEADINGS) fmt = info.config.headingFormats[MarkdownConfig.NUMHEADINGS-1]; 116 else fmt = info.config.headingFormats[info.level-1]; 117 118 if (start) { 119 igSetWindowFontScale(fmt.scale); 120 // igNewLine(); 121 } else { 122 if(fmt.separator) igSeparator(); 123 // igNewLine(); 124 igSetWindowFontScale(1); 125 } 126 break; 127 case MarkdownFormatType.Link: 128 if(start) 129 { 130 igPushStyleColor(ImGuiCol.Text, igGetStyle().Colors[ImGuiCol.Button]); 131 } 132 else 133 { 134 igPopStyleColor(); 135 if(info.itemHovered) incUnderLine(igGetStyle().Colors[ImGuiCol.ButtonHovered]); 136 else incUnderLine(igGetStyle().Colors[ImGuiCol.Button]); 137 } 138 break; 139 default: 140 break; 141 } 142 } 143 144 // Text that starts after a new line (or at beginning) and ends with a newline (or at end) 145 struct Line { 146 bool isHeading = false; 147 bool isEmphasis = false; 148 bool isUnorderedListStart = false; 149 bool isLeadingSpace = true; // spaces at start of line 150 int leadSpaceCount = 0; 151 int headingCount = 0; 152 int emphasisCount = 0; 153 int lineStart = 0; 154 int lineEnd = 0; 155 int lastRenderPosition = 0; // lines may get rendered in multiple pieces 156 } 157 158 // struct TextBlock { // subset of line 159 // int start = 0; 160 // int stop = 0; 161 // int size() const{ 162 // return stop - start; 163 // } 164 // } 165 166 struct Link { 167 enum LinkState { 168 NoLink, 169 HasSquareBracketOpen, 170 HasSquareBrackets, 171 HasSquareBracketsRoundBracketOpen, 172 } 173 LinkState state = LinkState.NoLink; 174 string text; 175 string url; 176 bool isImage = false; 177 int numBracketsOpen = 0; 178 } 179 180 struct Emphasis { 181 enum EmphasisState { 182 None, 183 Left, 184 Middle, 185 Right, 186 } 187 EmphasisState state = EmphasisState.None; 188 string text; 189 char sym; 190 } 191 192 struct TextRegion { 193 private: 194 float indentX = 0; 195 196 public: 197 void renderTextWrapped(string textSlice, bool indentToHere) { 198 const(char)* sliceStart = textSlice.ptr; 199 const(char)* sliceEnd = textSlice.ptr+textSlice.length; 200 201 float scale = igGetIO().FontGlobalScale; 202 float widthLeft = incAvailableSpace().x; 203 ImFont_CalcWordWrapPositionA(igGetFont(), scale, sliceStart, sliceEnd, widthLeft); 204 igTextUnformatted(sliceStart, sliceEnd); 205 206 // Handle indenting 207 if(indentToHere) { 208 float indentNeeded = incAvailableSpace().x - widthLeft; 209 if (indentNeeded) { 210 igIndent(indentNeeded); 211 indentX += indentNeeded; 212 } 213 } 214 215 widthLeft = incAvailableSpace().x; 216 while(sliceEnd < textSlice.ptr+textSlice.length) { 217 sliceStart = sliceEnd; 218 sliceEnd = textSlice.ptr+textSlice.length; 219 220 if (*sliceStart == ' ' ) ++sliceStart; 221 ImFont_CalcWordWrapPositionA(igGetFont(), scale, sliceStart, sliceEnd, widthLeft); 222 if (sliceStart == sliceEnd) sliceEnd++; 223 igTextUnformatted(sliceStart, sliceEnd); 224 } 225 } 226 227 void renderListTextWrapped(string textSlice) { 228 igBullet(); 229 igSameLine(); 230 renderTextWrapped(textSlice, true); 231 } 232 233 void renderLinkTextWrapped(string textSlice, ref Link link, string markdown, ref MarkdownConfig cfg, out string linkHoverStart, bool indentToHere) { 234 const(char)* sliceStart = textSlice.ptr; 235 const(char)* sliceEnd = textSlice.ptr+textSlice.length; 236 237 float scale = igGetIO().FontGlobalScale; 238 float widthLeft = incAvailableSpace().x; 239 ImFont_CalcWordWrapPositionA(igGetFont(), scale, sliceStart, sliceEnd, widthLeft); 240 bool bHovered = renderLinkText(textSlice, link, markdown, cfg, linkHoverStart); 241 if (indentToHere) { 242 float indentNeeded = incAvailableSpace().x - widthLeft; 243 if (indentNeeded) { 244 igIndent(indentNeeded); 245 indentX += indentNeeded; 246 } 247 } 248 249 widthLeft = incAvailableSpace().x; 250 while(sliceEnd < textSlice.ptr+textSlice.length) { 251 sliceStart = sliceEnd; 252 sliceEnd = textSlice.ptr+textSlice.length; 253 254 if (*sliceStart == ' ' ) ++sliceStart; 255 ImFont_CalcWordWrapPositionA(igGetFont(), scale, sliceStart, sliceEnd, widthLeft); 256 257 if (sliceStart == sliceEnd) sliceEnd++; 258 259 bool bThisLineHovered = renderLinkText(textSlice, link, markdown, cfg, linkHoverStart); 260 bHovered = bHovered || bThisLineHovered; 261 } 262 if (bHovered) igSetMouseCursor(ImGuiMouseCursor.Hand); 263 264 if (!bHovered && linkHoverStart == link.text) { 265 linkHoverStart = null; 266 } 267 } 268 269 bool renderLinkText(string textSlice, ref Link link, string markdown, ref MarkdownConfig cfg, out string linkHoverStart) { 270 MarkdownFormatInfo formatInfo; 271 formatInfo.config = cfg; 272 formatInfo.type = MarkdownFormatType.Link; 273 cfg.formatCallback(formatInfo, true); 274 275 igPushTextWrapPos(-1); 276 igTextUnformatted(textSlice.ptr, textSlice.ptr+textSlice.length); 277 igPopTextWrapPos(); 278 279 bool bIsHovered = igIsItemHovered(); 280 if (bIsHovered) { 281 linkHoverStart = link.text; 282 } 283 bool bHovered = bIsHovered || (linkHoverStart == link.text); 284 285 formatInfo.itemHovered = bHovered; 286 cfg.formatCallback(formatInfo, false); 287 288 if (bHovered) { 289 igSetMouseCursor(ImGuiMouseCursor.Hand); 290 291 if (igIsMouseReleased(ImGuiMouseButton.Left) && cfg.linkCallback) { 292 cfg.linkCallback(MarkdownLinkCallbackData(link.text, link.url, cfg.userData, false)); 293 } 294 295 if (cfg.tooltipCallback) { 296 cfg.tooltipCallback(MarkdownTooltipCallbackData( 297 MarkdownLinkCallbackData( 298 link.text, link.url, cfg.userData, false 299 ), 300 cfg.linkIcon 301 )); 302 } 303 } 304 305 return bIsHovered; 306 } 307 308 void resetIndent() { 309 if (indentX > 0) { 310 igUnindent(indentX); 311 } 312 indentX = 0; 313 } 314 } 315 316 void incUnderLine(ImVec4 color) { 317 ImVec2 min, max; 318 igGetItemRectMin(&min); 319 igGetItemRectMax(&max); 320 min.y = max.y; 321 ImDrawList_AddLine(igGetWindowDrawList(), min, max, igGetColorU32(color), 1); 322 } 323 324 void incRenderLine(string markdown, ref Line line, ref TextRegion textRegion, ref MarkdownConfig cfg) { 325 int indentStart = 0; 326 if (line.isUnorderedListStart) indentStart = 1; 327 for(int j = indentStart; j < line.leadSpaceCount / 2; ++j) igIndent(); 328 import std.stdio : writeln; 329 330 MarkdownFormatInfo formatInfo; 331 formatInfo.config = cfg; 332 int textStart = line.lastRenderPosition+1; 333 int textSize = line.lineEnd - textStart; 334 if (line.isUnorderedListStart) { 335 formatInfo.type = MarkdownFormatType.UnorderedList; 336 cfg.formatCallback(formatInfo, true); 337 string text = markdown[textStart+1..textStart+textSize]; 338 textRegion.renderListTextWrapped(text); 339 } else if (line.isHeading) { 340 formatInfo.level = line.headingCount; 341 formatInfo.type = MarkdownFormatType.Heading; 342 cfg.formatCallback(formatInfo, true); 343 string text = markdown[textStart+1..textStart+textSize]; 344 textRegion.renderTextWrapped(text, false); 345 } else if (line.isEmphasis) { 346 formatInfo.level = line.emphasisCount; 347 formatInfo.type = MarkdownFormatType.Emphasis; 348 cfg.formatCallback(formatInfo, true); 349 string text = markdown[textStart+1..textStart+textSize]; 350 textRegion.renderTextWrapped(text, false); 351 } else { 352 formatInfo.type = MarkdownFormatType.NormalText; 353 cfg.formatCallback(formatInfo, true); 354 string text = markdown[textStart..textStart+textSize]; 355 textRegion.renderTextWrapped(text, false); 356 } 357 cfg.formatCallback(formatInfo, false); 358 359 for(int j = indentStart; j < line.leadSpaceCount / 2; ++j) igUnindent(); 360 } 361 } 362 363 void incMarkdown(string markdown, ref MarkdownConfig cfg) { 364 static string linkHoverStart; 365 ImGuiStyle* style = igGetStyle(); 366 Line line; 367 Link link; 368 Emphasis em; 369 TextRegion textRegion; 370 371 char c; 372 for(int i; i < markdown.length; ++i) { 373 c = markdown[i]; 374 375 if (line.isLeadingSpace) { 376 if (c == ' ') { 377 ++line.leadSpaceCount; 378 continue; 379 } 380 381 line.isLeadingSpace = false; 382 line.lastRenderPosition = i - 1; 383 if (c == '*' && line.leadSpaceCount >= 1) { 384 if (markdown.length > i+1 && markdown[i+1] == ' ') { 385 line.isUnorderedListStart = true; 386 ++i; 387 ++line.lastRenderPosition; 388 } 389 } else if (c == '#') { 390 line.headingCount++; 391 bool bContinueChecking = true; 392 393 int j = i; 394 while (++j < markdown.length && bContinueChecking) { 395 c = markdown[j]; 396 switch(c) { 397 case '#': 398 line.headingCount++; 399 break; 400 case ' ': 401 line.lastRenderPosition = j-1; 402 i = j; 403 line.isHeading = true; 404 bContinueChecking = false; 405 break; 406 default: 407 line.isHeading = false; 408 bContinueChecking = false; 409 break; 410 } 411 } 412 413 if (line.isHeading) { 414 em = Emphasis(); 415 continue; 416 } 417 } 418 } 419 420 switch(link.state) { 421 default: 422 break; 423 case Link.LinkState.NoLink: 424 if (c == '[' && !line.isHeading) { 425 link.state = Link.LinkState.HasSquareBracketOpen; 426 link.text = markdown[i+1..$]; 427 if (i > 0 && markdown[i-1] == '!') { 428 link.isImage = true; 429 } 430 } 431 break; 432 case Link.LinkState.HasSquareBracketOpen: 433 if (c == ']') { 434 link.state = Link.LinkState.HasSquareBrackets; 435 link.text = markdown[link.text.ptr-markdown.ptr..i]; 436 } 437 break; 438 case Link.LinkState.HasSquareBrackets: 439 if (c == '(') { 440 link.state = Link.LinkState.HasSquareBracketsRoundBracketOpen; 441 link.url = markdown[i+1..$]; 442 link.numBracketsOpen = 1; 443 } 444 break; 445 case Link.LinkState.HasSquareBracketsRoundBracketOpen: 446 if (c == '(') ++link.numBracketsOpen; 447 else if (c == ')') --link.numBracketsOpen; 448 449 if (link.numBracketsOpen == 0) { 450 em = Emphasis(); 451 452 line.lineEnd = cast(int)(link.text.ptr-markdown.ptr) - (link.isImage ? 2 : 1); 453 454 incRenderLine(markdown, line, textRegion, cfg); 455 456 line.leadSpaceCount = 0; 457 link.url = markdown[link.url.ptr-markdown.ptr..i]; 458 line.isUnorderedListStart = false; 459 igSameLine(0, 0); 460 if (link.isImage) { 461 bool drawnImage = false; 462 bool useLinkCallback = false; 463 if (cfg.imageCallback) { 464 MarkdownImageData imageData = cfg.imageCallback(MarkdownLinkCallbackData( 465 link.text, link.url, cfg.userData, true 466 )); 467 useLinkCallback = imageData.useLinkCallback; 468 if (imageData.isValid) { 469 igImage(imageData.userTextureId, imageData.size, imageData.uv0, imageData.uv1, imageData.tint_col, imageData.border_col); 470 drawnImage = true; 471 } 472 } 473 if (!drawnImage) { 474 igText("\ubeef"); 475 } 476 if (igIsItemHovered()) { 477 if (igIsMouseReleased(ImGuiMouseButton.Left) && cfg.linkCallback) { 478 cfg.linkCallback(MarkdownLinkCallbackData(link.text, link.url, cfg.userData, true)); 479 } 480 481 if (cfg.tooltipCallback) { 482 cfg.tooltipCallback(MarkdownTooltipCallbackData( 483 MarkdownLinkCallbackData( 484 link.text, link.url, cfg.userData, true 485 ), 486 cfg.linkIcon 487 )); 488 } 489 } 490 } else textRegion.renderLinkTextWrapped(link.text, link, markdown, cfg, linkHoverStart, false); 491 igSameLine(); 492 link = Link(); 493 line.lastRenderPosition = i; 494 } 495 break; 496 } 497 498 switch(em.state) { 499 default: 500 break; 501 } 502 503 if (c == '\n') { 504 line.lineEnd = i; 505 if (em.state == Emphasis.EmphasisState.Middle && line.emphasisCount >= 3 && (line.lineStart + line.emphasisCount) == i) { 506 igSeparator(); 507 } else { 508 incRenderLine(markdown, line, textRegion, cfg); 509 } 510 511 line = Line(); 512 em = Emphasis(); 513 514 line.lineStart = i+1; 515 line.lastRenderPosition = i; 516 textRegion.resetIndent(); 517 518 link = Link(); 519 } 520 } 521 522 if (em.state == Emphasis.EmphasisState.Left && line.emphasisCount >= 3) { 523 igSeparator(); 524 } else { 525 if (markdown.length && line.lineStart < markdown.length && markdown[line.lineStart] != 0) { 526 line.lineEnd = cast(int)markdown.length; 527 if (0 == markdown[line.lineEnd-1]) --line.lineEnd; 528 incRenderLine(markdown, line, textRegion, cfg); 529 } 530 } 531 }