1032 lines
38 KiB
C++
1032 lines
38 KiB
C++
#pragma once
|
|
|
|
// License: zlib
|
|
// Copyright (c) 2019 Juliette Foucaut & Doug Binks
|
|
//
|
|
// This software is provided 'as-is', without any express or implied
|
|
// warranty. In no event will the authors be held liable for any damages
|
|
// arising from the use of this software.
|
|
//
|
|
// Permission is granted to anyone to use this software for any purpose,
|
|
// including commercial applications, and to alter it and redistribute it
|
|
// freely, subject to the following restrictions:
|
|
//
|
|
// 1. The origin of this software must not be misrepresented; you must not
|
|
// claim that you wrote the original software. If you use this software
|
|
// in a product, an acknowledgment in the product documentation would be
|
|
// appreciated but is not required.
|
|
// 2. Altered source versions must be plainly marked as such, and must not be
|
|
// misrepresented as being the original software.
|
|
// 3. This notice may not be removed or altered from any source distribution.
|
|
|
|
/*
|
|
API BREAKING CHANGES
|
|
====================
|
|
- 2020/04/22 - Added tooltipCallback parameter to ImGui::MarkdownConfig
|
|
- 2019/02/01 - Changed LinkCallback parameters, see https://github.com/juliettef/imgui_markdown/issues/2
|
|
- 2019/02/05 - Added imageCallback parameter to ImGui::MarkdownConfig
|
|
- 2019/02/06 - Added useLinkCallback member variable to MarkdownImageData to configure using images as links
|
|
*/
|
|
|
|
/*
|
|
imgui_markdown https://github.com/juliettef/imgui_markdown
|
|
Markdown for Dear ImGui
|
|
|
|
A permissively licensed markdown single-header library for https://github.com/ocornut/imgui
|
|
|
|
Currently requires C++11 or above
|
|
|
|
imgui_markdown currently supports the following markdown functionality:
|
|
- Wrapped text
|
|
- Headers H1, H2, H3
|
|
- Emphasis
|
|
- Indented text, multi levels
|
|
- Unordered lists and sub-lists
|
|
- Link
|
|
- Image
|
|
- Horizontal rule
|
|
|
|
Syntax
|
|
|
|
Wrapping:
|
|
Text wraps automatically. To add a new line, use 'Return'.
|
|
|
|
Headers:
|
|
# H1
|
|
## H2
|
|
### H3
|
|
|
|
Emphasis:
|
|
*emphasis*
|
|
_emphasis_
|
|
**strong emphasis**
|
|
__strong emphasis__
|
|
|
|
Indents:
|
|
On a new line, at the start of the line, add two spaces per indent.
|
|
Indent level 1
|
|
Indent level 2
|
|
|
|
Unordered lists:
|
|
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.
|
|
* Unordered List level 1
|
|
* Unordered List level 2
|
|
|
|
Link:
|
|
[link description](https://...)
|
|
|
|
Image:
|
|

|
|
|
|
Horizontal Rule:
|
|
***
|
|
___
|
|
|
|
===============================================================================
|
|
|
|
// Example use on Windows with links opening in a browser
|
|
|
|
#include "ImGui.h" // https://github.com/ocornut/imgui
|
|
#include "imgui_markdown.h" // https://github.com/juliettef/imgui_markdown
|
|
#include "IconsFontAwesome5.h" // https://github.com/juliettef/IconFontCppHeaders
|
|
|
|
// Following includes for Windows LinkCallback
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <Windows.h>
|
|
#include "Shellapi.h"
|
|
#include <string>
|
|
|
|
void LinkCallback( ImGui::MarkdownLinkCallbackData data_ );
|
|
inline ImGui::MarkdownImageData ImageCallback( ImGui::MarkdownLinkCallbackData data_ );
|
|
|
|
static ImFont* H1 = NULL;
|
|
static ImFont* H2 = NULL;
|
|
static ImFont* H3 = NULL;
|
|
|
|
static ImGui::MarkdownConfig mdConfig;
|
|
|
|
|
|
void LinkCallback( ImGui::MarkdownLinkCallbackData data_ )
|
|
{
|
|
std::string url( data_.link, data_.linkLength );
|
|
if( !data_.isImage )
|
|
{
|
|
ShellExecuteA( NULL, "open", url.c_str(), NULL, NULL, SW_SHOWNORMAL );
|
|
}
|
|
}
|
|
|
|
inline ImGui::MarkdownImageData ImageCallback( ImGui::MarkdownLinkCallbackData data_ )
|
|
{
|
|
// In your application you would load an image based on data_ input. Here we just use the imgui font texture.
|
|
ImTextureID image = ImGui::GetIO().Fonts->TexID;
|
|
// > C++14 can use ImGui::MarkdownImageData imageData{ true, false, image, ImVec2( 40.0f, 20.0f ) };
|
|
ImGui::MarkdownImageData imageData;
|
|
imageData.isValid = true;
|
|
imageData.useLinkCallback = false;
|
|
imageData.user_texture_id = image;
|
|
imageData.size = ImVec2( 40.0f, 20.0f );
|
|
|
|
// For image resize when available size.x > image width, add
|
|
ImVec2 const contentSize = ImGui::GetContentRegionAvail();
|
|
if( imageData.size.x > contentSize.x )
|
|
{
|
|
float const ratio = imageData.size.y/imageData.size.x;
|
|
imageData.size.x = contentSize.x;
|
|
imageData.size.y = contentSize.x*ratio;
|
|
}
|
|
|
|
return imageData;
|
|
}
|
|
|
|
void LoadFonts( float fontSize_ = 12.0f )
|
|
{
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.Fonts->Clear();
|
|
// Base font
|
|
io.Fonts->AddFontFromFileTTF( "myfont.ttf", fontSize_ );
|
|
// Bold headings H2 and H3
|
|
H2 = io.Fonts->AddFontFromFileTTF( "myfont-bold.ttf", fontSize_ );
|
|
H3 = mdConfig.headingFormats[ 1 ].font;
|
|
// bold heading H1
|
|
float fontSizeH1 = fontSize_ * 1.1f;
|
|
H1 = io.Fonts->AddFontFromFileTTF( "myfont-bold.ttf", fontSizeH1 );
|
|
}
|
|
|
|
void ExampleMarkdownFormatCallback( const ImGui::MarkdownFormatInfo& markdownFormatInfo_, bool start_ )
|
|
{
|
|
// Call the default first so any settings can be overwritten by our implementation.
|
|
// Alternatively could be called or not called in a switch statement on a case by case basis.
|
|
// See defaultMarkdownFormatCallback definition for furhter examples of how to use it.
|
|
ImGui::defaultMarkdownFormatCallback( markdownFormatInfo_, start_ );
|
|
|
|
switch( markdownFormatInfo_.type )
|
|
{
|
|
// example: change the colour of heading level 2
|
|
case ImGui::MarkdownFormatType::HEADING:
|
|
{
|
|
if( markdownFormatInfo_.level == 2 )
|
|
{
|
|
if( start_ )
|
|
{
|
|
ImGui::PushStyleColor( ImGuiCol_Text, ImGui::GetStyle().Colors[ ImGuiCol_TextDisabled ] );
|
|
}
|
|
else
|
|
{
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Markdown( const std::string& markdown_ )
|
|
{
|
|
// You can make your own Markdown function with your prefered string container and markdown config.
|
|
// > C++14 can use ImGui::MarkdownConfig mdConfig{ LinkCallback, NULL, ImageCallback, ICON_FA_LINK, { { H1, true }, { H2, true }, { H3, false } }, NULL };
|
|
mdConfig.linkCallback = LinkCallback;
|
|
mdConfig.tooltipCallback = NULL;
|
|
mdConfig.imageCallback = ImageCallback;
|
|
mdConfig.linkIcon = ICON_FA_LINK;
|
|
mdConfig.headingFormats[0] = { H1, true };
|
|
mdConfig.headingFormats[1] = { H2, true };
|
|
mdConfig.headingFormats[2] = { H3, false };
|
|
mdConfig.userData = NULL;
|
|
mdConfig.formatCallback = ExampleMarkdownFormatCallback;
|
|
ImGui::Markdown( markdown_.c_str(), markdown_.length(), mdConfig );
|
|
}
|
|
|
|
void MarkdownExample()
|
|
{
|
|
const std::string markdownText = u8R"(
|
|
# H1 Header: Text and Links
|
|
You can add [links like this one to enkisoftware](https://www.enkisoftware.com/) and lines will wrap well.
|
|
You can also insert images 
|
|
Horizontal rules:
|
|
***
|
|
___
|
|
*Emphasis* and **strong emphasis** change the appearance of the text.
|
|
## H2 Header: indented text.
|
|
This text has an indent (two leading spaces).
|
|
This one has two.
|
|
### H3 Header: Lists
|
|
* Unordered lists
|
|
* Lists can be indented with two extra spaces.
|
|
* Lists can have [links like this one to Avoyd](https://www.avoyd.com/) and *emphasized text*
|
|
)";
|
|
Markdown( markdownText );
|
|
}
|
|
|
|
===============================================================================
|
|
*/
|
|
|
|
|
|
#include <stdint.h>
|
|
|
|
namespace ImGui
|
|
{
|
|
//-----------------------------------------------------------------------------
|
|
// Basic types
|
|
//-----------------------------------------------------------------------------
|
|
|
|
struct Link;
|
|
struct MarkdownConfig;
|
|
|
|
struct MarkdownLinkCallbackData // for both links and images
|
|
{
|
|
const char* text; // text between square brackets []
|
|
int textLength;
|
|
const char* link; // text between brackets ()
|
|
int linkLength;
|
|
void* userData;
|
|
bool isImage; // true if '!' is detected in front of the link syntax
|
|
};
|
|
|
|
struct MarkdownTooltipCallbackData // for tooltips
|
|
{
|
|
MarkdownLinkCallbackData linkData;
|
|
const char* linkIcon;
|
|
};
|
|
|
|
struct MarkdownImageData
|
|
{
|
|
bool isValid = false; // if true, will draw the image
|
|
bool useLinkCallback = false; // if true, linkCallback will be called when image is clicked
|
|
ImTextureID user_texture_id = 0; // see ImGui::Image
|
|
ImVec2 size = ImVec2( 100.0f, 100.0f ); // see ImGui::Image
|
|
ImVec2 uv0 = ImVec2( 0, 0 ); // see ImGui::Image
|
|
ImVec2 uv1 = ImVec2( 1, 1 ); // see ImGui::Image
|
|
ImVec4 tint_col = ImVec4( 1, 1, 1, 1 ); // see ImGui::Image
|
|
ImVec4 border_col = ImVec4( 0, 0, 0, 0 ); // see ImGui::Image
|
|
};
|
|
|
|
enum class MarkdownFormatType
|
|
{
|
|
NORMAL_TEXT,
|
|
HEADING,
|
|
UNORDERED_LIST,
|
|
LINK,
|
|
EMPHASIS,
|
|
};
|
|
|
|
struct MarkdownFormatInfo
|
|
{
|
|
MarkdownFormatType type = MarkdownFormatType::NORMAL_TEXT;
|
|
int32_t level = 0; // Set for headings: 1 for H1, 2 for H2 etc.
|
|
bool itemHovered = false; // Currently only set for links when mouse hovered, only valid when start_ == false
|
|
const MarkdownConfig* config = NULL;
|
|
};
|
|
|
|
typedef void MarkdownLinkCallback( MarkdownLinkCallbackData data );
|
|
typedef void MarkdownTooltipCallback( MarkdownTooltipCallbackData data );
|
|
|
|
inline void defaultMarkdownTooltipCallback( MarkdownTooltipCallbackData data_ )
|
|
{
|
|
if( data_.linkData.isImage )
|
|
{
|
|
ImGui::SetTooltip( "%.*s", data_.linkData.linkLength, data_.linkData.link );
|
|
}
|
|
else
|
|
{
|
|
ImGui::SetTooltip( "%s Open in browser\n%.*s", data_.linkIcon, data_.linkData.linkLength, data_.linkData.link );
|
|
}
|
|
}
|
|
|
|
typedef MarkdownImageData MarkdownImageCallback( MarkdownLinkCallbackData data );
|
|
typedef void MarkdownFormalCallback( const MarkdownFormatInfo& markdownFormatInfo_, bool start_ );
|
|
|
|
inline void defaultMarkdownFormatCallback( const MarkdownFormatInfo& markdownFormatInfo_, bool start_ );
|
|
|
|
struct MarkdownHeadingFormat
|
|
{
|
|
ImFont* font; // ImGui font
|
|
bool separator; // if true, an underlined separator is drawn after the header
|
|
};
|
|
|
|
// Configuration struct for Markdown
|
|
// - linkCallback is called when a link is clicked on
|
|
// - 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)
|
|
// - headingFormats controls the format of heading H1 to H3, those above H3 use H3 format
|
|
struct MarkdownConfig
|
|
{
|
|
static const int NUMHEADINGS = 3;
|
|
|
|
MarkdownLinkCallback* linkCallback = NULL;
|
|
MarkdownTooltipCallback* tooltipCallback = NULL;
|
|
MarkdownImageCallback* imageCallback = NULL;
|
|
const char* linkIcon = ""; // icon displayd in link tooltip
|
|
MarkdownHeadingFormat headingFormats[ NUMHEADINGS ] = { { NULL, true }, { NULL, true }, { NULL, true } };
|
|
void* userData = NULL;
|
|
MarkdownFormalCallback* formatCallback = defaultMarkdownFormatCallback;
|
|
};
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// External interface
|
|
//-----------------------------------------------------------------------------
|
|
|
|
inline void Markdown( const char* markdown_, size_t markdownLength_, const MarkdownConfig& mdConfig_ );
|
|
|
|
//-----------------------------------------------------------------------------
|
|
// Internals
|
|
//-----------------------------------------------------------------------------
|
|
|
|
struct TextRegion;
|
|
struct Line;
|
|
inline void UnderLine( ImColor col_ );
|
|
inline void RenderLine( const char* markdown_, Line& line_, TextRegion& textRegion_, const MarkdownConfig& mdConfig_ );
|
|
|
|
struct TextRegion
|
|
{
|
|
TextRegion() : indentX( 0.0f )
|
|
{
|
|
}
|
|
~TextRegion()
|
|
{
|
|
ResetIndent();
|
|
}
|
|
|
|
// ImGui::TextWrapped will wrap at the starting position
|
|
// so to work around this we render using our own wrapping for the first line
|
|
void RenderTextWrapped( const char* text_, const char* text_end_, bool bIndentToHere_ = false )
|
|
{
|
|
float scale = ImGui::GetIO().FontGlobalScale;
|
|
float widthLeft = GetContentRegionAvail().x;
|
|
const char* endLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthLeft );
|
|
ImGui::TextUnformatted( text_, endLine );
|
|
if( bIndentToHere_ )
|
|
{
|
|
float indentNeeded = GetContentRegionAvail().x - widthLeft;
|
|
if( indentNeeded )
|
|
{
|
|
ImGui::Indent( indentNeeded );
|
|
indentX += indentNeeded;
|
|
}
|
|
}
|
|
widthLeft = GetContentRegionAvail().x;
|
|
while( endLine < text_end_ )
|
|
{
|
|
text_ = endLine;
|
|
if( *text_ == ' ' ) { ++text_; } // skip a space at start of line
|
|
endLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthLeft );
|
|
if( text_ == endLine )
|
|
{
|
|
endLine++;
|
|
}
|
|
ImGui::TextUnformatted( text_, endLine );
|
|
}
|
|
}
|
|
|
|
void RenderListTextWrapped( const char* text_, const char* text_end_ )
|
|
{
|
|
ImGui::Bullet();
|
|
ImGui::SameLine();
|
|
RenderTextWrapped( text_, text_end_, true );
|
|
}
|
|
|
|
bool RenderLinkText( const char* text_, const char* text_end_, const Link& link_,
|
|
const char* markdown_, const MarkdownConfig& mdConfig_, const char** linkHoverStart_ );
|
|
|
|
void RenderLinkTextWrapped( const char* text_, const char* text_end_, const Link& link_,
|
|
const char* markdown_, const MarkdownConfig& mdConfig_, const char** linkHoverStart_, bool bIndentToHere_ = false );
|
|
|
|
void ResetIndent()
|
|
{
|
|
if( indentX > 0.0f )
|
|
{
|
|
ImGui::Unindent( indentX );
|
|
}
|
|
indentX = 0.0f;
|
|
}
|
|
|
|
private:
|
|
float indentX;
|
|
};
|
|
|
|
// Text that starts after a new line (or at beginning) and ends with a newline (or at end)
|
|
struct Line {
|
|
bool isHeading = false;
|
|
bool isEmphasis = false;
|
|
bool isUnorderedListStart = false;
|
|
bool isLeadingSpace = true; // spaces at start of line
|
|
int leadSpaceCount = 0;
|
|
int headingCount = 0;
|
|
int emphasisCount = 0;
|
|
int lineStart = 0;
|
|
int lineEnd = 0;
|
|
int lastRenderPosition = 0; // lines may get rendered in multiple pieces
|
|
};
|
|
|
|
struct TextBlock { // subset of line
|
|
int start = 0;
|
|
int stop = 0;
|
|
int size() const
|
|
{
|
|
return stop - start;
|
|
}
|
|
};
|
|
|
|
struct Link {
|
|
enum LinkState {
|
|
NO_LINK,
|
|
HAS_SQUARE_BRACKET_OPEN,
|
|
HAS_SQUARE_BRACKETS,
|
|
HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN,
|
|
};
|
|
LinkState state = NO_LINK;
|
|
TextBlock text;
|
|
TextBlock url;
|
|
bool isImage = false;
|
|
int num_brackets_open = 0;
|
|
};
|
|
|
|
struct Emphasis {
|
|
enum EmphasisState {
|
|
NONE,
|
|
LEFT,
|
|
MIDDLE,
|
|
RIGHT,
|
|
};
|
|
EmphasisState state = NONE;
|
|
TextBlock text;
|
|
char sym;
|
|
};
|
|
|
|
inline void UnderLine( ImColor col_ )
|
|
{
|
|
ImVec2 min = ImGui::GetItemRectMin();
|
|
ImVec2 max = ImGui::GetItemRectMax();
|
|
min.y = max.y;
|
|
ImGui::GetWindowDrawList()->AddLine( min, max, col_, 1.0f );
|
|
}
|
|
|
|
inline void RenderLine( const char* markdown_, Line& line_, TextRegion& textRegion_, const MarkdownConfig& mdConfig_ )
|
|
{
|
|
// indent
|
|
int indentStart = 0;
|
|
if( line_.isUnorderedListStart ) // ImGui unordered list render always adds one indent
|
|
{
|
|
indentStart = 1;
|
|
}
|
|
for( int j = indentStart; j < line_.leadSpaceCount / 2; ++j ) // add indents
|
|
{
|
|
ImGui::Indent();
|
|
}
|
|
|
|
// render
|
|
MarkdownFormatInfo formatInfo;
|
|
formatInfo.config = &mdConfig_;
|
|
int textStart = line_.lastRenderPosition + 1;
|
|
int textSize = line_.lineEnd - textStart;
|
|
if( line_.isUnorderedListStart ) // render unordered list
|
|
{
|
|
formatInfo.type = MarkdownFormatType::UNORDERED_LIST;
|
|
mdConfig_.formatCallback( formatInfo, true );
|
|
const char* text = markdown_ + textStart + 1;
|
|
textRegion_.RenderListTextWrapped( text, text + textSize - 1 );
|
|
}
|
|
else if( line_.isHeading ) // render heading
|
|
{
|
|
formatInfo.level = line_.headingCount;
|
|
formatInfo.type = MarkdownFormatType::HEADING;
|
|
mdConfig_.formatCallback( formatInfo, true );
|
|
const char* text = markdown_ + textStart + 1;
|
|
textRegion_.RenderTextWrapped( text, text + textSize - 1 );
|
|
}
|
|
else if( line_.isEmphasis ) // render emphasis
|
|
{
|
|
formatInfo.level = line_.emphasisCount;
|
|
formatInfo.type = MarkdownFormatType::EMPHASIS;
|
|
mdConfig_.formatCallback(formatInfo, true);
|
|
const char* text = markdown_ + textStart;
|
|
textRegion_.RenderTextWrapped(text, text + textSize);
|
|
}
|
|
else // render a normal paragraph chunk
|
|
{
|
|
formatInfo.type = MarkdownFormatType::NORMAL_TEXT;
|
|
mdConfig_.formatCallback( formatInfo, true );
|
|
const char* text = markdown_ + textStart;
|
|
textRegion_.RenderTextWrapped( text, text + textSize );
|
|
}
|
|
mdConfig_.formatCallback( formatInfo, false );
|
|
|
|
// unindent
|
|
for( int j = indentStart; j < line_.leadSpaceCount / 2; ++j )
|
|
{
|
|
ImGui::Unindent();
|
|
}
|
|
}
|
|
|
|
// render markdown
|
|
inline void Markdown( const char* markdown_, size_t markdownLength_, const MarkdownConfig& mdConfig_ )
|
|
{
|
|
static const char* linkHoverStart = NULL; // we need to preserve status of link hovering between frames
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
Line line;
|
|
Link link;
|
|
Emphasis em;
|
|
TextRegion textRegion;
|
|
|
|
char c = 0;
|
|
for( int i=0; i < (int)markdownLength_; ++i )
|
|
{
|
|
c = markdown_[i]; // get the character at index
|
|
if( c == 0 ) { break; } // shouldn't happen but don't go beyond 0.
|
|
|
|
// If we're at the beginning of the line, count any spaces
|
|
if( line.isLeadingSpace )
|
|
{
|
|
if( c == ' ' )
|
|
{
|
|
++line.leadSpaceCount;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
line.isLeadingSpace = false;
|
|
line.lastRenderPosition = i - 1;
|
|
if(( c == '*' ) && ( line.leadSpaceCount >= 2 ))
|
|
{
|
|
if( ( (int)markdownLength_ > i + 1 ) && ( markdown_[ i + 1 ] == ' ' ) ) // space after '*'
|
|
{
|
|
line.isUnorderedListStart = true;
|
|
++i;
|
|
++line.lastRenderPosition;
|
|
}
|
|
// carry on processing as could be emphasis
|
|
}
|
|
else if( c == '#' )
|
|
{
|
|
line.headingCount++;
|
|
bool bContinueChecking = true;
|
|
int j = i;
|
|
while( ++j < (int)markdownLength_ && bContinueChecking )
|
|
{
|
|
c = markdown_[j];
|
|
switch( c )
|
|
{
|
|
case '#':
|
|
line.headingCount++;
|
|
break;
|
|
case ' ':
|
|
line.lastRenderPosition = j - 1;
|
|
i = j;
|
|
line.isHeading = true;
|
|
bContinueChecking = false;
|
|
break;
|
|
default:
|
|
line.isHeading = false;
|
|
bContinueChecking = false;
|
|
break;
|
|
}
|
|
}
|
|
if( line.isHeading )
|
|
{
|
|
// reset emphasis status, we do not support emphasis around headers for now
|
|
em = Emphasis();
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test to see if we have a link
|
|
switch( link.state )
|
|
{
|
|
case Link::NO_LINK:
|
|
if( c == '[' && !line.isHeading ) // we do not support headings with links for now
|
|
{
|
|
link.state = Link::HAS_SQUARE_BRACKET_OPEN;
|
|
link.text.start = i + 1;
|
|
if( i > 0 && markdown_[i - 1] == '!' )
|
|
{
|
|
link.isImage = true;
|
|
}
|
|
}
|
|
break;
|
|
case Link::HAS_SQUARE_BRACKET_OPEN:
|
|
if( c == ']' )
|
|
{
|
|
link.state = Link::HAS_SQUARE_BRACKETS;
|
|
link.text.stop = i;
|
|
}
|
|
break;
|
|
case Link::HAS_SQUARE_BRACKETS:
|
|
if( c == '(' )
|
|
{
|
|
link.state = Link::HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN;
|
|
link.url.start = i + 1;
|
|
link.num_brackets_open = 1;
|
|
}
|
|
break;
|
|
case Link::HAS_SQUARE_BRACKETS_ROUND_BRACKET_OPEN:
|
|
if( c == '(' )
|
|
{
|
|
++link.num_brackets_open;
|
|
}
|
|
else if( c == ')' )
|
|
{
|
|
--link.num_brackets_open;
|
|
}
|
|
if( link.num_brackets_open == 0 )
|
|
{
|
|
// reset emphasis status, we do not support emphasis around links for now
|
|
em = Emphasis();
|
|
// render previous line content
|
|
line.lineEnd = link.text.start - ( link.isImage ? 2 : 1 );
|
|
RenderLine( markdown_, line, textRegion, mdConfig_ );
|
|
line.leadSpaceCount = 0;
|
|
link.url.stop = i;
|
|
line.isUnorderedListStart = false; // the following text shouldn't have bullets
|
|
ImGui::SameLine( 0.0f, 0.0f );
|
|
if( link.isImage ) // it's an image, render it.
|
|
{
|
|
bool drawnImage = false;
|
|
bool useLinkCallback = false;
|
|
if( mdConfig_.imageCallback )
|
|
{
|
|
MarkdownImageData imageData = mdConfig_.imageCallback( { markdown_ + link.text.start, link.text.size(), markdown_ + link.url.start, link.url.size(), mdConfig_.userData, true } );
|
|
useLinkCallback = imageData.useLinkCallback;
|
|
if( imageData.isValid )
|
|
{
|
|
ImGui::Image( imageData.user_texture_id, imageData.size, imageData.uv0, imageData.uv1, imageData.tint_col, imageData.border_col );
|
|
drawnImage = true;
|
|
}
|
|
}
|
|
if( !drawnImage )
|
|
{
|
|
ImGui::Text( "( Image %.*s not loaded )", link.url.size(), markdown_ + link.url.start );
|
|
}
|
|
if( ImGui::IsItemHovered() )
|
|
{
|
|
if( ImGui::IsMouseReleased( 0 ) && mdConfig_.linkCallback && useLinkCallback )
|
|
{
|
|
mdConfig_.linkCallback( { markdown_ + link.text.start, link.text.size(), markdown_ + link.url.start, link.url.size(), mdConfig_.userData, true } );
|
|
}
|
|
if( link.text.size() > 0 && mdConfig_.tooltipCallback )
|
|
{
|
|
mdConfig_.tooltipCallback( { { markdown_ + link.text.start, link.text.size(), markdown_ + link.url.start, link.url.size(), mdConfig_.userData, true }, mdConfig_.linkIcon } );
|
|
}
|
|
}
|
|
}
|
|
else // it's a link, render it.
|
|
{
|
|
textRegion.RenderLinkTextWrapped( markdown_ + link.text.start, markdown_ + link.text.start + link.text.size(), link, markdown_, mdConfig_, &linkHoverStart, false );
|
|
}
|
|
ImGui::SameLine( 0.0f, 0.0f );
|
|
// reset the link by reinitializing it
|
|
link = Link();
|
|
line.lastRenderPosition = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Test to see if we have emphasis styling
|
|
switch( em.state )
|
|
{
|
|
case Emphasis::NONE:
|
|
if( link.state == Link::NO_LINK && !line.isHeading )
|
|
{
|
|
int next = i + 1;
|
|
int prev = i - 1;
|
|
if( ( c == '*' || c == '_' )
|
|
&& ( i == line.lineStart
|
|
|| markdown_[ prev ] == ' '
|
|
|| markdown_[ prev ] == '\t' ) // empasis must be preceded by whitespace or line start
|
|
&& (int)markdownLength_ > next // emphasis must precede non-whitespace
|
|
&& markdown_[ next ] != ' '
|
|
&& markdown_[ next ] != '\n'
|
|
&& markdown_[ next ] != '\t' )
|
|
{
|
|
em.state = Emphasis::LEFT;
|
|
em.sym = c;
|
|
em.text.start = i;
|
|
line.emphasisCount = 1;
|
|
continue;
|
|
}
|
|
}
|
|
break;
|
|
case Emphasis::LEFT:
|
|
if( em.sym == c )
|
|
{
|
|
++line.emphasisCount;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
em.text.start = i;
|
|
em.state = Emphasis::MIDDLE;
|
|
}
|
|
break;
|
|
case Emphasis::MIDDLE:
|
|
if( em.sym == c )
|
|
{
|
|
em.state = Emphasis::RIGHT;
|
|
em.text.stop = i;
|
|
// pass through to case Emphasis::RIGHT
|
|
}
|
|
else
|
|
{
|
|
break;
|
|
}
|
|
case Emphasis::RIGHT:
|
|
if( em.sym == c )
|
|
{
|
|
if( line.emphasisCount < 3 && ( i - em.text.stop + 1 == line.emphasisCount ) )
|
|
{
|
|
// render text up to emphasis
|
|
int lineEnd = em.text.start - line.emphasisCount;
|
|
if( lineEnd > line.lineStart )
|
|
{
|
|
line.lineEnd = lineEnd;
|
|
RenderLine( markdown_, line, textRegion, mdConfig_ );
|
|
ImGui::SameLine( 0.0f, 0.0f );
|
|
line.isUnorderedListStart = false;
|
|
line.leadSpaceCount = 0;
|
|
}
|
|
line.isEmphasis = true;
|
|
line.lastRenderPosition = em.text.start - 1;
|
|
line.lineStart = em.text.start;
|
|
line.lineEnd = em.text.stop;
|
|
RenderLine( markdown_, line, textRegion, mdConfig_ );
|
|
ImGui::SameLine( 0.0f, 0.0f );
|
|
line.isEmphasis = false;
|
|
line.lastRenderPosition = i;
|
|
em = Emphasis();
|
|
}
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
em.state = Emphasis::NONE;
|
|
// render text up to here
|
|
int start = em.text.start - line.emphasisCount;
|
|
if( start < line.lineStart )
|
|
{
|
|
line.lineEnd = line.lineStart;
|
|
line.lineStart = start;
|
|
line.lastRenderPosition = start - 1;
|
|
RenderLine(markdown_, line, textRegion, mdConfig_);
|
|
line.lineStart = line.lineEnd;
|
|
line.lastRenderPosition = line.lineStart - 1;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
// handle end of line (render)
|
|
if( c == '\n' )
|
|
{
|
|
// first check if the line is a horizontal rule
|
|
line.lineEnd = i;
|
|
if( em.state == Emphasis::MIDDLE && line.emphasisCount >=3 &&
|
|
( line.lineStart + line.emphasisCount ) == i )
|
|
{
|
|
ImGui::Separator();
|
|
}
|
|
else
|
|
{
|
|
// render the line: multiline emphasis requires a complex implementation so not supporting
|
|
RenderLine( markdown_, line, textRegion, mdConfig_ );
|
|
}
|
|
|
|
// reset the line and emphasis state
|
|
line = Line();
|
|
em = Emphasis();
|
|
|
|
line.lineStart = i + 1;
|
|
line.lastRenderPosition = i;
|
|
|
|
textRegion.ResetIndent();
|
|
|
|
// reset the link
|
|
link = Link();
|
|
}
|
|
}
|
|
|
|
if( em.state == Emphasis::LEFT && line.emphasisCount >= 3 )
|
|
{
|
|
ImGui::Separator();
|
|
}
|
|
else
|
|
{
|
|
// render any remaining text if last char wasn't 0
|
|
if( markdownLength_ && line.lineStart < (int)markdownLength_ && markdown_[ line.lineStart ] != 0 )
|
|
{
|
|
// handle both null terminated and non null terminated strings
|
|
line.lineEnd = (int)markdownLength_;
|
|
if( 0 == markdown_[ line.lineEnd - 1 ] )
|
|
{
|
|
--line.lineEnd;
|
|
}
|
|
RenderLine( markdown_, line, textRegion, mdConfig_ );
|
|
}
|
|
}
|
|
}
|
|
|
|
inline bool TextRegion::RenderLinkText( const char* text_, const char* text_end_, const Link& link_,
|
|
const char* markdown_, const MarkdownConfig& mdConfig_, const char** linkHoverStart_ )
|
|
{
|
|
MarkdownFormatInfo formatInfo;
|
|
formatInfo.config = &mdConfig_;
|
|
formatInfo.type = MarkdownFormatType::LINK;
|
|
mdConfig_.formatCallback( formatInfo, true );
|
|
ImGui::PushTextWrapPos( -1.0f );
|
|
ImGui::TextUnformatted( text_, text_end_ );
|
|
ImGui::PopTextWrapPos();
|
|
|
|
bool bThisItemHovered = ImGui::IsItemHovered();
|
|
if(bThisItemHovered)
|
|
{
|
|
*linkHoverStart_ = markdown_ + link_.text.start;
|
|
}
|
|
bool bHovered = bThisItemHovered || ( *linkHoverStart_ == ( markdown_ + link_.text.start ) );
|
|
|
|
formatInfo.itemHovered = bHovered;
|
|
mdConfig_.formatCallback( formatInfo, false );
|
|
|
|
if(bHovered)
|
|
{
|
|
if( ImGui::IsMouseReleased( 0 ) && mdConfig_.linkCallback )
|
|
{
|
|
mdConfig_.linkCallback( { markdown_ + link_.text.start, link_.text.size(), markdown_ + link_.url.start, link_.url.size(), mdConfig_.userData, false } );
|
|
}
|
|
if( mdConfig_.tooltipCallback )
|
|
{
|
|
mdConfig_.tooltipCallback( { { markdown_ + link_.text.start, link_.text.size(), markdown_ + link_.url.start, link_.url.size(), mdConfig_.userData, false }, mdConfig_.linkIcon } );
|
|
}
|
|
}
|
|
return bThisItemHovered;
|
|
}
|
|
|
|
// IsCharInsideWord based on ImGui's CalcWordWrapPositionA
|
|
inline bool IsCharInsideWord( char c_ )
|
|
{
|
|
return c_ != ' ' && c_ != '.' && c_ != ',' && c_ != ';' && c_ != '!' && c_ != '?' && c_ != '\"';
|
|
}
|
|
|
|
inline void TextRegion::RenderLinkTextWrapped( const char* text_, const char* text_end_, const Link& link_,
|
|
const char* markdown_, const MarkdownConfig& mdConfig_, const char** linkHoverStart_, bool bIndentToHere_ )
|
|
{
|
|
float scale = ImGui::GetIO().FontGlobalScale;
|
|
float widthLeft = GetContentRegionAvail().x;
|
|
const char* endLine = text_;
|
|
if( widthLeft > 0.0f )
|
|
{
|
|
endLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthLeft );
|
|
}
|
|
|
|
if( endLine > text_ && endLine < text_end_ )
|
|
{
|
|
if( IsCharInsideWord( *endLine ) )
|
|
{
|
|
// see if we can do a better cut.
|
|
float widthNextLine = GetContentRegionMax().x;
|
|
const char* endNextLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthNextLine );
|
|
if( endNextLine == text_end_ || ( endNextLine <= text_end_ && !IsCharInsideWord( *endNextLine ) ) )
|
|
{
|
|
// can possibly do better if go to next line
|
|
endLine = text_;
|
|
}
|
|
}
|
|
}
|
|
bool bHovered = RenderLinkText( text_, endLine, link_, markdown_, mdConfig_, linkHoverStart_ );
|
|
if( bIndentToHere_ )
|
|
{
|
|
float indentNeeded = GetContentRegionAvail().x - widthLeft;
|
|
if( indentNeeded )
|
|
{
|
|
ImGui::Indent( indentNeeded );
|
|
indentX += indentNeeded;
|
|
}
|
|
}
|
|
widthLeft = GetContentRegionAvail().x;
|
|
while( endLine < text_end_ )
|
|
{
|
|
text_ = endLine;
|
|
if( *text_ == ' ' ) { ++text_; } // skip a space at start of line
|
|
endLine = ImGui::GetFont()->CalcWordWrapPositionA( scale, text_, text_end_, widthLeft );
|
|
if( text_ == endLine )
|
|
{
|
|
endLine++;
|
|
}
|
|
bool bThisLineHovered = RenderLinkText( text_, endLine, link_, markdown_, mdConfig_, linkHoverStart_ );
|
|
bHovered = bHovered || bThisLineHovered;
|
|
}
|
|
if( !bHovered && *linkHoverStart_ == markdown_ + link_.text.start )
|
|
{
|
|
*linkHoverStart_ = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
inline void defaultMarkdownFormatCallback( const MarkdownFormatInfo& markdownFormatInfo_, bool start_ )
|
|
{
|
|
switch( markdownFormatInfo_.type )
|
|
{
|
|
case MarkdownFormatType::NORMAL_TEXT:
|
|
break;
|
|
case MarkdownFormatType::EMPHASIS:
|
|
{
|
|
MarkdownHeadingFormat fmt;
|
|
// default styling for emphasis uses last headingFormats - for your own styling
|
|
// implement EMPHASIS in your formatCallback
|
|
if( markdownFormatInfo_.level == 1 )
|
|
{
|
|
// normal emphasis
|
|
if( start_ )
|
|
{
|
|
ImGui::PushStyleColor( ImGuiCol_Text, ImGui::GetStyle().Colors[ ImGuiCol_TextDisabled ] );
|
|
}
|
|
else
|
|
{
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// strong emphasis
|
|
fmt = markdownFormatInfo_.config->headingFormats[ MarkdownConfig::NUMHEADINGS - 1 ];
|
|
if( start_ )
|
|
{
|
|
if( fmt.font )
|
|
{
|
|
ImGui::PushFont( fmt.font );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( fmt.font )
|
|
{
|
|
ImGui::PopFont();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case MarkdownFormatType::HEADING:
|
|
{
|
|
MarkdownHeadingFormat fmt;
|
|
if( markdownFormatInfo_.level > MarkdownConfig::NUMHEADINGS )
|
|
{
|
|
fmt = markdownFormatInfo_.config->headingFormats[ MarkdownConfig::NUMHEADINGS - 1 ];
|
|
}
|
|
else
|
|
{
|
|
fmt = markdownFormatInfo_.config->headingFormats[ markdownFormatInfo_.level - 1 ];
|
|
}
|
|
if( start_ )
|
|
{
|
|
if( fmt.font )
|
|
{
|
|
ImGui::PushFont( fmt.font );
|
|
}
|
|
ImGui::NewLine();
|
|
}
|
|
else
|
|
{
|
|
if( fmt.separator )
|
|
{
|
|
ImGui::Separator();
|
|
ImGui::NewLine();
|
|
}
|
|
else
|
|
{
|
|
ImGui::NewLine();
|
|
}
|
|
if( fmt.font )
|
|
{
|
|
ImGui::PopFont();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case MarkdownFormatType::UNORDERED_LIST:
|
|
break;
|
|
case MarkdownFormatType::LINK:
|
|
if( start_ )
|
|
{
|
|
ImGui::PushStyleColor( ImGuiCol_Text, ImGui::GetStyle().Colors[ ImGuiCol_ButtonHovered ] );
|
|
}
|
|
else
|
|
{
|
|
ImGui::PopStyleColor();
|
|
if( markdownFormatInfo_.itemHovered )
|
|
{
|
|
ImGui::UnderLine( ImGui::GetStyle().Colors[ ImGuiCol_ButtonHovered ] );
|
|
}
|
|
else
|
|
{
|
|
ImGui::UnderLine( ImGui::GetStyle().Colors[ ImGuiCol_Button ] );
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|