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