45.3 KB、1177行 生 Blame 履歴
1#pragma once
2
3// License: zlib
4// Copyright (c) 2019 Juliette Foucaut & Doug Binks
5//
6// This software is provided 'as-is', without any express or implied
7// warranty. In no event will the authors be held liable for any damages
8// arising from the use of this software.
9//
10// Permission is granted to anyone to use this software for any purpose,
11// including commercial applications, and to alter it and redistribute it
12// freely, subject to the following restrictions:
13//
14// 1. The origin of this software must not be misrepresented; you must not
15// claim that you wrote the original software. If you use this software
16// in a product, an acknowledgment in the product documentation would be
17// appreciated but is not required.
18// 2. Altered source versions must be plainly marked as such, and must not be
19// misrepresented as being the original software.
20// 3. This notice may not be removed or altered from any source distribution.
21
22/*
23API BREAKING CHANGES
24====================
25- 2020/04/22 - Added tooltipCallback parameter to ImGui::MarkdownConfig
26- 2019/02/01 - Changed LinkCallback parameters, see https://github.com/juliettef/imgui_markdown/issues/2
27- 2019/02/05 - Added imageCallback parameter to ImGui::MarkdownConfig
28- 2019/02/06 - Added useLinkCallback member variable to MarkdownImageData to configure using images as links
29*/
30
31/*
32imgui_markdown https://github.com/juliettef/imgui_markdown
33Markdown for Dear ImGui
34
35A permissively licensed markdown single-header library for https://github.com/ocornut/imgui
36
37Currently requires C++11 or above
38
39imgui_markdown currently supports the following markdown functionality:
40 - Wrapped text
41 - Headers H1, H2, H3
42 - Emphasis
43 - Indented text, multi levels
44 - Unordered lists and sub-lists
45 - Link
46 - Image
47 - Horizontal rule
48
49Syntax
50
51Wrapping:
52Text wraps automatically. To add a new line, use 'Return'.
53
54Headers:
55# H1
56## H2
57### H3
58
59Emphasis:
60*emphasis*
61_emphasis_
62**strong emphasis**
63__strong emphasis__
64
65Indents:
66On a new line, at the start of the line, add two spaces per indent.
67 Indent level 1
68 Indent level 2
69
70Unordered lists:
71On a new line, at the start of the line, add two spaces, an asterisks and a space.
72For nested lists, add two additional spaces in front of the asterisk per list level increment.
73 * Unordered List level 1
74 * Unordered List level 2
75
76Link:
77[link description](https://...)
78
79Image:
80![image alt text](image identifier e.g. filename)
81
82Horizontal Rule:
83***
84___
85
86===============================================================================
87
88// Example use on Windows with links opening in a browser
89
90#include "ImGui.h" // https://github.com/ocornut/imgui
91#include "imgui_markdown.h" // https://github.com/juliettef/imgui_markdown
92#include "IconsFontAwesome5.h" // https://github.com/juliettef/IconFontCppHeaders
93
94// Following includes for Windows LinkCallback
95#define WIN32_LEAN_AND_MEAN
96#include <Windows.h>
97#include "Shellapi.h"
98#include <string>
99
100void LinkCallback( ImGui::MarkdownLinkCallbackData data_ );
101inline ImGui::MarkdownImageData ImageCallback( ImGui::MarkdownLinkCallbackData data_ );
102
103static ImFont* H1 = NULL;
104static ImFont* H2 = NULL;
105static ImFont* H3 = NULL;
106
107static ImGui::MarkdownConfig mdConfig;
108
109
110void LinkCallback( ImGui::MarkdownLinkCallbackData data_ )
111{
112 std::string url( data_.link, data_.linkLength );
113 if( !data_.isImage )
114 {
115 ShellExecuteA( NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL );
116 }
117}
118
119inline ImGui::MarkdownImageData ImageCallback( ImGui::MarkdownLinkCallbackData data_ )
120{
121 // In your application you would load an image based on data_ input. Here we just use the imgui font texture.
122 ImTextureID image = ImGui::GetIO().Fonts->TexID;
123 // > C++14 can use ImGui::MarkdownImageData imageData{ true, false, image, ImVec2( 40.0f, 20.0f ) };
124 ImGui::MarkdownImageData imageData;
125 imageData.isValid = true;
126 imageData.useLinkCallback = false;
127 imageData.user_texture_id = image;
128 imageData.size = ImVec2( 40.0f, 20.0f );
129
130 // For image resize when available size.x > image width, add
131 ImVec2 const contentSize = ImGui::GetContentRegionAvail();
132 if( imageData.size.x > contentSize.x )
133 {
134 float const ratio = imageData.size.y/imageData.size.x;
135 imageData.size.x = contentSize.x;
136 imageData.size.y = contentSize.x*ratio;
137 }
138
139 return imageData;
140}
141
142void LoadFonts( float fontSize_ = 12.0f )
143{
144 ImGuiIO& io = ImGui::GetIO();
145 io.Fonts->Clear();
146 // Base font
147 io.Fonts->AddFontFromFileTTF( "myfont.ttf", fontSize_ );
148 // Bold headings H2 and H3
149 H2 = io.Fonts->AddFontFromFileTTF( "myfont-bold.ttf", fontSize_ );
150 H3 = mdConfig.headingFormats[ 1 ].font;
151 // bold heading H1
152 float fontSizeH1 = fontSize_ * 1.1f;
153 H1 = io.Fonts->AddFontFromFileTTF( "myfont-bold.ttf", fontSizeH1 );
154}
155
156void ExampleMarkdownFormatCallback( const ImGui::MarkdownFormatInfo& markdownFormatInfo_, bool start_ )
157{
158 // Call the default first so any settings can be overwritten by our implementation.
159 // Alternatively could be called or not called in a switch statement on a case by case basis.
160 // See defaultMarkdownFormatCallback definition for furhter examples of how to use it.
161 ImGui::defaultMarkdownFormatCallback( markdownFormatInfo_, start_ );
162
163 switch( markdownFormatInfo_.type )
164 {
165 // example: change the colour of heading level 2
166 case ImGui::MarkdownFormatType::HEADING:
167 {
168 if( markdownFormatInfo_.level == 2 )
169 {
170 if( start_ )
171 {
172 ImGui::PushStyleColor( ImGuiCol_Text, ImGui::GetStyle().Colors[ ImGuiCol_TextDisabled ] );
173 }
174 else
175 {
176 ImGui::PopStyleColor();
177 }
178 }
179 break;
180 }
181 default:
182 {
183 break;
184 }
185 }
186}
187
188void Markdown( const std::string& markdown_ )
189{
190 // You can make your own Markdown function with your prefered string container and markdown config.
191 // > C++14 can use ImGui::MarkdownConfig mdConfig{ LinkCallback, NULL, ImageCallback, ICON_FA_LINK, { { H1, true }, { H2, true }, { H3, false } }, NULL };
192 mdConfig.linkCallback = LinkCallback;
193 mdConfig.tooltipCallback = NULL;
194 mdConfig.imageCallback = ImageCallback;
195 mdConfig.linkIcon = ICON_FA_LINK;
196 mdConfig.headingFormats[0] = { H1, true };
197 mdConfig.headingFormats[1] = { H2, true };
198 mdConfig.headingFormats[2] = { H3, false };
199 mdConfig.userData = NULL;
200 mdConfig.formatCallback = ExampleMarkdownFormatCallback;
201 ImGui::Markdown( markdown_.c_str(), markdown_.length(), mdConfig );
202}
203
204void MarkdownExample()
205{
206 const std::string markdownText = u8R"(
207# H1 Header: Text and Links
208You can add [links like this one to enkisoftware](https://www.enkisoftware.com/) and lines will wrap well.
209You can also insert images ![image alt text](image identifier e.g. filename)
210Horizontal rules:
211***
212___
213*Emphasis* and **strong emphasis** change the appearance of the text.
214## H2 Header: indented text.
215 This text has an indent (two leading spaces).
216 This one has two.
217### H3 Header: Lists
218 * Unordered lists
219 * Lists can be indented with two extra spaces.
220 * Lists can have [links like this one to Avoyd](https://www.avoyd.com/) and *emphasized text*
221)";
222 Markdown( markdownText );
223}
224
225===============================================================================
226*/
227
228#include <stdint.h>
229
230typedef int ImGuiMarkdownFormatFlags;
231
232enum ImGuiMarkdownFormatFlags_
233{
234 ImGuiMarkdownFormatFlags_None = 0,
235 ImGuiMarkdownFormatFlags_DiscardExtraNewLines = 1 << 0, // (Accurate parsing) Provided markdown will discard all redundant newlines
236 ImGuiMarkdownFormatFlags_NoNewLineBeforeHeading = 1 << 1, // (Accurate parsing) Provided markdown will not format a newline after the first line if it is a heading
237 ImGuiMarkdownFormatFlags_SeparatorDoesNotAdvance = 1 << 2, // (Accurate parsing) Provided markdown will not advance to the next line after formatting a separator
238 ImGuiMarkdownFormatFlags_GithubStyle = ImGuiMarkdownFormatFlags_DiscardExtraNewLines | ImGuiMarkdownFormatFlags_NoNewLineBeforeHeading | ImGuiMarkdownFormatFlags_SeparatorDoesNotAdvance,
239 ImGuiMarkdownFormatFlags_CommonMarkAll = ImGuiMarkdownFormatFlags_DiscardExtraNewLines | ImGuiMarkdownFormatFlags_NoNewLineBeforeHeading | ImGuiMarkdownFormatFlags_SeparatorDoesNotAdvance,
240};
241
242namespace ImGui
243{
244 //-----------------------------------------------------------------------------
245 // Basic types
246 //-----------------------------------------------------------------------------
247
248 struct Link;
249 struct MarkdownConfig;
250
251 struct MarkdownLinkCallbackData // for both links and images
252 {
253 const char* text; // text between square brackets []
254 int textLength;
255 const char* link; // text between brackets ()
256 int linkLength;
257 void* userData;
258 bool isImage; // true if '!' is detected in front of the link syntax
259 };
260
261 struct MarkdownTooltipCallbackData // for tooltips
262 {
263 MarkdownLinkCallbackData linkData;
264 const char* linkIcon;
265 };
266
267 struct MarkdownImageData
268 {
269 bool isValid = false; // if true, will draw the image
270 bool useLinkCallback = false; // if true, linkCallback will be called when image is clicked
271 ImTextureID user_texture_id = {}; // see ImGui::Image
272 ImVec2 size = ImVec2( 100.0f, 100.0f ); // see ImGui::Image
273 ImVec2 uv0 = ImVec2( 0, 0 ); // see ImGui::Image
274 ImVec2 uv1 = ImVec2( 1, 1 ); // see ImGui::Image
275 ImVec4 tint_col = ImVec4( 1, 1, 1, 1 ); // see ImGui::Image
276 ImVec4 border_col = ImVec4( 0, 0, 0, 0 ); // see ImGui::Image
277 ImVec4 bg_col = ImVec4( 0, 0, 0, 0 ); // see ImGui::Image
278 };
279
280 enum class MarkdownFormatType
281 {
282 NORMAL_TEXT,
283 HEADING,
284 UNORDERED_LIST,
285 LINK,
286 EMPHASIS,
287 };
288
289 struct MarkdownFormatInfo
290 {
291 MarkdownFormatType type = MarkdownFormatType::NORMAL_TEXT;
292 int32_t level = 0; // Set for headings: 1 for H1, 2 for H2 etc.
293 bool itemHovered = false; // Currently only set for links when mouse hovered, only valid when start_ == false
294 const MarkdownConfig* config = NULL;
295 const char* text = NULL;
296 int32_t textLength = 0;
297 };
298
299 typedef void MarkdownLinkCallback( MarkdownLinkCallbackData data );
300 typedef void MarkdownTooltipCallback( MarkdownTooltipCallbackData data );
301
302 inline void defaultMarkdownTooltipCallback( MarkdownTooltipCallbackData data_ )
303 {
304 if( data_.linkData.isImage )
305 {
306 ImGui::SetTooltip( "%.*s", data_.linkData.linkLength, data_.linkData.link );
307 }
308 else
309 {
310 ImGui::SetTooltip( "%s Open in browser\n%.*s", data_.linkIcon, data_.linkData.linkLength, data_.linkData.link );
311 }
312 }
313
314 typedef MarkdownImageData MarkdownImageCallback( MarkdownLinkCallbackData data );
315 typedef void MarkdownFormalCallback( const MarkdownFormatInfo& markdownFormatInfo_, bool start_ );
316
317 inline void defaultMarkdownFormatCallback( const MarkdownFormatInfo& markdownFormatInfo_, bool start_ );
318
319 struct MarkdownHeadingFormat
320 {
321 ImFont* font; // ImGui font
322 bool separator; // if true, an underlined separator is drawn after the header
323 #ifdef IMGUI_HAS_TEXTURES // used to detect dynamic font capability: https://github.com/ocornut/imgui/issues/8465#issuecomment-2701570771
324 float fontSize = 0.0f; // Font size if using dynamic fonts
325 #endif
326 };
327
328 // Configuration struct for Markdown
329 // - linkCallback is called when a link is clicked on
330 // - 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)
331 // - headingFormats controls the format of heading H1 to H3, those above H3 use H3 format
332 struct MarkdownConfig
333 {
334 static const int NUMHEADINGS = 3;
335
336 MarkdownLinkCallback* linkCallback = NULL;
337 MarkdownTooltipCallback* tooltipCallback = NULL;
338 MarkdownImageCallback* imageCallback = NULL;
339 const char* linkIcon = ""; // icon displayd in link tooltip
340 MarkdownHeadingFormat headingFormats[ NUMHEADINGS ] = { { NULL, true }, { NULL, true }, { NULL, true } };
341 void* userData = NULL;
342 MarkdownFormalCallback* formatCallback = defaultMarkdownFormatCallback;
343 ImGuiMarkdownFormatFlags formatFlags = ImGuiMarkdownFormatFlags_None; // Configure this to change how Markdown gets formatted. By default imgui_markdown uses psuedo-Markdown for backwards compatibility.
344 };
345
346 //-----------------------------------------------------------------------------
347 // External interface
348 //-----------------------------------------------------------------------------
349
350 inline void Markdown( const char* markdown_, size_t markdownLength_, const MarkdownConfig& mdConfig_ );
351
352 //-----------------------------------------------------------------------------
353 // Internals
354 //-----------------------------------------------------------------------------
355
356 struct TextRegion;
357 struct Line;
358 inline void UnderLine( ImColor col_ );
359 inline void RenderLine( const char* markdown_, Line& line_, TextRegion& textRegion_, const MarkdownConfig& mdConfig_ );
360
361 struct TextRegion
362 {
363 TextRegion() : indentX( 0.0f )
364 {
365 }
366 ~TextRegion()
367 {
368 ResetIndent();
369 }
370
371 void RenderTextWrapped( const char* text_, const char* text_end_, bool bIndentToHere_ = false );
372
373 void RenderListTextWrapped( const char* text_, const char* text_end_ )
374 {
375 ImGui::Bullet();
376 ImGui::SameLine();
377 RenderTextWrapped( text_, text_end_, true );
378 }
379
380 bool RenderLinkText( const char* text_, const char* text_end_, const Link& link_,
381 const char* markdown_, const MarkdownConfig& mdConfig_, const char** linkHoverStart_ );
382
383 void RenderLinkTextWrapped( const char* text_, const char* text_end_, const Link& link_,
384 const char* markdown_, const MarkdownConfig& mdConfig_, const char** linkHoverStart_, bool bIndentToHere_ = false );
385
386 void ResetIndent()
387 {
388 if( indentX > 0.0f )
389 {
390 ImGui::Unindent( indentX );
391 }
392 indentX = 0.0f;
393 }
394
395 private:
396 float indentX;
397 };
398
399 // Text that starts after a new line (or at beginning) and ends with a newline (or at end)
400 struct Line {
401 bool isHeading = false;
402 bool isEmphasis = false;
403 bool isUnorderedListStart = false;
404 bool isLeadingSpace = true; // spaces at start of line
405 int leadSpaceCount = 0;
406 int headingCount = 0;
407 int emphasisCount = 0;
408 int lineStart = 0;
409 int lineEnd = 0;
410 int lastRenderPosition = 0; // lines may get rendered in multiple pieces
411 };
412
413 struct TextBlock { // subset of line
414 int start = 0;
415 int stop = 0;
416 int size() const
417 {
418 return stop - start;
419 }
420 };
421
422 struct Link {
423 enum LinkState {
424 NO_LINK,
425 HAS_SQUARE_BRACKET_OPEN,
426 HAS_SQUARE_BRACKETS,
427 HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN,
428 };
429 LinkState state = NO_LINK;
430 TextBlock text;
431 TextBlock url;
432 bool isImage = false;
433 int num_brackets_open = 0;
434 };
435
436 struct Emphasis {
437 enum EmphasisState {
438 NONE,
439 LEFT,
440 MIDDLE,
441 RIGHT,
442 };
443 EmphasisState state = NONE;
444 TextBlock text;
445 char sym;
446 };
447
448 inline void UnderLine( ImColor col_ )
449 {
450 ImVec2 min = ImGui::GetItemRectMin();
451 ImVec2 max = ImGui::GetItemRectMax();
452 min.y = max.y;
453 ImGui::GetWindowDrawList()->AddLine( min, max, col_, 1.0f );
454 }
455
456 inline void RenderLine( const char* markdown_, Line& line_, TextRegion& textRegion_, const MarkdownConfig& mdConfig_ )
457 {
458 // indent
459 int indentStart = 0;
460 if( line_.isUnorderedListStart ) // ImGui unordered list render always adds one indent
461 {
462 indentStart = 1;
463 }
464 for( int j = indentStart; j < line_.leadSpaceCount / 2; ++j ) // add indents
465 {
466 ImGui::Indent();
467 }
468
469 // render
470 MarkdownFormatInfo formatInfo;
471 formatInfo.config = &mdConfig_;
472 int textStart = line_.lastRenderPosition + 1;
473 int textSize = line_.lineEnd - textStart;
474 if( line_.isUnorderedListStart ) // render unordered list
475 {
476 formatInfo.type = MarkdownFormatType::UNORDERED_LIST;
477 mdConfig_.formatCallback( formatInfo, true );
478 const char* text = markdown_ + textStart + 1;
479 textRegion_.RenderListTextWrapped( text, text + textSize - 1 );
480 }
481 else if( line_.isHeading ) // render heading
482 {
483 formatInfo.level = line_.headingCount;
484 formatInfo.type = MarkdownFormatType::HEADING;
485 const char* text = markdown_ + textStart + 1;
486 formatInfo.text = text;
487 formatInfo.textLength = textSize - 1;
488 mdConfig_.formatCallback( formatInfo, true );
489 textRegion_.RenderTextWrapped( text, text + textSize - 1 );
490 }
491 else if( line_.isEmphasis ) // render emphasis
492 {
493 formatInfo.level = line_.emphasisCount;
494 formatInfo.type = MarkdownFormatType::EMPHASIS;
495 mdConfig_.formatCallback(formatInfo, true);
496 const char* text = markdown_ + textStart;
497 textRegion_.RenderTextWrapped(text, text + textSize);
498 }
499 else // render a normal paragraph chunk
500 {
501 formatInfo.type = MarkdownFormatType::NORMAL_TEXT;
502 mdConfig_.formatCallback( formatInfo, true );
503 const char* text = markdown_ + textStart;
504 textRegion_.RenderTextWrapped( text, text + textSize );
505 }
506 mdConfig_.formatCallback( formatInfo, false );
507
508 // unindent
509 for( int j = indentStart; j < line_.leadSpaceCount / 2; ++j )
510 {
511 ImGui::Unindent();
512 }
513 }
514
515 // render markdown
516 inline void Markdown( const char* markdown_, size_t markdownLength_, const MarkdownConfig& mdConfig_ )
517 {
518 static const char* s_linkHoverStart = NULL; // we need to preserve status of link hovering between frames
519 static ImGuiID s_linkHoverID = 0;
520 const char* linkHoverStart = NULL;
521 ImGuiID linkHoverID = ImGui::GetID("MDLHS");
522 if( linkHoverID == s_linkHoverID )
523 {
524 linkHoverStart = s_linkHoverStart;
525 }
526
527 ImGuiStyle& style = ImGui::GetStyle();
528 Line line;
529 Line prevLine;
530 Link link;
531 Emphasis em;
532 TextRegion textRegion;
533 int concurrentEmptyNewlines = 0;
534 bool appliedExtraNewline = false;
535
536 char c = 0;
537 for( int i=0; i < (int)markdownLength_; ++i )
538 {
539 c = markdown_[i]; // get the character at index
540 if( c == 0 ) { break; } // shouldn't happen but don't go beyond 0.
541
542 // If we're at the beginning of the line, count any spaces
543 if( line.isLeadingSpace )
544 {
545 if ( (mdConfig_.formatFlags & ImGuiMarkdownFormatFlags_DiscardExtraNewLines) ) // Discard LF and CRLF newlines by markdown spec
546 {
547 if ( c == '\n' )
548 {
549 concurrentEmptyNewlines++;
550 line.lineStart += 1;
551 continue;
552 }
553 else if ( ( c == '\r' ) && ( (int)markdownLength_ > i + 1 ) && ( markdown_[i + 1] == '\n' ) )
554 {
555 concurrentEmptyNewlines++;
556 line.lineStart += 2;
557 i += 1;
558 continue;
559 }
560 }
561
562 if( c == ' ' )
563 {
564 ++line.leadSpaceCount;
565 continue;
566 }
567 else
568 {
569 line.isLeadingSpace = false;
570 line.lastRenderPosition = i - 1;
571 if(( c == '*' ) && ( line.leadSpaceCount >= 2 ))
572 {
573 if( ( (int)markdownLength_ > i + 1 ) && ( markdown_[ i + 1 ] == ' ' ) ) // space after '*'
574 {
575 line.isUnorderedListStart = true;
576 ++i;
577 ++line.lastRenderPosition;
578 }
579 // carry on processing as could be emphasis
580 }
581 else if( c == '#' )
582 {
583 line.headingCount++;
584 bool bContinueChecking = true;
585 int j = i;
586 while( ++j < (int)markdownLength_ && bContinueChecking )
587 {
588 c = markdown_[j];
589 switch( c )
590 {
591 case '#':
592 line.headingCount++;
593 break;
594 case ' ':
595 line.lastRenderPosition = j - 1;
596 i = j;
597 line.isHeading = true;
598 bContinueChecking = false;
599 break;
600 default:
601 line.isHeading = false;
602 bContinueChecking = false;
603 break;
604 }
605 }
606 if( line.isHeading )
607 {
608 // reset emphasis status, we do not support emphasis around headers for now
609 em = Emphasis();
610 continue;
611 }
612 }
613 }
614 }
615
616 if ( (mdConfig_.formatFlags & ImGuiMarkdownFormatFlags_DiscardExtraNewLines) )
617 {
618 // In markdown spec, 2 or more consecutive newlines gets converted to a single blank
619 // line. The first newline is always digested by this parser so we check for 1 or more here.
620 if (!appliedExtraNewline && !prevLine.isHeading && concurrentEmptyNewlines >= 1) {
621 ImGui::NewLine();
622 appliedExtraNewline = true;
623 }
624 }
625
626 // Test to see if we have a link
627 switch( link.state )
628 {
629 case Link::NO_LINK:
630 if( c == '[' && !line.isHeading ) // we do not support headings with links for now
631 {
632 link.state = Link::HAS_SQUARE_BRACKET_OPEN;
633 link.text.start = i + 1;
634 if( i > 0 && markdown_[i - 1] == '!' )
635 {
636 link.isImage = true;
637 }
638 }
639 break;
640 case Link::HAS_SQUARE_BRACKET_OPEN:
641 if( c == ']' )
642 {
643 link.state = Link::HAS_SQUARE_BRACKETS;
644 link.text.stop = i;
645 }
646 break;
647 case Link::HAS_SQUARE_BRACKETS:
648 if( c == '(' )
649 {
650 link.state = Link::HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN;
651 link.url.start = i + 1;
652 link.num_brackets_open = 1;
653 }
654 break;
655 case Link::HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN:
656 if( c == '(' )
657 {
658 ++link.num_brackets_open;
659 }
660 else if( c == ')' )
661 {
662 --link.num_brackets_open;
663 }
664 if( link.num_brackets_open == 0 )
665 {
666 // reset emphasis status, we do not support emphasis around links for now
667 em = Emphasis();
668 // render previous line content
669 line.lineEnd = link.text.start - ( link.isImage ? 2 : 1 );
670 RenderLine( markdown_, line, textRegion, mdConfig_ );
671 line.leadSpaceCount = 0;
672 link.url.stop = i;
673 line.isUnorderedListStart = false; // the following text shouldn't have bullets
674 ImGui::SameLine( 0.0f, 0.0f );
675 if( link.isImage ) // it's an image, render it.
676 {
677 bool drawnImage = false;
678 bool useLinkCallback = false;
679 if( mdConfig_.imageCallback )
680 {
681 MarkdownImageData imageData = mdConfig_.imageCallback( { markdown_ + link.text.start, link.text.size(), markdown_ + link.url.start, link.url.size(), mdConfig_.userData, true } );
682 useLinkCallback = imageData.useLinkCallback;
683 if( imageData.isValid )
684 {
685#if IMGUI_VERSION_NUM < 19185
686 if( imageData.bg_col.w > 0.0f )
687 {
688 ImVec2 p = ImGui::GetCursorScreenPos();
689 ImGui::GetWindowDrawList()->AddRectFilled( p, ImVec2( p.x + imageData.size.x, p.y + imageData.size.y ), ImGui::GetColorU32( imageData.bg_col ));
690 }
691 ImGui::Image( imageData.user_texture_id, imageData.size, imageData.uv0, imageData.uv1, imageData.tint_col, imageData.border_col );
692#else
693 ImGui::PushStyleColor( ImGuiCol_Border, imageData.border_col );
694 ImGui::ImageWithBg( imageData.user_texture_id, imageData.size, imageData.uv0, imageData.uv1, imageData.bg_col, imageData.tint_col );
695 ImGui::PopStyleColor();
696#endif
697 drawnImage = true;
698 }
699 }
700 if( !drawnImage )
701 {
702 ImGui::Text( "( Image %.*s not loaded )", link.url.size(), markdown_ + link.url.start );
703 }
704 if( ImGui::IsItemHovered() )
705 {
706 if( ImGui::IsMouseReleased( 0 ) && mdConfig_.linkCallback && useLinkCallback )
707 {
708 mdConfig_.linkCallback( { markdown_ + link.text.start, link.text.size(), markdown_ + link.url.start, link.url.size(), mdConfig_.userData, true } );
709 }
710 if( link.text.size() > 0 && mdConfig_.tooltipCallback )
711 {
712 mdConfig_.tooltipCallback( { { markdown_ + link.text.start, link.text.size(), markdown_ + link.url.start, link.url.size(), mdConfig_.userData, true }, mdConfig_.linkIcon } );
713 }
714 }
715 }
716 else // it's a link, render it.
717 {
718 textRegion.RenderLinkTextWrapped( markdown_ + link.text.start, markdown_ + link.text.start + link.text.size(), link, markdown_, mdConfig_, &linkHoverStart, false );
719 }
720 ImGui::SameLine( 0.0f, 0.0f );
721 // reset the link by reinitializing it
722 link = Link();
723 line.lastRenderPosition = i;
724 break;
725 }
726 }
727
728 // Test to see if we have emphasis styling
729 switch( em.state )
730 {
731 case Emphasis::NONE:
732 if( link.state == Link::NO_LINK && !line.isHeading )
733 {
734 int next = i + 1;
735 int prev = i - 1;
736 if( ( c == '*' || c == '_' )
737 && ( i == line.lineStart
738 || markdown_[ prev ] == ' '
739 || markdown_[ prev ] == '\t' ) // empasis must be preceded by whitespace or line start
740 && (int)markdownLength_ > next // emphasis must precede non-whitespace
741 && markdown_[ next ] != ' '
742 && markdown_[ next ] != '\n'
743 && markdown_[ next ] != '\t' )
744 {
745 em.state = Emphasis::LEFT;
746 em.sym = c;
747 em.text.start = i;
748 line.emphasisCount = 1;
749 continue;
750 }
751 }
752 break;
753 case Emphasis::LEFT:
754 if( em.sym == c )
755 {
756 ++line.emphasisCount;
757 continue;
758 }
759 else
760 {
761 em.text.start = i;
762 em.state = Emphasis::MIDDLE;
763 }
764 break;
765 case Emphasis::MIDDLE:
766 if( em.sym == c )
767 {
768 em.state = Emphasis::RIGHT;
769 em.text.stop = i;
770 // pass through to case Emphasis::RIGHT
771 }
772 else
773 {
774 break;
775 }
776 #if __cplusplus >= 201703L
777 [[fallthrough]];
778 #endif
779 case Emphasis::RIGHT:
780 if( em.sym == c )
781 {
782 if( line.emphasisCount < 3 && ( i - em.text.stop + 1 == line.emphasisCount ) )
783 {
784 // render text up to emphasis
785 int lineEnd = em.text.start - line.emphasisCount;
786 if( lineEnd > line.lineStart )
787 {
788 line.lineEnd = lineEnd;
789 RenderLine( markdown_, line, textRegion, mdConfig_ );
790 ImGui::SameLine( 0.0f, 0.0f );
791 line.isUnorderedListStart = false;
792 line.leadSpaceCount = 0;
793 }
794 line.isEmphasis = true;
795 line.lastRenderPosition = em.text.start - 1;
796 line.lineStart = em.text.start;
797 line.lineEnd = em.text.stop;
798 RenderLine( markdown_, line, textRegion, mdConfig_ );
799 ImGui::SameLine( 0.0f, 0.0f );
800 line.isEmphasis = false;
801 line.lastRenderPosition = i;
802 em = Emphasis();
803 }
804 continue;
805 }
806 else
807 {
808 em.state = Emphasis::NONE;
809 // render text up to here
810 int start = em.text.start - line.emphasisCount;
811 if( start < line.lineStart )
812 {
813 line.lineEnd = line.lineStart;
814 line.lineStart = start;
815 line.lastRenderPosition = start - 1;
816 RenderLine( markdown_, line, textRegion, mdConfig_ );
817 line.lineStart = line.lineEnd;
818 line.lastRenderPosition = line.lineStart - 1;
819 }
820 }
821 break;
822 }
823
824 // handle end of line (render)
825 if( c == '\n' )
826 {
827 // first check if the line is a horizontal rule
828 line.lineEnd = i;
829 if( em.state == Emphasis::MIDDLE && line.emphasisCount >=3 &&
830 ( line.lineStart + line.emphasisCount ) == i )
831 {
832 ImGui::Separator();
833 }
834 else
835 {
836 // render the line: multiline emphasis requires a complex implementation so not supporting
837 RenderLine( markdown_, line, textRegion, mdConfig_ );
838 }
839
840 // reset the line and emphasis state
841 prevLine = line;
842 line = Line();
843 em = Emphasis();
844
845 line.lineStart = i + 1;
846 line.lastRenderPosition = i;
847
848 textRegion.ResetIndent();
849
850 // reset the link
851 link = Link();
852
853 concurrentEmptyNewlines = 0;
854 appliedExtraNewline = false;
855 }
856 }
857
858 if( em.state == Emphasis::LEFT && line.emphasisCount >= 3 )
859 {
860 ImGui::Separator();
861 }
862 else
863 {
864 // render any remaining text if last char wasn't 0
865 if( markdownLength_ && line.lineStart < (int)markdownLength_ && markdown_[ line.lineStart ] != 0 )
866 {
867 // handle both null terminated and non null terminated strings
868 line.lineEnd = (int)markdownLength_;
869 if( 0 == markdown_[ line.lineEnd - 1 ] )
870 {
871 --line.lineEnd;
872 }
873 RenderLine( markdown_, line, textRegion, mdConfig_ );
874 }
875 }
876
877 if( NULL != linkHoverStart || linkHoverID == s_linkHoverID )
878 {
879 s_linkHoverStart = linkHoverStart;
880 s_linkHoverID = linkHoverID;
881 }
882 }
883
884 inline bool TextRegion::RenderLinkText( const char* text_, const char* text_end_, const Link& link_,
885 const char* markdown_, const MarkdownConfig& mdConfig_, const char** linkHoverStart_ )
886 {
887 MarkdownFormatInfo formatInfo;
888 formatInfo.config = &mdConfig_;
889 formatInfo.type = MarkdownFormatType::LINK;
890 mdConfig_.formatCallback( formatInfo, true );
891 ImGui::PushTextWrapPos( -1.0f );
892 ImGui::TextUnformatted( text_, text_end_ );
893 ImGui::PopTextWrapPos();
894
895 bool bThisItemHovered = ImGui::IsItemHovered();
896 if(bThisItemHovered)
897 {
898 *linkHoverStart_ = markdown_ + link_.text.start;
899 }
900 bool bHovered = bThisItemHovered || ( *linkHoverStart_ == ( markdown_ + link_.text.start ) );
901
902 formatInfo.itemHovered = bHovered;
903 mdConfig_.formatCallback( formatInfo, false );
904
905 if(bHovered)
906 {
907 if( ImGui::IsMouseReleased( 0 ) && mdConfig_.linkCallback )
908 {
909 mdConfig_.linkCallback( { markdown_ + link_.text.start, link_.text.size(), markdown_ + link_.url.start, link_.url.size(), mdConfig_.userData, false } );
910 }
911 if( mdConfig_.tooltipCallback )
912 {
913 mdConfig_.tooltipCallback( { { markdown_ + link_.text.start, link_.text.size(), markdown_ + link_.url.start, link_.url.size(), mdConfig_.userData, false }, mdConfig_.linkIcon } );
914 }
915 }
916 return bThisItemHovered;
917 }
918
919 // IsCharInsideWord based on ImGui's CalcWordWrapPositionA
920 inline bool IsCharInsideWord( char c_ )
921 {
922 return c_ != ' ' && c_ != '.' && c_ != ',' && c_ != ';' && c_ != '!' && c_ != '?' && c_ != '\"';
923 }
924
925 // ImGui::TextWrapped will wrap at the starting position
926 // so to work around this we render using our own wrapping for the first line
927 inline void TextRegion::RenderTextWrapped( const char* text_, const char* text_end_, bool bIndentToHere_ )
928 {
929 #if IMGUI_VERSION_NUM >= 19197
930 float fontSize = ImGui::GetFontSize();
931 #else
932 float scale = ImGui::GetIO().FontGlobalScale;
933 #endif
934 float widthLeft = GetContentRegionAvail().x;
935 const char* endLine = text_;
936 if( widthLeft > 0.0f )
937 {
938 #if IMGUI_VERSION_NUM >= 19197
939 endLine = ImGui::GetFont()->CalcWordWrapPosition( fontSize, text_, text_end_, widthLeft );
940 #else
941 endLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthLeft );
942 #endif
943 }
944
945 if( endLine > text_ && endLine < text_end_ )
946 {
947 if( IsCharInsideWord( *endLine ) )
948 {
949 // see if we can do a better cut.
950 float widthNextLine = widthLeft + GetCursorScreenPos().x - GetWindowPos().x; // was GetContentRegionMax().x on IMGUI_VERSION_NUM < 19099
951 #if IMGUI_VERSION_NUM >= 19197
952 const char* endNextLine = ImGui::GetFont()->CalcWordWrapPosition( fontSize, text_, text_end_, widthNextLine );
953 #else
954 const char* endNextLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthNextLine );
955 #endif
956 if( endNextLine == text_end_ || ( endNextLine <= text_end_ && !IsCharInsideWord( *endNextLine ) ) )
957 {
958 // can possibly do better if go to next line
959 endLine = text_;
960 }
961 }
962 }
963 ImGui::TextUnformatted( text_, endLine );
964 if( bIndentToHere_ )
965 {
966 float indentNeeded = GetContentRegionAvail().x - widthLeft;
967 if( indentNeeded )
968 {
969 ImGui::Indent( indentNeeded );
970 indentX += indentNeeded;
971 }
972 }
973 widthLeft = GetContentRegionAvail().x;
974 while( endLine < text_end_ )
975 {
976 text_ = endLine;
977 if( *text_ == ' ' ) { ++text_; } // skip a space at start of line
978 #if IMGUI_VERSION_NUM >= 19197
979 endLine = ImGui::GetFont()->CalcWordWrapPosition( fontSize, text_, text_end_, widthLeft );
980 #else
981 endLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthLeft );
982 #endif
983 if( text_ == endLine )
984 {
985 endLine++;
986 }
987 ImGui::TextUnformatted( text_, endLine );
988 }
989 }
990
991 inline void TextRegion::RenderLinkTextWrapped( const char* text_, const char* text_end_, const Link& link_,
992 const char* markdown_, const MarkdownConfig& mdConfig_, const char** linkHoverStart_, bool bIndentToHere_ )
993 {
994 #if IMGUI_VERSION_NUM >= 19197
995 float fontSize = ImGui::GetFontSize();
996 #else
997 float scale = ImGui::GetIO().FontGlobalScale;
998 #endif
999 float widthLeft = GetContentRegionAvail().x;
1000 const char* endLine = text_;
1001 if( widthLeft > 0.0f )
1002 {
1003 #if IMGUI_VERSION_NUM >= 19197
1004 endLine = ImGui::GetFont()->CalcWordWrapPosition( fontSize, text_, text_end_, widthLeft );
1005 #else
1006 endLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthLeft );
1007 #endif
1008 }
1009
1010 if( endLine > text_ && endLine < text_end_ )
1011 {
1012 if( IsCharInsideWord( *endLine ) )
1013 {
1014 // see if we can do a better cut.
1015 float widthNextLine = widthLeft + GetCursorScreenPos().x - GetWindowPos().x; // was GetContentRegionMax().x on IMGUI_VERSION_NUM < 19099
1016 #if IMGUI_VERSION_NUM >= 19197
1017 const char* endNextLine = ImGui::GetFont()->CalcWordWrapPosition( fontSize, text_, text_end_, widthNextLine );
1018 #else
1019 const char* endNextLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthNextLine );
1020 #endif
1021 if( endNextLine == text_end_ || ( endNextLine <= text_end_ && !IsCharInsideWord( *endNextLine ) ) )
1022 {
1023 // can possibly do better if go to next line
1024 endLine = text_;
1025 }
1026 }
1027 }
1028 bool bHovered = RenderLinkText( text_, endLine, link_, markdown_, mdConfig_, linkHoverStart_ );
1029 if( bIndentToHere_ )
1030 {
1031 float indentNeeded = GetContentRegionAvail().x - widthLeft;
1032 if( indentNeeded )
1033 {
1034 ImGui::Indent( indentNeeded );
1035 indentX += indentNeeded;
1036 }
1037 }
1038 widthLeft = GetContentRegionAvail().x;
1039 while( endLine < text_end_ )
1040 {
1041 text_ = endLine;
1042 if( *text_ == ' ' ) { ++text_; } // skip a space at start of line
1043 #if IMGUI_VERSION_NUM >= 19197
1044 endLine = ImGui::GetFont()->CalcWordWrapPosition( fontSize, text_, text_end_, widthLeft );
1045 #else
1046 endLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthLeft );
1047 #endif
1048 if( text_ == endLine )
1049 {
1050 endLine++;
1051 }
1052 bool bThisLineHovered = RenderLinkText( text_, endLine, link_, markdown_, mdConfig_, linkHoverStart_ );
1053 bHovered = bHovered || bThisLineHovered;
1054 }
1055 if( !bHovered && *linkHoverStart_ == markdown_ + link_.text.start )
1056 {
1057 *linkHoverStart_ = NULL;
1058 }
1059 }
1060
1061
1062 inline void defaultMarkdownFormatCallback( const MarkdownFormatInfo& markdownFormatInfo_, bool start_ )
1063 {
1064 switch( markdownFormatInfo_.type )
1065 {
1066 case MarkdownFormatType::NORMAL_TEXT:
1067 break;
1068 case MarkdownFormatType::EMPHASIS:
1069 {
1070 MarkdownHeadingFormat fmt;
1071 // default styling for emphasis uses last headingFormats - for your own styling
1072 // implement EMPHASIS in your formatCallback
1073 if( markdownFormatInfo_.level == 1 )
1074 {
1075 // normal emphasis
1076 if( start_ )
1077 {
1078 ImGui::PushStyleColor( ImGuiCol_Text, ImGui::GetStyle().Colors[ ImGuiCol_TextDisabled ] );
1079 }
1080 else
1081 {
1082 ImGui::PopStyleColor();
1083 }
1084 }
1085 else
1086 {
1087 // strong emphasis
1088 fmt = markdownFormatInfo_.config->headingFormats[ MarkdownConfig::NUMHEADINGS - 1 ];
1089 if( start_ )
1090 {
1091 if( fmt.font )
1092 {
1093 #ifdef IMGUI_HAS_TEXTURES // used to detect dynamic font capability:
1094 ImGui::PushFont( fmt.font, 0.0f ); // Change font and keep current size
1095 #else
1096 ImGui::PushFont( fmt.font );
1097 #endif
1098 }
1099 }
1100 else
1101 {
1102 if( fmt.font )
1103 {
1104 ImGui::PopFont();
1105 }
1106 }
1107 }
1108 break;
1109 }
1110 case MarkdownFormatType::HEADING:
1111 {
1112 MarkdownHeadingFormat fmt;
1113 if( markdownFormatInfo_.level > MarkdownConfig::NUMHEADINGS )
1114 {
1115 fmt = markdownFormatInfo_.config->headingFormats[ MarkdownConfig::NUMHEADINGS - 1 ];
1116 }
1117 else
1118 {
1119 fmt = markdownFormatInfo_.config->headingFormats[ markdownFormatInfo_.level - 1 ];
1120 }
1121 if (start_)
1122 {
1123 if ( 0 == ( markdownFormatInfo_.config->formatFlags & ImGuiMarkdownFormatFlags_NoNewLineBeforeHeading ) )
1124 {
1125 ImGui::NewLine();
1126 }
1127 if (fmt.font)
1128 {
1129#ifdef IMGUI_HAS_TEXTURES // used to detect dynamic font capability: https://github.com/ocornut/imgui/issues/8465#issuecomment-2701570771
1130 ImGui::PushFont(fmt.font, fmt.fontSize > 0.0f ? fmt.fontSize : fmt.font->LegacySize);
1131#else
1132 ImGui::PushFont(fmt.font);
1133#endif
1134 }
1135 }
1136 else
1137 {
1138 if (fmt.separator)
1139 {
1140 // In markdown the separator does not advance the cursor
1141 ImVec2 cursor = ImGui::GetCursorPos();
1142 ImGui::Separator();
1143 if ( (markdownFormatInfo_.config->formatFlags & ImGuiMarkdownFormatFlags_SeparatorDoesNotAdvance) ) {
1144 ImGui::SetCursorPos(cursor);
1145 }
1146 }
1147 if (fmt.font)
1148 {
1149 ImGui::PopFont();
1150 }
1151 ImGui::NewLine();
1152 }
1153 break;
1154 }
1155 case MarkdownFormatType::UNORDERED_LIST:
1156 break;
1157 case MarkdownFormatType::LINK:
1158 if( start_ )
1159 {
1160 ImGui::PushStyleColor( ImGuiCol_Text, ImGui::GetStyle().Colors[ ImGuiCol_ButtonHovered ] );
1161 }
1162 else
1163 {
1164 ImGui::PopStyleColor();
1165 if( markdownFormatInfo_.itemHovered )
1166 {
1167 ImGui::UnderLine( ImGui::GetStyle().Colors[ ImGuiCol_ButtonHovered ] );
1168 }
1169 else
1170 {
1171 ImGui::UnderLine( ImGui::GetStyle().Colors[ ImGuiCol_Button ] );
1172 }
1173 }
1174 break;
1175 }
1176 }
1177}