├── .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 ![unsupported image alt text](unsupported image identifier e.g. filename) 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 ![image alt text](image identifier e.g. filename) 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) [Become a Patron](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 | ![imgui_markdown demo live editing](https://github.com/juliettef/Media/blob/main/imgui_markdown_demo_live_editing.gif?raw=true) 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 | ![image alt text](image identifier e.g. filename) 75 | ``` 76 | ### Horizontal Rule 77 | ``` 78 | *** 79 | ___ 80 | ``` 81 | 82 | ![Example use of imgui_markdown with icon fonts](https://github.com/juliettef/Media/blob/main/imgui_markdown_icon_font.jpg?raw=true) 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 ![image alt text](image identifier e.g. filename) 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 | ![Avoyd screenshot](https://github.com/juliettef/Media/blob/main/imgui_markdown_Avoyd_about_OSS.png?raw=true) 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 | ![Imogen screenshot](https://camo.githubusercontent.com/28347bc0c1627aa4f289e1b2b769afcb3a5de370/68747470733a2f2f692e696d6775722e636f6d2f7351664f3542722e706e67) 266 | 267 | ### [Light Tracer](https://lighttracer.org/) 268 | Experimental GPU ray tracer for web 269 | 270 | ![Light Tracer screenshot](https://github.com/juliettef/Media/blob/main/imgui_markdown_Light_Tracer.png?raw=true) 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 | [![Using imgui_markdown as help viewer for Visual 6502 Remix with internal and external links - animated gif](https://user-images.githubusercontent.com/1699414/69185510-320baa00-0b17-11ea-9fd5-82ed6e02a05c.gif) 279 | ![Using imgui_markdown as help viewer for Visual 6502 Remix - screenshot](https://user-images.githubusercontent.com/1699414/69185626-67b09300-0b17-11ea-85a8-fed54a0082b4.png)](https://github.com/ocornut/imgui/issues/2847#issuecomment-555710973) 280 | 281 | ![Using imgui_markdown in the About page for Visual 6502 Remix - screenshot](https://github.com/juliettef/Media/blob/main/imgui_markdown_Visual_6502_Remix_About.png?raw=true) 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 | ![image alt text](image identifier e.g. filename) 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 ![image alt text](image identifier e.g. filename) 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 | --------------------------------------------------------------------------------