preview
MQL5 Trading Tools (Part 33): Building a Rich Content Markup Documentation System for MQL5 Programs

MQL5 Trading Tools (Part 33): Building a Rich Content Markup Documentation System for MQL5 Programs

MetaTrader 5Trading systems |
800 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

Imagine attaching your program to a chart and opening a clear, scrollable, tabbed document inside MetaTrader 5 (MetaTrader 5). It includes styled headings, colored text, embedded images, and interactive navigation. No external files or broken links. No guessing. That is exactly what we built in this article. In Part 9 of this series, we developed a setup wizard for Expert Advisors in MetaQuotes Language 5 (MQL5) that uses standard chart objects and a scrollable text guide. In Part 32, we enhanced the tools palette with a crosshair reticle and magnifier lens.

Part 33 builds directly on that. Here, we take the foundation much further by rebuilding the documentation concept from the ground up using the CCanvas class. This enables a fully canvas-rendered, rich-content document system. It supports tabs, inline markup styling, embedded images, themed palettes, supersampled anti-aliased rendering, and a scrollable body. These features allow MQL5 developers and algorithmic traders to deliver self-contained documentation directly inside their programs. We will cover the following topics:

  1. Rethinking In-Chart Documentation — Concept and Design
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion

By the end, you will have a reusable documentation engine ready to embed into any MQL5 program you build — let's get into it.


Rethinking In-Chart Documentation — Concept and Design

Documentation has always lived outside the tool. A separate PDF, an external link, a forum thread the user has to go and find, and more often than not, never does. The idea here is straightforward: bring the documentation inside the program itself, formatted and interactive, always available the moment the program is attached to a chart. Think of it as a user manual or a rich-content guide — the kind of formatted document you would expect to open in a PDF reader or a word processor, but living natively inside MetaTrader 5, attached to the very program it describes.

At its core, a rich content document system is built around a few key ideas. Content is organized into tabs, each covering a distinct topic, so the user can navigate freely without scrolling through everything at once. Within each tab, paragraphs carry meaning through formatting — headings stand out, warnings are visually distinct from tips, numbered lists guide step-by-step, and inline text can be bold, italic, underlined, colored, or highlighted. Images sit inline with the text, scaled to fit the panel. Everything scrolls smoothly within a defined body area, and the whole panel adapts to the chart size.

The rendering approach is what separates this from a plain chart object panel. Rather than placing individual label objects for every piece of text, everything is drawn pixel by pixel onto dedicated drawing surfaces. To achieve smooth, professional edges on rounded corners, buttons, and scrollbar pills, we use supersampling — rendering each element at four times the display resolution on an internal high-resolution surface, then downsampling it to the visible display. This eliminates the jagged edges that bitmap rendering typically produces at normal scale.

Think of this system like a well-written trading plan. It doesn't change how the tool works, but it improves how confidently and correctly the tool is used. A trader who understands what each input controls, what conditions the program is designed for, and what to expect from it is far less likely to misuse it. Embedded documentation is not decoration — it is risk management for the developer. We build this in steps. First, we define the layout and theme. Next, we implement supersampled rendering. Then we add paragraphs and inline markup, handle tabs and scrolling, embed images, and wire up interactions. Here is a visualization of our objectives.

RICH CONTENT DOCUMENTATION ARCHITECTURE


Implementation in MQL5

Defining the Core Structures, Enumerations, and Forward Declarations

Before any rendering or interaction logic can take place, we lay out the entire architectural foundation of the rich content documentation system — the resources, configuration constants, data structures, state variables, and function signatures that everything else will build upon.

//+------------------------------------------------------------------+
//|                                        Rich Content Document.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict
#property description "Rich Content Document. A canvas-rendered, PDF-like documentation"
#property description "system demonstrating MQL5's full rich content capabilities."
#property description "Scrollable. Formatted. Beautiful. Right inside your MT5 chart."

#include <Canvas/Canvas.mqh>

//+------------------------------------------------------------------+
//| Resources                                                        |
//+------------------------------------------------------------------+
#resource "Rich Content Document - Logo.bmp"           // Embedded logo bitmap
#resource "Rich Content Document - MQL5 IDE.bmp"       // MQL5 IDE screenshot
#resource "Rich Content Document - MT5 Chart.bmp"      // MT5 chart screenshot
#resource "Rich Content Document - Strategy Tester.bmp"// Strategy Tester screenshot
#resource "Rich Content Document - Market Watch.bmp"   // Market Watch screenshot
#resource "Rich Content Document - Navigator.bmp"      // Navigator screenshot

//+------------------------------------------------------------------+
//| Resource path macros                                             |
//+------------------------------------------------------------------+
#define RCD_LOGO_RESOURCE     "::Rich Content Document - Logo.bmp"           // Logo resource string
#define RCD_IMG_MQL5_IDE      "::Rich Content Document - MQL5 IDE.bmp"       // MQL5 IDE resource string
#define RCD_IMG_MT5_CHART     "::Rich Content Document - MT5 Chart.bmp"      // MT5 Chart resource string
#define RCD_IMG_STRAT_TESTER  "::Rich Content Document - Strategy Tester.bmp"// Strategy Tester resource string
#define RCD_IMG_MARKET_WATCH  "::Rich Content Document - Market Watch.bmp"   // Market Watch resource string
#define RCD_IMG_NAVIGATOR     "::Rich Content Document - Navigator.bmp"      // Navigator resource string

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "RICH CONTENT DOCUMENT SETTINGS"
input int  rcdTheme        = 1;    // Theme: 0=Dark, 1=Light
input bool rcdShowOnAttach = true; // Show document on attach

//+------------------------------------------------------------------+
//| Constants                                                        |
//+------------------------------------------------------------------+
#define RCD_SS                 4        // Supersampling factor for AA rendering

#define RCD_HEADER_H           48       // Header height in pixels
#define RCD_TABS_H             36       // Tabs bar height in pixels
#define RCD_FOOTER_H           50       // Footer height in pixels
#define RCD_GAP                0        // Gap between sections

#define RCD_PAD                16       // Body text padding
#define RCD_LOGO_SIZE          32       // Logo display size in header

#define RCD_MIN_WIDTH          520      // Minimum panel width
#define RCD_MAX_WIDTH          720      // Maximum panel width
#define RCD_MIN_BODY_H         160      // Minimum body height
#define RCD_MAX_BODY_H         540      // Maximum body height
#define RCD_TOP_MARGIN         40       // Distance from top of chart
#define RCD_SIDE_MARGIN        40       // Distance from sides of chart
#define RCD_BOTTOM_MARGIN      40       // Distance from bottom of chart

#define RCD_SB_PILL_W          5        // Scrollbar pill width
#define RCD_SB_MARGIN_R        4        // Scrollbar right margin

#define RCD_CHECKBOX_SIZE      16       // Checkbox size in pixels
#define RCD_BUTTON_W           90       // Footer button width
#define RCD_BUTTON_H           30       // Footer button height

#define RCD_FONT_TITLE         11       // Header title font size
#define RCD_FONT_SUBTITLE      9        // Header subtitle font size
#define RCD_FONT_TAB           9        // Tab label font size
#define RCD_FONT_BODY          11       // Body text font size
#define RCD_FONT_HEADING       11       // Section heading font size
#define RCD_FONT_BUTTON        9        // Button label font size
#define RCD_FONT_LABEL         9        // Checkbox label font size
#define RCD_FONT_CLOSE         13       // Close button font size

#define RCD_LINE_GAP           5        // Extra pixels between text lines
#define RCD_TOP_PAD_BODY       10       // Top padding inside body area

#define RCD_IMG_COUNT          5        // Total number of content images
#define RCD_IMG_MQL5IDE_IDX    0        // Image index: MQL5 IDE
#define RCD_IMG_MT5CHART_IDX   1        // Image index: MT5 Chart
#define RCD_IMG_STTEST_IDX     2        // Image index: Strategy Tester
#define RCD_IMG_MWATCH_IDX     3        // Image index: Market Watch
#define RCD_IMG_NAV_IDX        4        // Image index: Navigator

#define RCD_TAB_COUNT          5        // Total number of tabs
#define RCD_TAB_LANGUAGE       0        // Tab index: MQL5 Language
#define RCD_TAB_METAEDITOR     1        // Tab index: MetaEditor
#define RCD_TAB_PLATFORM       2        // Tab index: MT5 Platform
#define RCD_TAB_TESTER         3        // Tab index: Strategy Tester
#define RCD_TAB_RESOURCES      4        // Tab index: Resources

#define RCD_IMG_LINE_PREFIX    "RCDIMG:"  // Prefix for image placeholder lines
#define RCD_LOGO_LINE_PREFIX   "RCDLOGO:" // Prefix for logo placeholder lines

//+------------------------------------------------------------------+
//| Paragraph type enum                                              |
//+------------------------------------------------------------------+
enum RcdParaType
  {
   RCD_PARA_EMPTY    = 0,  // Blank spacer line
   RCD_PARA_BODY     = 1,  // Plain body text
   RCD_PARA_HEADING  = 2,  // Section heading
   RCD_PARA_NUMBERED = 3,  // Numbered list item
   RCD_PARA_WARN     = 5,  // Warning block (orange/red bar)
   RCD_PARA_INFO     = 7,  // Info block (blue bar)
   RCD_PARA_ANSWER   = 9,  // Answer/tip block (green bar)
   RCD_PARA_IMG      = 11, // Image placeholder
   RCD_PARA_LOGO     = 12, // Logo placeholder
   RCD_PARA_BULLET   = 13  // Bullet list item
  };

//+------------------------------------------------------------------+
//| Inline run structure — styled text segment within a paragraph    |
//+------------------------------------------------------------------+
struct RcdRun
  {
   string text;          // Actual text content
   color  col;           // Text color (0 = inherit paragraph default)
   color  bgCol;         // Background highlight color (0 = none)
   bool   bold;          // Bold style flag
   bool   italic;        // Italic style flag
   bool   underline;     // Underline style flag
   bool   strikethrough; // Strikethrough style flag
  };

//+------------------------------------------------------------------+
//| Paragraph structure — single content unit                        |
//+------------------------------------------------------------------+
struct RcdPara
  {
   RcdParaType type;   // Paragraph type
   string      text;   // Paragraph text content
   int         imgIdx; // Image index (only for RCD_PARA_IMG)
  };

//+------------------------------------------------------------------+
//| Canvas object name strings                                       |
//+------------------------------------------------------------------+
string rcdHeaderCanvasName  = "RCD_Header";        // Header canvas object name
string rcdTabsCanvasName    = "RCD_Tabs";          // Tabs canvas object name
string rcdBodyCanvasName    = "RCD_Body";          // Body canvas object name
string rcdBodyHRCanvasName  = "RCD_Body_HR";       // Body high-res canvas name
string rcdBlockCanvasName   = "RCD_Block";         // Block overlay canvas name
string rcdFooterCanvasName  = "RCD_Footer";        // Footer canvas object name
string rcdLogoScaledResName = "::RCD_Logo_Scaled"; // Scaled logo resource name

//+------------------------------------------------------------------+
//| Canvas instances                                                 |
//+------------------------------------------------------------------+
CCanvas rcdCanvHeader;   // Header canvas
CCanvas rcdCanvTabs;     // Tabs canvas
CCanvas rcdCanvBody;     // Body canvas
CCanvas rcdCanvBodyHR;   // Body high-resolution supersampled canvas
CCanvas rcdCanvBlock;    // Block overlay canvas (text + images + scrollbar)
CCanvas rcdCanvFooter;   // Footer canvas

//+------------------------------------------------------------------+
//| Per-tab paragraph arrays                                         |
//+------------------------------------------------------------------+
RcdPara rcdTabParasLanguage[];   // MQL5 Language tab paragraphs
RcdPara rcdTabParasMetaEditor[]; // MetaEditor tab paragraphs
RcdPara rcdTabParasPlatform[];   // MT5 Platform tab paragraphs
RcdPara rcdTabParasTester[];     // Strategy Tester tab paragraphs
RcdPara rcdTabParasResources[];  // Resources tab paragraphs

//+------------------------------------------------------------------+
//| State variables                                                  |
//+------------------------------------------------------------------+
bool rcdIsActive     = false; // Document is currently displayed
bool rcdContentBuilt = false; // Content arrays have been populated
bool rcdLogoLoaded   = false; // Header logo was successfully loaded

//--- Layout geometry
int rcdPanelX      = 0; // Panel left edge X coordinate
int rcdPanelY      = 0; // Panel top edge Y coordinate
int rcdPanelWidth  = 0; // Panel total width
int rcdBodyHeight  = 0; // Body section height
int rcdTotalHeight = 0; // Total panel height
int rcdHeaderY     = 0; // Header top Y coordinate
int rcdTabsY       = 0; // Tabs bar top Y coordinate
int rcdBodyY       = 0; // Body top Y coordinate
int rcdFooterY     = 0; // Footer top Y coordinate

//--- Tab and content state
int     rcdActiveTab          = RCD_TAB_LANGUAGE; // Currently selected tab index
string  rcdTabTitles[RCD_TAB_COUNT];              // Display names for each tab
RcdPara rcdCurrentParas[];                        // Working copy of active tab paragraphs
string  rcdWrappedLines[];                        // Wrapped text lines for rendering
int     rcdLineHeight         = 0;                // Pixel height of one text line
int     rcdTotalContentHeight = 0;                // Total pixel height of all lines

//--- Scroll state
int  rcdScrollPos          = 0;     // Current scroll offset in pixels
int  rcdMaxScroll          = 0;     // Maximum allowed scroll position
int  rcdSliderHeight       = 0;     // Computed scrollbar pill height
bool rcdScrollVisible      = false; // Whether scrollbar is shown
bool rcdIsDraggingSlider   = false; // Slider drag in progress
int  rcdDragStartMouseY    = 0;     // Mouse Y when drag started
int  rcdDragStartScrollPos = 0;     // Scroll position when drag started

//--- Hover state flags
bool rcdHoverClose    = false;           // Mouse over close button
bool rcdHoverOK       = false;           // Mouse over OK button
bool rcdHoverCancel   = false;           // Mouse over Cancel button
bool rcdHoverCheckbox = false;           // Mouse over checkbox
bool rcdHoverSlider   = false;           // Mouse over scrollbar slider
bool rcdHoverTabs[RCD_TAB_COUNT];        // Mouse over each tab
bool rcdMouseInBody   = false;           // Mouse is inside body area
bool rcdPrevMouseInBody  = false;        // Previous mouse-in-body state
int  rcdPrevMouseState   = 0;            // Previous mouse button state

//--- Checkbox state
bool rcdDontShowAgain = false; // User checked "don't show again"

//--- Image cache arrays (5 images x flat pixel storage)
int  rcdImgOrigW[RCD_IMG_COUNT];         // Original image widths
int  rcdImgOrigH[RCD_IMG_COUNT];         // Original image heights
int  rcdImgScaledW[RCD_IMG_COUNT];       // Scaled image widths
int  rcdImgScaledH[RCD_IMG_COUNT];       // Scaled image heights
bool rcdImgLoaded[RCD_IMG_COUNT];        // Image was loaded from resource
bool rcdImgCacheValid[RCD_IMG_COUNT];    // Scaled cache is current
int  rcdImgCacheForWidth[RCD_IMG_COUNT]; // Panel width when cache was built
uint rcdImgPixels0[];                    // Pixel data for image 0 (MQL5 IDE)
uint rcdImgPixels1[];                    // Pixel data for image 1 (MT5 Chart)
uint rcdImgPixels2[];                    // Pixel data for image 2 (Strategy Tester)
uint rcdImgPixels3[];                    // Pixel data for image 3 (Market Watch)
uint rcdImgPixels4[];                    // Pixel data for image 4 (Navigator)

//--- Logo display (large version used in Resources tab)
uint rcdLogoDisplayPixels[];  // Scaled logo pixel data for body display
int  rcdLogoDisplayW    = 0;  // Logo display width
int  rcdLogoDisplayH    = 0;  // Logo display height
bool rcdLogoDisplayReady = false; // Logo display data is ready

//+------------------------------------------------------------------+
//| Theme color variables                                            |
//+------------------------------------------------------------------+
color rcdBg;               // Main background color
color rcdPanelAlt;         // Alternate panel color (footer)
color rcdHeaderBg;         // Header background color
color rcdTabsBg;           // Tabs bar background color
color rcdBorder;           // Border color
color rcdHeaderText;       // Header title text color
color rcdSubText;          // Subtitle and secondary text color
color rcdBodyText;         // Body paragraph text color
color rcdHeadingText;      // Section heading text color
color rcdAccentColor;      // Primary accent color
color rcdLinkColor;        // Hyperlink text color
color rcdHighlightColor;   // Code highlight and gold text color
color rcdTabInactive;      // Inactive tab text color
color rcdTabActive;        // Active tab text color
color rcdTabHover;         // Hovered tab text color
color rcdButtonBg;         // Primary button background color
color rcdButtonBgHover;    // Primary button hover background color
color rcdButtonText;       // Button text color
color rcdCancelBg;         // Cancel button background color
color rcdCancelBgHover;    // Cancel button hover background color
color rcdCheckboxBg;       // Checkbox background color
color rcdCheckboxBorder;   // Checkbox border color
color rcdCheckboxChecked;  // Checkbox checked fill color
color rcdCloseColor;       // Close button icon color
color rcdCloseHoverColor;  // Close button hover icon color
color rcdScrollSlider;     // Scrollbar pill normal color
color rcdScrollSliderHover;// Scrollbar pill hover color
color rcdScrollSliderDrag; // Scrollbar pill drag color

//+------------------------------------------------------------------+
//| Forward declarations                                             |
//+------------------------------------------------------------------+
void        RcdApplyTheme(int theme);
void        RcdCalculateLayout();
bool        RcdCreateCanvases();
void        RcdDestroyCanvases();
bool        RcdLoadLogo();
void        RcdLoadLogoDisplay();
bool        RcdLoadImage(int imgIndex);
void        RcdEnsureImageCache(int imgIndex);
void        RcdBuildContent();
void        RcdRebuildWrappedLines();
void        RcdRenderAll();
void        RcdRenderHeader();
void        RcdRenderTabs();
void        RcdRenderBody();
void        RcdRenderFooter();
void        RcdUpdateHovers(int mouseX, int mouseY);
void        RcdAddPara(RcdPara &paraArray[], RcdParaType paraType, const string paraText = "", int imageIndex = -1);
void        RcdCopyParas(const RcdPara &sourceArray[], RcdPara &destArray[]);
void        RcdGetTabParas(int tabIndex, RcdPara &outputArray[]);
void        RcdWrapText(const RcdPara &paraArray[], int maxPixelWidth, string &outputLines[]);
bool        RcdHasMarkup(const string inputText);
string      RcdStripInlineTags(const string inputText);
void        RcdParseRuns(const string inputText, RcdRun &outputRuns[]);
void        RcdCopyRuns(const RcdRun &sourceRuns[], RcdRun &destRuns[]);
color       RcdResolveColorToken(const string colorToken);
void        RcdStampRuns(CCanvas &targetCanvas, int startX, int startY, int lineH, const RcdRun &runs[], color defaultColor, color stampBg, int fontSize);
void        RcdStampText(CCanvas &targetCanvas, int posX, int posY, const string displayText, const string fontName, int fontSize, color textColor, color bgColor, bool transparentBg);
bool        RcdPointInRect(int pointX, int pointY, int rectX, int rectY, int rectW, int rectH);
int         RcdCalcSliderHeight(int visibleH, int totalH, int trackH, int minH);
void        RcdArgbSplit(uint pixelValue, uchar &alphaOut, uchar &redOut, uchar &greenOut, uchar &blueOut);
uint        RcdBlendPixel(uint destPixel, uint srcPixel);
void        RcdScaleImage(uint &pixelArray[], int origW, int origH, int newW, int newH);
uint        RcdBicubicInterp(uint &pixelArray[], int imgW, int imgH, double sampleX, double sampleY);
double      RcdBicubicComponent(uchar &componentValues[], double fracX, double fracY);
void        RcdFillRoundRectHR(CCanvas &targetCanvas, int rectX, int rectY, int rectW, int rectH, int cornerRadius, uint fillArgb);
void        RcdFillCornerQuadrantHR(CCanvas &targetCanvas, int centerX, int centerY, int radius, uint fillArgb, int signX, int signY);
void        RcdDrawRoundRectBorderHR(CCanvas &targetCanvas, int rectX, int rectY, int rectW, int rectH, int cornerRadius, uint borderArgb, bool drawTop, bool drawLeft, bool drawRight, bool drawBottom, bool arcTL, bool arcTR, bool arcBL, bool arcBR);
void        RcdDownsampleCanvas(CCanvas &targetCanvas, CCanvas &sourceCanvas);
void        RcdImgGetPixels(int imgIndex, uint &outputPixels[]);
void        RcdImgSetPixels(int imgIndex, uint &inputPixels[]);
string      RcdImgResourcePath(int imgIndex);
RcdParaType RcdLineType(const string wrappedLine);
string      RcdLineText(const string wrappedLine);
int         RcdLineIndent(const string wrappedLine);
bool        RcdIsImgLine(const string wrappedLine);
int         RcdImgLineIndex(const string wrappedLine);
int         RcdImgLineSlot(const string wrappedLine);
bool        RcdIsLogoLine(const string wrappedLine);
int         RcdLogoLineSlot(const string wrappedLine);
void        RcdShow();
void        RcdHide();
void        RcdHandleChartEvent(const int eventId, const long &lParam, const double &dParam, const string &sParam);

We start the implementation by including the canvas library and embedding six bitmap files directly into the compiled program using the #resource directive — a logo and five screenshots that will appear inline within the document body. Embedding them this way means they travel with the program file itself, requiring no external assets. We explained this in the initial version. Corresponding macro definitions are then declared to give each resource a clean, readable reference name used throughout the code.

The input parameters expose two user-facing settings: the color theme and whether the document should open automatically on attachment. A set of numeric constants then defines the panel geometry — header, tabs, footer heights, padding values, scrollbar dimensions, font sizes, body size limits, and margin distances — keeping all layout measurements in one place for easy adjustment.

Next, we define the "RcdParaType" enumeration to classify every paragraph the document can contain — blank spacers, plain body text, section headings, numbered items, bullet points, warning blocks, info blocks, answer blocks, image placeholders, and logo placeholders. This drives how each line is rendered visually. Alongside it, the "RcdRun" structure describes a single styled text segment within a paragraph, carrying the text itself plus flags for bold, italic, underline, strikethrough, a text color override, and a background highlight color. The "RcdPara" structure then represents a full paragraph, combining its type, text content, and an optional image index.

From there we declare the canvas object name strings, six canvas instances for the header, tabs, body, high-resolution body, block overlay, and footer, per-tab paragraph arrays for each of the five tabs, and a comprehensive set of state variables covering layout geometry, tab and content state, scroll position and slider dimensions, hover flags for every interactive element, checkbox state, and the image cache arrays for all five embedded images. Finally, all functions are forward-declared, so the compiler resolves references regardless of definition order. Next, we explain the key functions that bring this to life. We already covered similar canvas-based tools in previous parts of the series, and this idea is based on an existing part in the series. So the baseline is, you have interacted with most of the logic that makes this work. We will start with the logic to stamp text onto the canvas.

Two-Pass Alpha-Reconstructed Text Stamping

Standard text rendering onto a bitmap surface loses true per-pixel transparency information, producing ugly fringes when text is placed over any background other than the one it was rendered against. To solve this cleanly, we use a two-pass alpha reconstruction technique that recovers the true alpha of every glyph pixel before compositing it onto the canvas.

//+------------------------------------------------------------------+
//| Stamp text onto canvas using two-pass alpha reconstruction       |
//+------------------------------------------------------------------+
void RcdStampText(CCanvas &targetCanvas, int posX, int posY, const string displayText,
                  const string fontName, int fontSize, color textColor, color bgColor, bool transparentBg)
  {
   if(StringLen(displayText) == 0) return;
   //--- Set the font and measure the text extent
   TextSetFont(fontName, -(fontSize * 10));
   uint textW = 0, textH = 0;
   TextGetSize(displayText, textW, textH);
   if(textW == 0 || textH == 0) return;
   int lineW = (int)textW, lineH = (int)textH;
   //--- Render text onto a black background buffer
   uint bufBlack[];
   ArrayResize(bufBlack, lineW * lineH);
   ArrayFill(bufBlack, 0, lineW * lineH, 0xFF000000);
   TextOut(displayText, 0, 0, TA_LEFT | TA_TOP, bufBlack, lineW, lineH,
           ColorToARGB(textColor, 255), COLOR_FORMAT_ARGB_NORMALIZE);
   //--- Render same text onto a white background buffer
   uint bufWhite[];
   ArrayResize(bufWhite, lineW * lineH);
   ArrayFill(bufWhite, 0, lineW * lineH, 0xFFFFFFFF);
   TextOut(displayText, 0, 0, TA_LEFT | TA_TOP, bufWhite, lineW, lineH,
           ColorToARGB(textColor, 255), COLOR_FORMAT_ARGB_NORMALIZE);
   int canvasW = targetCanvas.Width(), canvasH = targetCanvas.Height();
   //--- Composite each pixel using the difference between buffers to recover true alpha
   for(int py = 0; py < lineH; py++)
     {
      int dstY = posY + py;
      if(dstY < 0 || dstY >= canvasH) continue;
      for(int px = 0; px < lineW; px++)
        {
         int dstX = posX + px;
         if(dstX < 0 || dstX >= canvasW) continue;
         uint pb = bufBlack[py * lineW + px];
         uint pw = bufWhite[py * lineW + px];
         //--- Recover alpha from the channel difference between renders
         int diffR = (int)((pw >> 16) & 0xFF) - (int)((pb >> 16) & 0xFF);
         int diffG = (int)((pw >>  8) & 0xFF) - (int)((pb >>  8) & 0xFF);
         int diffB = (int)( pw        & 0xFF) - (int)( pb        & 0xFF);
         int alpha = 255 - (diffR + diffG + diffB) / 3;
         if(alpha <= 0) continue;
         if(alpha > 255) alpha = 255;
         //--- Reconstruct true RGB from pre-multiplied black-background values
         uchar oR = (alpha > 0) ? (uchar)MathMin(255, (int)((pb >> 16) & 0xFF) * 255 / alpha) : (uchar)((textColor >> 16) & 0xFF);
         uchar oG = (alpha > 0) ? (uchar)MathMin(255, (int)((pb >>  8) & 0xFF) * 255 / alpha) : (uchar)((textColor >>  8) & 0xFF);
         uchar oB = (alpha > 0) ? (uchar)MathMin(255, (int)( pb        & 0xFF) * 255 / alpha) : (uchar)( textColor        & 0xFF);
         //--- Composite reconstructed pixel onto the canvas
         uint stampPixel    = ((uint)(uchar)alpha << 24) | ((uint)oR << 16) | ((uint)oG << 8) | (uint)oB;
         uint existingPixel = targetCanvas.PixelGet(dstX, dstY);
         targetCanvas.PixelSet(dstX, dstY, RcdBlendPixel(existingPixel, stampPixel));
        }
     }
  }

First, we define the "RcdStampText" function to handle all text rendering onto a canvas surface. After an early exit for empty strings, we set the font using TextSetFont and measure the text extent with TextGetSize to size two temporary pixel buffers. The first buffer is filled with pure black, and the second with pure white, then the same text is rendered onto both using TextOut at the same position and color.

The key insight is mathematical: where a glyph pixel is fully opaque, both buffers show the same color. Where it is semi-transparent — as happens on anti-aliased edges — the black buffer shows a darker result, and the white buffer shows a lighter one. The difference between the two reveals exactly how much the background bled through, which directly tells us the true alpha of that pixel. We compute this by averaging the channel differences across red, green, and blue, then subtracting from 255 to get the coverage value.

With the alpha recovered, we reconstruct the true RGB by reversing the pre-multiplication that the black background render introduced — dividing each channel by the alpha fraction and clamping to the valid range. The result is packed into a full ARGB pixel and composited onto the target canvas using "RcdBlendPixel", which applies the Porter-Duff over formula to blend it naturally against whatever is already drawn there. This gives us clean, background-independent text at any position on any surface. With that done, the next thing we will work on is defining the images that we will embed.

Image Scaling, Storage, and Cache Management

Every image embedded in the document needs to be loaded from its resource, scaled to fit the current panel width, and cached so it is not rescaled unnecessarily on every render pass. This group of functions handles that entire pipeline.

//+------------------------------------------------------------------+
//| Scale an image pixel array to new dimensions using bicubic AA    |
//+------------------------------------------------------------------+
void RcdScaleImage(uint &pixelArray[], int origW, int origH, int newW, int newH)
  {
   uint scaledPixels[];
   ArrayResize(scaledPixels, newW * newH);
   //--- Map each destination pixel back to fractional source coordinates
   for(int y = 0; y < newH; y++)
      for(int x = 0; x < newW; x++)
        {
         double ox = (double)x * origW / newW; // Fractional source X
         double oy = (double)y * origH / newH; // Fractional source Y
         scaledPixels[y*newW + x] = RcdBicubicInterp(pixelArray, origW, origH, ox, oy);
        }
   //--- Replace the original pixel array with the scaled result
   ArrayResize(pixelArray, newW * newH);
   ArrayCopy(pixelArray, scaledPixels);
  }

//+------------------------------------------------------------------+
//| Copy pixel data out of an image slot by index                    |
//+------------------------------------------------------------------+
void RcdImgGetPixels(int imgIndex, uint &outputPixels[])
  {
   //--- Copy from the matching flat pixel array based on slot index
   if(imgIndex==0)      ArrayCopy(outputPixels, rcdImgPixels0);
   else if(imgIndex==1) ArrayCopy(outputPixels, rcdImgPixels1);
   else if(imgIndex==2) ArrayCopy(outputPixels, rcdImgPixels2);
   else if(imgIndex==3) ArrayCopy(outputPixels, rcdImgPixels3);
   else if(imgIndex==4) ArrayCopy(outputPixels, rcdImgPixels4);
  }

//+------------------------------------------------------------------+
//| Store pixel data into an image slot by index                     |
//+------------------------------------------------------------------+
void RcdImgSetPixels(int imgIndex, uint &inputPixels[])
  {
   //--- Resize and copy into the matching flat pixel array for the given slot
   if(imgIndex==0)
     { ArrayResize(rcdImgPixels0, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels0, inputPixels); }
   else if(imgIndex==1)
     { ArrayResize(rcdImgPixels1, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels1, inputPixels); }
   else if(imgIndex==2)
     { ArrayResize(rcdImgPixels2, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels2, inputPixels); }
   else if(imgIndex==3)
     { ArrayResize(rcdImgPixels3, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels3, inputPixels); }
   else if(imgIndex==4)
     { ArrayResize(rcdImgPixels4, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels4, inputPixels); }
  }

//+------------------------------------------------------------------+
//| Return the embedded resource path string for an image slot       |
//+------------------------------------------------------------------+
string RcdImgResourcePath(int imgIndex)
  {
   //--- Map slot index to its corresponding #resource path macro
   if(imgIndex==0) return RCD_IMG_MQL5_IDE;
   if(imgIndex==1) return RCD_IMG_MT5_CHART;
   if(imgIndex==2) return RCD_IMG_STRAT_TESTER;
   if(imgIndex==3) return RCD_IMG_MARKET_WATCH;
   if(imgIndex==4) return RCD_IMG_NAVIGATOR;
   return "";
  }

//+------------------------------------------------------------------+
//| Load raw image pixels from embedded resource into a slot         |
//+------------------------------------------------------------------+
bool RcdLoadImage(int imgIndex)
  {
   //--- Validate index bounds
   if(imgIndex < 0 || imgIndex >= RCD_IMG_COUNT) return false;
   uint px[];
   uint ow = 0, oh = 0;
   //--- Read pixels from the embedded resource
   if(!ResourceReadImage(RcdImgResourcePath(imgIndex), px, ow, oh)) return false;
   if(ow == 0 || oh == 0) return false;
   //--- Store original dimensions and pixel data
   rcdImgOrigW[imgIndex] = (int)ow;
   rcdImgOrigH[imgIndex] = (int)oh;
   RcdImgSetPixels(imgIndex, px);
   //--- Mark image as loaded and cache as invalid
   rcdImgLoaded[imgIndex]     = true;
   rcdImgCacheValid[imgIndex] = false;
   return true;
  }

//+------------------------------------------------------------------+
//| Rebuild scaled image cache if panel width has changed            |
//+------------------------------------------------------------------+
void RcdEnsureImageCache(int imgIndex)
  {
   //--- Skip if index invalid or image not yet loaded
   if(imgIndex < 0 || imgIndex >= RCD_IMG_COUNT) return;
   if(!rcdImgLoaded[imgIndex]) return;
   //--- Skip if cache is still valid for the current panel width
   if(rcdImgCacheValid[imgIndex] && rcdImgCacheForWidth[imgIndex] == rcdPanelWidth) return;
   //--- Compute display width fitting inside the text area
   int textAreaW = rcdPanelWidth - RCD_PAD * 2 - RCD_SB_PILL_W - RCD_SB_MARGIN_R * 2;
   int displayW  = textAreaW - 4;
   //--- Clamp display width between 10 and the original image width
   if(displayW > rcdImgOrigW[imgIndex]) displayW = rcdImgOrigW[imgIndex];
   if(displayW < 10) displayW = 10;
   //--- Maintain aspect ratio for display height
   double aspectRatio = (double)rcdImgOrigH[imgIndex] / (double)rcdImgOrigW[imgIndex];
   int displayH = (int)MathRound(displayW * aspectRatio);
   if(displayH < 1) displayH = 1;
   //--- Re-read original pixels from resource and scale them
   uint px[];
   uint ow = 0, oh = 0;
   if(!ResourceReadImage(RcdImgResourcePath(imgIndex), px, ow, oh)) return;
   RcdScaleImage(px, (int)ow, (int)oh, displayW, displayH);
   //--- Store scaled pixel data and update cache state
   RcdImgSetPixels(imgIndex, px);
   rcdImgScaledW[imgIndex]       = displayW;
   rcdImgScaledH[imgIndex]       = displayH;
   rcdImgCacheValid[imgIndex]    = true;
   rcdImgCacheForWidth[imgIndex] = rcdPanelWidth;
  }

The "RcdScaleImage" function resizes a flat pixel array to new dimensions using bicubic interpolation. For every pixel in the destination image, it maps back to fractional source coordinates by multiplying the destination position by the original-to-new dimension ratio, then calls "RcdBicubicInterp" to sample the source at that fractional position. The result fills a temporary scaled array, which is then copied back into the original using ArrayCopy, replacing it in place.

The "RcdImgGetPixels" and "RcdImgSetPixels" functions provide indexed access to the five flat pixel arrays declared globally for each image slot. Since MQL5 does not support arrays of arrays, each slot has its own dedicated array, and these two functions abstract that detail away with a simple slot index. "RcdImgResourcePath" completes the access layer by mapping a slot index back to its embedded resource path string.

Loading is handled by "RcdLoadImage", which validates the index, reads the raw pixels from the embedded resource using ResourceReadImage, stores the original dimensions and pixel data into the appropriate slot, and marks the cache as invalid so the next render pass knows a fresh scale is needed.

The most important function in this group is "RcdEnsureImageCache". It skips immediately if the image is not loaded or if the cache is already valid for the current panel width. Otherwise, it computes the display width by fitting the image inside the available text area, clamps it between a minimum of 10 pixels and the original image width, then derives the display height by maintaining the original aspect ratio. It re-reads the raw pixels fresh from the resource, scales them to the computed dimensions, stores the result, and records the panel width against which the cache was built — so the next call can determine in one comparison whether rescaling is needed again. The next thing is working on the paragraphs, and most importantly, the markup tags.

The Inline Markup System — Parsing, Resolving, and Rendering Styled Text

A plain text label cannot carry bold, italic, colored, or highlighted segments within the same line. To achieve that, we build a lightweight inline markup system that parses tagged strings into styled runs and renders each one independently onto the canvas.

//+------------------------------------------------------------------+
//| Append a paragraph entry to a paragraph array                    |
//+------------------------------------------------------------------+
void RcdAddPara(RcdPara &paraArray[], RcdParaType paraType, const string paraText = "", int imageIndex = -1)
  {
   //--- Extend array by one and populate the new slot
   int currentSize = ArraySize(paraArray);
   ArrayResize(paraArray, currentSize + 1);
   paraArray[currentSize].type   = paraType;
   paraArray[currentSize].text   = paraText;
   paraArray[currentSize].imgIdx = imageIndex;
  }

//+------------------------------------------------------------------+
//| Copy all paragraph entries from source to destination array      |
//+------------------------------------------------------------------+
void RcdCopyParas(const RcdPara &sourceArray[], RcdPara &destArray[])
  {
   int n = ArraySize(sourceArray);
   ArrayResize(destArray, n);
   //--- Copy each field of every paragraph element individually
   for(int i = 0; i < n; i++)
     {
      destArray[i].type   = sourceArray[i].type;
      destArray[i].text   = sourceArray[i].text;
      destArray[i].imgIdx = sourceArray[i].imgIdx;
     }
  }

//+------------------------------------------------------------------+
//| Load the active tab's paragraph array into the output buffer     |
//+------------------------------------------------------------------+
void RcdGetTabParas(int tabIndex, RcdPara &outputArray[])
  {
   //--- Dispatch to the correct per-tab paragraph array
   switch(tabIndex)
     {
      case RCD_TAB_LANGUAGE:   RcdCopyParas(rcdTabParasLanguage,   outputArray); break;
      case RCD_TAB_METAEDITOR: RcdCopyParas(rcdTabParasMetaEditor, outputArray); break;
      case RCD_TAB_PLATFORM:   RcdCopyParas(rcdTabParasPlatform,   outputArray); break;
      case RCD_TAB_TESTER:     RcdCopyParas(rcdTabParasTester,     outputArray); break;
      case RCD_TAB_RESOURCES:  RcdCopyParas(rcdTabParasResources,  outputArray); break;
      default: ArrayResize(outputArray, 0); break;
     }
  }

//+------------------------------------------------------------------+
//| Test whether a text string contains any inline markup tags       |
//+------------------------------------------------------------------+
bool RcdHasMarkup(const string inputText)
  {
   //--- Check for any known opening or closing inline tags
   return (StringFind(inputText, "[b]")  >= 0 || StringFind(inputText, "[u]")  >= 0 ||
           StringFind(inputText, "[i]")  >= 0 || StringFind(inputText, "[s]")  >= 0 ||
           StringFind(inputText, "[/b]") >= 0 || StringFind(inputText, "[/u]") >= 0 ||
           StringFind(inputText, "[/i]") >= 0 || StringFind(inputText, "[/s]") >= 0 ||
           StringFind(inputText, "[/c]") >= 0 || StringFind(inputText, "[/h]") >= 0 ||
           StringFind(inputText, "[c=")  >= 0 || StringFind(inputText, "[h=")  >= 0);
  }

//+------------------------------------------------------------------+
//| Strip all inline markup tags and return plain text               |
//+------------------------------------------------------------------+
string RcdStripInlineTags(const string inputText)
  {
   string result = "";
   int len = StringLen(inputText);
   int i = 0;
   //--- Walk input character by character
   while(i < len)
     {
      if(StringGetCharacter(inputText, i) == '[')
        {
         //--- Find the closing bracket
         int closePos = StringFind(inputText, "]", i);
         //--- Treat unclosed bracket as literal text
         if(closePos < 0) { result += StringSubstr(inputText, i, 1); i++; continue; }
         string tagContent = StringSubstr(inputText, i+1, closePos-i-1);
         //--- Discard recognised markup tags
         if(tagContent == "b"  || tagContent == "/b" || tagContent == "u"  || tagContent == "/u" ||
            tagContent == "i"  || tagContent == "/i" || tagContent == "s"  || tagContent == "/s" ||
            tagContent == "/c" || tagContent == "/h" ||
            StringSubstr(tagContent, 0, 2) == "c=" || StringSubstr(tagContent, 0, 2) == "h=")
           {
            i = closePos + 1;
           }
         else
           {
            //--- Preserve unrecognised bracket sequences as literal text
            result += "[" + tagContent + "]";
            i = closePos + 1;
           }
        }
      else
        {
         //--- Append plain character directly
         result += StringSubstr(inputText, i, 1);
         i++;
        }
     }
   return result;
  }

//+------------------------------------------------------------------+
//| Map a named color token string to its color value                |
//+------------------------------------------------------------------+
color RcdResolveColorToken(const string colorToken)
  {
   //--- Return the theme color matching the token name
   if(colorToken == "accent")  return rcdAccentColor;
   if(colorToken == "heading") return rcdHeadingText;
   if(colorToken == "gold")    return rcdHighlightColor;
   if(colorToken == "red")     return C'220,80,80';
   if(colorToken == "green")   return rcdCheckboxChecked;
   if(colorToken == "dim")     return rcdSubText;
   if(colorToken == "link")    return rcdLinkColor;
   if(colorToken == "warn")    return rcdHighlightColor;
   if(colorToken == "white")   return clrWhite;
   if(colorToken == "black")   return clrBlack;
   //--- Parse hex color literals prefixed with #
   if(StringSubstr(colorToken, 0, 1) == "#")
      return (color)StringToInteger(StringSubstr(colorToken, 1));
   return 0;
  }

//+------------------------------------------------------------------+
//| Copy all run entries from source to destination array            |
//+------------------------------------------------------------------+
void RcdCopyRuns(const RcdRun &sourceRuns[], RcdRun &destRuns[])
  {
   int n = ArraySize(sourceRuns);
   ArrayResize(destRuns, n);
   //--- Copy each field of every run element individually
   for(int i = 0; i < n; i++)
     {
      destRuns[i].text          = sourceRuns[i].text;
      destRuns[i].col           = sourceRuns[i].col;
      destRuns[i].bgCol         = sourceRuns[i].bgCol;
      destRuns[i].bold          = sourceRuns[i].bold;
      destRuns[i].italic        = sourceRuns[i].italic;
      destRuns[i].underline     = sourceRuns[i].underline;
      destRuns[i].strikethrough = sourceRuns[i].strikethrough;
     }
  }

//+------------------------------------------------------------------+
//| Parse inline markup string into a sequence of styled runs        |
//+------------------------------------------------------------------+
void RcdParseRuns(const string inputText, RcdRun &outputRuns[])
  {
   ArrayResize(outputRuns, 0);
   int len = StringLen(inputText);
   if(len == 0) return;
   //--- Track current inline style state
   bool  curBold = false, curItalic = false, curUnderline = false, curStrikethrough = false;
   color curColor = 0, curBgColor = 0;
   string currentSegment = "";
   int i = 0;
   //--- Walk input and emit a run whenever style changes at a tag boundary
   while(i < len)
     {
      if(StringGetCharacter(inputText, i) == '[')
        {
         int closePos = StringFind(inputText, "]", i);
         //--- Treat unclosed bracket as literal character
         if(closePos < 0) { currentSegment += StringSubstr(inputText, i, 1); i++; continue; }
         string tagContent = StringSubstr(inputText, i+1, closePos-i-1);
         //--- Flush accumulated text as a run before applying the tag
         if(StringLen(currentSegment) > 0)
           {
            int sz = ArraySize(outputRuns); ArrayResize(outputRuns, sz+1);
            outputRuns[sz].text          = currentSegment;
            outputRuns[sz].col           = curColor;
            outputRuns[sz].bgCol         = curBgColor;
            outputRuns[sz].bold          = curBold;
            outputRuns[sz].italic        = curItalic;
            outputRuns[sz].underline     = curUnderline;
            outputRuns[sz].strikethrough = curStrikethrough;
            currentSegment = "";
           }
         //--- Apply the recognised tag to the current style state
         if(tagContent == "b")                               curBold = true;
         else if(tagContent == "/b")                         curBold = false;
         else if(tagContent == "i")                          curItalic = true;
         else if(tagContent == "/i")                         curItalic = false;
         else if(tagContent == "u")                          curUnderline = true;
         else if(tagContent == "/u")                         curUnderline = false;
         else if(tagContent == "s")                          curStrikethrough = true;
         else if(tagContent == "/s")                         curStrikethrough = false;
         else if(tagContent == "/c")                         curColor = 0;
         else if(tagContent == "/h")                         curBgColor = 0;
         else if(StringSubstr(tagContent, 0, 2) == "c=")    curColor   = RcdResolveColorToken(StringSubstr(tagContent, 2));
         else if(StringSubstr(tagContent, 0, 2) == "h=")    curBgColor = RcdResolveColorToken(StringSubstr(tagContent, 2));
         else
           {
            //--- Preserve unrecognised bracket sequences verbatim
            currentSegment += "[" + tagContent + "]";
            i = closePos + 1;
            continue;
           }
         i = closePos + 1;
        }
      else
        {
         //--- Accumulate plain character into the current segment
         currentSegment += StringSubstr(inputText, i, 1);
         i++;
        }
     }
   //--- Flush any remaining text as a final run
   if(StringLen(currentSegment) > 0)
     {
      int sz = ArraySize(outputRuns); ArrayResize(outputRuns, sz+1);
      outputRuns[sz].text          = currentSegment;
      outputRuns[sz].col           = curColor;
      outputRuns[sz].bgCol         = curBgColor;
      outputRuns[sz].bold          = curBold;
      outputRuns[sz].italic        = curItalic;
      outputRuns[sz].underline     = curUnderline;
      outputRuns[sz].strikethrough = curStrikethrough;
     }
  }

//+------------------------------------------------------------------+
//| Stamp a sequence of styled runs onto a canvas at a given Y       |
//+------------------------------------------------------------------+
void RcdStampRuns(CCanvas &targetCanvas, int startX, int startY, int lineH,
                  const RcdRun &runs[], color defaultColor, color stampBg, int fontSize)
  {
   int currentX = startX;
   int numRuns  = ArraySize(runs);
   //--- Process each run in sequence
   for(int r = 0; r < numRuns; r++)
     {
      if(StringLen(runs[r].text) == 0) continue;
      //--- Resolve effective text color, falling back to the paragraph default
      color textColor = (runs[r].col != 0) ? runs[r].col : defaultColor;
      //--- Select font variant based on bold/italic flags
      string fontName;
      if(runs[r].bold && runs[r].italic) fontName = "Calibri Bold Italic";
      else if(runs[r].bold)              fontName = "Calibri Bold";
      else if(runs[r].italic)            fontName = "Calibri Italic";
      else                               fontName = "Calibri";
      TextSetFont(fontName, -(fontSize * 10));
      uint runW = 0, runH = 0;
      TextGetSize(runs[r].text, runW, runH);
      int runWidth = (int)runW;
      //--- Detect true glyph top and bottom via a reference render of "H"
      int glyphTop = (int)runH / 4, glyphBot = (int)runH * 3 / 4;
      {
         uint refBuf[];
         int rw = MathMax((int)runW, 8), rh = (int)runH;
         ArrayResize(refBuf, rw * rh);
         ArrayFill(refBuf, 0, rw * rh, 0xFF000000);
         TextOut("H", 0, 0, TA_LEFT | TA_TOP, refBuf, rw, rh, 0xFFFFFFFF, COLOR_FORMAT_ARGB_NORMALIZE);
         //--- Scan downward to find the first row with glyph pixels
         for(int py = 0; py < rh; py++)
           {
            bool found = false;
            for(int px = 0; px < rw; px++)
              {
               if((uchar)((refBuf[py*rw+px] >> 16) & 0xFF) > 60) { glyphTop = py; found = true; break; }
              }
            if(found) break;
           }
         //--- Scan upward to find the last row with glyph pixels
         for(int py = rh-1; py >= 0; py--)
           {
            bool found = false;
            for(int px = 0; px < rw; px++)
              {
               if((uchar)((refBuf[py*rw+px] >> 16) & 0xFF) > 60) { glyphBot = py; found = true; break; }
              }
            if(found) break;
           }
        }
      //--- Draw background highlight rectangle behind the run if requested
      if(runs[r].bgCol != 0 && runWidth > 0)
        {
         int hlTop = startY + glyphTop - 2;
         int hlBot = startY + glyphBot + 4;
         if(hlTop < 0) hlTop = 0;
         if(hlBot >= targetCanvas.Height()) hlBot = targetCanvas.Height() - 1;
         targetCanvas.FillRectangle(currentX, hlTop, currentX + runWidth - 1, hlBot,
                                    ColorToARGB(runs[r].bgCol, 200));
        }
      //--- Determine the background color to pass to the stamp function
      color effectiveBg = (runs[r].bgCol != 0) ? runs[r].bgCol : stampBg;
      //--- Stamp the run text onto the canvas
      RcdStampText(targetCanvas, currentX, startY, runs[r].text, fontName, fontSize, textColor, effectiveBg, true);
      //--- Draw underline decoration below the glyph baseline
      if(runs[r].underline && runWidth > 0)
        {
         int ulY = startY + glyphBot + 2;
         if(ulY < targetCanvas.Height())
            targetCanvas.Line(currentX, ulY, currentX + runWidth - 1, ulY, ColorToARGB(textColor, 255));
        }
      //--- Draw strikethrough decoration through the glyph midpoint
      if(runs[r].strikethrough && runWidth > 0)
        {
         int stY = startY + glyphTop + (glyphBot - glyphTop) * 45 / 100;
         if(stY < targetCanvas.Height())
            targetCanvas.Line(currentX, stY, currentX + runWidth - 1, stY, ColorToARGB(textColor, 255));
        }
      //--- Advance the X cursor by the rendered run width
      currentX += runWidth;
     }
  }

We start with the paragraph management layer. The "RcdAddPara" function appends a single paragraph entry to a given array by resizing it by one and populating the new slot with the type, text, and optional image index. "RcdCopyParas" performs a field-by-field deep copy of a paragraph array, and "RcdGetTabParas" dispatches by tab index to copy the correct per-tab array into a working output buffer.

Before any rendering, "RcdHasMarkup" scans a string for any known opening or closing tag patterns using StringFind, returning true if any are present. This acts as a fast gate — plain text lines skip the markup pipeline entirely. For lines that do contain markup, "RcdStripInlineTags" walks the string character by character, identifies and discards all recognised tag sequences, and returns clean plain text used for width measurement during line wrapping.

Color tokens within tags like "[c=accent]" or "[c=gold]" are resolved by "RcdResolveColorToken", which maps named tokens to their current theme color values and also handles raw hexadecimal color literals prefixed with a hash character.

The core of the markup system is "RcdParseRuns". It walks the input string, maintaining a live style state — bold, italic, underline, strikethrough, text color, and background color. Each time a tag boundary is encountered, the accumulated plain text segment is flushed into a new run entry carrying a snapshot of the current style state, and the tag is applied to update that state. Any unrecognised bracket sequences are preserved verbatim as literal text. Any remaining text after the final tag is flushed as a closing run.

Finally, "RcdStampRuns" iterates through the parsed run array and renders each segment in sequence. For each run, it resolves the effective text color, selects the appropriate font variant based on the bold and italic flags, and measures the run width. To position underline and strikethrough decorations accurately, it renders a reference glyph of the letter "H" onto a temporary buffer and scans it pixel by pixel to find the true top and bottom of the glyph — avoiding font metric inaccuracies. We chose that letter because it gives the true glyph; you can use any letter — it doesn't have to be 'H'. Then, background highlights are drawn as filled rectangles behind the glyph before the text is stamped using "RcdStampText". Underline and strikethrough lines are then drawn at the detected glyph baseline and midpoint, respectively, and the horizontal cursor advances by the rendered run width before moving to the next segment. Next, we will define the extraction helper functions that will help in the entire wrapping process.

Wrapped Line Decoding Utilities

Once paragraphs are wrapped into display lines, each line is stored as an encoded string carrying its type, indent, and content as a compact prefix sequence. These utility functions decode that encoding during rendering, so each line knows exactly how to present itself.

//+------------------------------------------------------------------+
//| Test whether a wrapped line is an image placeholder              |
//+------------------------------------------------------------------+
bool RcdIsImgLine(const string wrappedLine)
  {
   //--- Check for the image line prefix at the start of the string
   return StringSubstr(wrappedLine, 0, StringLen(RCD_IMG_LINE_PREFIX)) == RCD_IMG_LINE_PREFIX;
  }

//+------------------------------------------------------------------+
//| Extract the image index encoded in an image placeholder line     |
//+------------------------------------------------------------------+
int RcdImgLineIndex(const string wrappedLine)
  {
   //--- Strip the prefix then parse the integer before the colon separator
   string s = StringSubstr(wrappedLine, StringLen(RCD_IMG_LINE_PREFIX));
   int colon = StringFind(s, ":");
   return (colon > 0) ? (int)StringToInteger(StringSubstr(s, 0, colon)) : 0;
  }

//+------------------------------------------------------------------+
//| Extract the slot number encoded in an image placeholder line     |
//+------------------------------------------------------------------+
int RcdImgLineSlot(const string wrappedLine)
  {
   //--- Strip the prefix then parse the integer after the colon separator
   string s = StringSubstr(wrappedLine, StringLen(RCD_IMG_LINE_PREFIX));
   int colon = StringFind(s, ":");
   return (colon >= 0) ? (int)StringToInteger(StringSubstr(s, colon+1)) : 0;
  }

//+------------------------------------------------------------------+
//| Test whether a wrapped line is a logo placeholder                |
//+------------------------------------------------------------------+
bool RcdIsLogoLine(const string wrappedLine)
  {
   //--- Check for the logo line prefix at the start of the string
   return StringSubstr(wrappedLine, 0, StringLen(RCD_LOGO_LINE_PREFIX)) == RCD_LOGO_LINE_PREFIX;
  }

//+------------------------------------------------------------------+
//| Extract the slot number encoded in a logo placeholder line       |
//+------------------------------------------------------------------+
int RcdLogoLineSlot(const string wrappedLine)
  {
   //--- Parse integer immediately after the logo prefix
   return (int)StringToInteger(StringSubstr(wrappedLine, StringLen(RCD_LOGO_LINE_PREFIX)));
  }

//+------------------------------------------------------------------+
//| Extract the paragraph type encoded at the start of a line        |
//+------------------------------------------------------------------+
RcdParaType RcdLineType(const string wrappedLine)
  {
   //--- Return body type when the type prefix character 'T' is absent
   if(StringSubstr(wrappedLine, 0, 1) != "T") return RCD_PARA_BODY;
   int colon = StringFind(wrappedLine, ":");
   if(colon < 2) return RCD_PARA_BODY;
   //--- Parse the integer type code between 'T' and the first colon
   return (RcdParaType)(int)StringToInteger(StringSubstr(wrappedLine, 1, colon - 1));
  }

//+------------------------------------------------------------------+
//| Extract the display text payload from a wrapped line string     |
//+------------------------------------------------------------------+
string RcdLineText(const string wrappedLine)
  {
   string s = wrappedLine;
   //--- Strip leading type prefix "TN:" if present
   if(StringSubstr(s, 0, 1) == "T")
     {
      int c = StringFind(s, ":");
      if(c >= 1) s = StringSubstr(s, c+1);
     }
   //--- Strip leading indent prefix "INDENT:N:" if present
   if(StringSubstr(s, 0, 7) == "INDENT:")
     {
      int c = StringFind(s, ":", 7);
      if(c > 7) s = StringSubstr(s, c+1);
     }
   return s;
  }

//+------------------------------------------------------------------+
//| Extract the hanging indent pixel width from a wrapped line       |
//+------------------------------------------------------------------+
int RcdLineIndent(const string wrappedLine)
  {
   string s = wrappedLine;
   //--- Strip leading type prefix if present
   if(StringSubstr(s, 0, 1) == "T")
     {
      int c = StringFind(s, ":");
      if(c >= 1) s = StringSubstr(s, c+1);
     }
   //--- Return zero when no indent prefix is present
   if(StringSubstr(s, 0, 7) != "INDENT:") return 0;
   //--- Parse the pixel indent value between "INDENT:" and the following colon
   int c = StringFind(s, ":", 7);
   return (c > 7) ? (int)StringToInteger(StringSubstr(s, 7, c-7)) : 0;
  }

The "RcdIsImgLine" and "RcdIsLogoLine" functions each check whether a wrapped line string begins with its respective prefix constant, identifying it as an image or logo placeholder rather than a text line. For image placeholders, "RcdImgLineIndex" strips the prefix and parses the integer before the colon separator to recover the image slot index, while "RcdImgLineSlot" parses the integer after the colon to recover the vertical slot number — since a tall image occupies multiple line slots and only the first slot triggers the actual pixel blit. "RcdLogoLineSlot" does the same for logo placeholders.

For text lines, "RcdLineType" checks whether the string begins with the type prefix character "T" and parses the integer between it and the first colon to recover the paragraph type as an "RcdParaType" value, defaulting to body type when the prefix is absent. "RcdLineText" then strips both the type prefix and any following indent prefix to return the clean display text ready for rendering. Finally, "RcdLineIndent" reads the pixel value stored in the indent prefix — used to shift continuation lines of numbered and bullet paragraphs rightward so they align beneath the text rather than the marker, producing proper hanging indentation. The entire wrapping logic is now as follows.

Wrapping Paragraphs into Display Lines

Before any text can be rendered, every paragraph must be broken into lines that fit within the available panel width. The "RcdWrapText" function handles this for the entire active tab content in a single pass, producing the flat array of encoded line strings that the renderer consumes directly.

//+------------------------------------------------------------------+
//| Wrap paragraph array into display lines within pixel width       |
//+------------------------------------------------------------------+
void RcdWrapText(const RcdPara &paraArray[], int maxPixelWidth, string &outputLines[])
  {
   ArrayResize(outputLines, 0);
   int numParas = ArraySize(paraArray);
   //--- Process each paragraph in order
   for(int p = 0; p < numParas; p++)
     {
      //--- Handle image placeholder paragraphs
      if(paraArray[p].type == RCD_PARA_IMG)
        {
         int idx = paraArray[p].imgIdx;
         if(idx >= 0 && idx < RCD_IMG_COUNT && rcdImgLoaded[idx] && rcdLineHeight > 0)
           {
            RcdEnsureImageCache(idx);
            //--- Compute how many line slots the image height occupies
            int totalPx = rcdImgScaledH[idx] + 8;
            int slots = (int)MathCeil((double)totalPx / rcdLineHeight);
            if(slots < 1) slots = 1;
            //--- Emit one placeholder line per slot
            for(int s = 0; s < slots; s++)
              {
               int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
               outputLines[sz] = RCD_IMG_LINE_PREFIX + IntegerToString(idx) + ":" + IntegerToString(s);
              }
           }
         else
           {
            //--- Emit empty line when image is unavailable
            int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
            outputLines[sz] = "";
           }
         continue;
        }
      //--- Handle logo placeholder paragraphs
      if(paraArray[p].type == RCD_PARA_LOGO)
        {
         if(rcdLogoDisplayReady && rcdLineHeight > 0)
           {
            int totalPx = rcdLogoDisplayH + 8;
            int slots = (int)MathCeil((double)totalPx / rcdLineHeight);
            if(slots < 1) slots = 1;
            for(int s = 0; s < slots; s++)
              {
               int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
               outputLines[sz] = RCD_LOGO_LINE_PREFIX + IntegerToString(s);
              }
           }
         else
           {
            int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
            outputLines[sz] = "";
           }
         continue;
        }
      //--- Emit a single blank line for empty paragraphs
      if(paraArray[p].type == RCD_PARA_EMPTY || StringLen(paraArray[p].text) == 0)
        {
         int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
         outputLines[sz] = "";
         continue;
        }

      RcdParaType ptype = paraArray[p].type;
      string ptext      = paraArray[p].text;
      bool isHeading = (ptype == RCD_PARA_HEADING);
      bool isBlock   = (ptype == RCD_PARA_WARN || ptype == RCD_PARA_INFO || ptype == RCD_PARA_ANSWER);
      //--- Select font and size based on paragraph type
      string mFont = isHeading ? "Calibri Bold" : "Calibri";
      int    mSize = isHeading ? RCD_FONT_HEADING : RCD_FONT_BODY;
      TextSetFont(mFont, -(mSize * 10));
      //--- Build the type prefix for stored line strings
      string marker   = "T" + IntegerToString((int)ptype) + ":";
      int    blockPad = isBlock ? 10 : 0;

      //--- Compute hanging indent width for numbered/bullet items
      int hangW = 0;
      if(ptype == RCD_PARA_BULLET)
        {
         uint bW = 0, bH = 0;
         TextGetSize("• ", bW, bH);
         hangW = (int)bW;
        }
      else
        {
         string plainCheck = RcdStripInlineTags(ptext);
         if(StringLen(plainCheck) > 2)
           {
            ushort c0 = StringGetCharacter(plainCheck,0);
            ushort c1 = StringGetCharacter(plainCheck,1);
            ushort c2 = StringGetCharacter(plainCheck,2);
            //--- Detect "X. " numbered list prefix pattern
            if(((c0 >= '1' && c0 <= '9') || (c0 >= 'a' && c0 <= 'z') || (c0 >= 'A' && c0 <= 'Z')) && c1 == '.' && c2 == ' ')
              {
               uint pW = 0, pH = 0;
               TextGetSize(StringSubstr(plainCheck,0,3), pW, pH);
               hangW = (int)pW;
              }
           }
        }

      bool hasMarkup = RcdHasMarkup(ptext);

      if(hasMarkup)
        {
         //--- Markup-aware wrapping: measure plain text widths, preserve original markup in stored lines
         string plain = RcdStripInlineTags(ptext);
         string plainWords[];
         int nw = StringSplit(plain, ' ', plainWords);
         //--- Build origCuts: byte position in ptext where each plain word starts
         int origCuts[];
         ArrayResize(origCuts, nw + 1);
         int pi = 0, li = 0;
         for(int w = 0; w < nw; w++)
           {
            if(w > 0)
              {
               //--- Skip the inter-word space in the plain text
               if(li < StringLen(plain) && StringGetCharacter(plain, li) == ' ') li++;
               //--- Skip any tags and the space between words in the markup source
               bool skippedSpace = false;
               while(pi < StringLen(ptext))
                 {
                  ushort ch = StringGetCharacter(ptext, pi);
                  if(ch == '[')
                    {
                     int cl = StringFind(ptext, "]", pi);
                     if(cl >= 0) { pi = cl+1; continue; }
                    }
                  if(ch == ' ' && !skippedSpace) { pi++; skippedSpace = true; continue; }
                  break;
                 }
               //--- Skip any tags that immediately precede this word
               while(pi < StringLen(ptext) && StringGetCharacter(ptext, pi) == '[')
                 {
                  int cl = StringFind(ptext, "]", pi);
                  if(cl >= 0) pi = cl+1;
                  else break;
                 }
               origCuts[w] = pi;
              }
            else
              {
               origCuts[w] = 0;
               //--- Skip any leading tags before the first word
               while(pi < StringLen(ptext) && StringGetCharacter(ptext, pi) == '[')
                 {
                  int cl = StringFind(ptext, "]", pi);
                  if(cl >= 0) pi = cl+1;
                  else break;
                 }
              }
            //--- Advance both cursors past the word characters
            int wlen = StringLen(plainWords[w]), matched = 0;
            while(pi < StringLen(ptext) && matched < wlen)
              {
               if(StringGetCharacter(ptext, pi) == '[')
                 {
                  int cl = StringFind(ptext,"]",pi);
                  if(cl >= 0) { pi=cl+1; continue; }
                 }
               pi++; li++; matched++;
              }
           }
         origCuts[nw] = StringLen(ptext);

         string stateAtWord[];
         ArrayResize(stateAtWord, nw + 1);
         for(int w2 = 0; w2 <= nw; w2++)
           {
            //--- scanEnd: include all tags whose ] sits at or before this word's start
            int scanEnd = (w2 < nw) ? origCuts[w2] : StringLen(ptext);
            bool stBold=false, stItalic=false, stUnder=false, stStrike=false;
            color stCol=0, stBg=0;
            int spi = 0;
            while(spi < scanEnd)
              {
               if(StringGetCharacter(ptext,spi) == '[')
                 {
                  int cl = StringFind(ptext,"]",spi);
                  //--- Include this tag only if its ] lands at or before scanEnd
                  if(cl >= 0 && cl <= scanEnd)
                    {
                     string tag = StringSubstr(ptext,spi+1,cl-spi-1);
                     if(tag=="b")      stBold=true;
                     else if(tag=="/b") stBold=false;
                     else if(tag=="i")  stItalic=true;
                     else if(tag=="/i") stItalic=false;
                     else if(tag=="u")  stUnder=true;
                     else if(tag=="/u") stUnder=false;
                     else if(tag=="s")  stStrike=true;
                     else if(tag=="/s") stStrike=false;
                     else if(tag=="/c") stCol=0;
                     else if(StringSubstr(tag,0,2)=="c=") stCol=RcdResolveColorToken(StringSubstr(tag,2));
                     else if(tag=="/h") stBg=0;
                     else if(StringSubstr(tag,0,2)=="h=") stBg=RcdResolveColorToken(StringSubstr(tag,2));
                     spi=cl+1;
                     continue;
                    }
                 }
               spi++;
              }
            //--- Serialise active style state as a reopening tag sequence
            string st="";
            if(stBold)   st+="[b]";
            if(stItalic) st+="[i]";
            if(stUnder)  st+="[u]";
            if(stStrike) st+="[s]";
            if(stCol!=0) st+="[c=#"+IntegerToString((int)stCol,6,'0')+"]";
            if(stBg !=0) st+="[h=#"+IntegerToString((int)stBg, 6,'0')+"]";
            stateAtWord[w2] = st;
           }

         //--- Greedy wrap loop: accumulate words into a line until width overflows
         string curPlain      = "";
         int    lineStartW    = 0;
         bool   isFirstLine   = true;
         for(int w = 0; w <= nw; w++)
           {
            bool flush = (w == nw);
            string testPlain = "";
            if(!flush) testPlain = curPlain + (StringLen(curPlain) > 0 ? " " : "") + plainWords[w];
            uint wW = 0, wH = 0;
            if(!flush) TextGetSize(testPlain, wW, wH);
            //--- Subtract block padding and hanging indent from available width
            int effMax = maxPixelWidth - blockPad - 8;
            if(!isFirstLine && hangW > 0) effMax -= hangW;
            //--- Add word to current line if it fits
            if(!flush && (int)wW <= effMax)
              {
               curPlain = testPlain;
              }
            else
              {
               //--- Flush accumulated line to output
               if(StringLen(curPlain) > 0)
                 {
                  int origStart = origCuts[lineStartW];
                  int origEnd   = (w < nw) ? origCuts[w] : StringLen(ptext);
                  //--- Extract the corresponding markup slice from the original text
                  string origSlice = StringSubstr(ptext, origStart, origEnd - origStart);
                  //--- Strip trailing whitespace from the slice
                  while(StringLen(origSlice) > 0 && StringGetCharacter(origSlice, StringLen(origSlice)-1) == ' ')
                     origSlice = StringSubstr(origSlice, 0, StringLen(origSlice)-1);
                  //--- Strip trailing dangling open tags that have no text after them
                  while(StringLen(origSlice) > 0 && StringGetCharacter(origSlice, StringLen(origSlice)-1) == ']')
                    {
                     int openPos = StringLen(origSlice) - 1;
                     while(openPos > 0 && StringGetCharacter(origSlice, openPos) != '[') openPos--;
                     if(openPos >= 0 && StringGetCharacter(origSlice, openPos) == '[')
                       {
                        string trailingTag = StringSubstr(origSlice, openPos+1, StringLen(origSlice)-openPos-2);
                        //--- Remove only opening tags with no following text content
                        bool isOpenTag = (StringLen(trailingTag) > 0 &&
                                          StringGetCharacter(trailingTag, 0) != '/' &&
                                          (StringSubstr(trailingTag,0,2)=="c=" || StringSubstr(trailingTag,0,2)=="h=" ||
                                           trailingTag=="b" || trailingTag=="i" || trailingTag=="u" || trailingTag=="s"));
                        if(isOpenTag) origSlice = StringSubstr(origSlice, 0, openPos);
                        else break;
                       }
                     else break;
                    }
                  //--- Prepend the reopening state tags for continuation lines
                  string prefix = (lineStartW > 0) ? stateAtWord[lineStartW] : "";
                  origSlice = prefix + origSlice;
                  //--- Emit the wrapped line with type marker and optional indent
                  int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
                  if(!isFirstLine && hangW > 0)
                     outputLines[sz] = marker + "INDENT:" + IntegerToString(hangW) + ":" + origSlice;
                  else
                     outputLines[sz] = marker + origSlice;
                  isFirstLine = false;
                 }
               //--- Begin the next line with the current word
               if(!flush) { curPlain = plainWords[w]; lineStartW = w; }
              }
           }
        }
      else
        {
         //--- Plain-text wrapping path: no markup to preserve
         string words[];
         int nw = StringSplit(ptext, ' ', words);
         string cur = "";
         bool isFirstLine = true;
         for(int w = 0; w < nw; w++)
           {
            //--- Build measurement string accounting for any existing indent prefix
            string measureStr;
            if(StringSubstr(cur,0,7)=="INDENT:")
              {
               int c2=StringFind(cur,":",7);
               string tp=(c2>7)?StringSubstr(cur,c2+1):"";
               measureStr=tp+(StringLen(tp)>0?" ":"")+words[w];
              }
            else
              {
               measureStr=cur+(StringLen(cur)>0?" ":"")+words[w];
              }
            uint wW=0,wH=0;
            TextGetSize(measureStr,wW,wH);
            int effMax=maxPixelWidth-blockPad-8;
            if(!isFirstLine&&hangW>0&&StringSubstr(cur,0,7)!="INDENT:") effMax-=hangW;
            //--- Add word to line if it fits within available width
            if((int)wW<=effMax)
              {
               if(StringSubstr(cur,0,7)=="INDENT:")
                 {
                  int c2=StringFind(cur,":",7);
                  string pfx=(c2>7)?StringSubstr(cur,0,c2+1):cur+":";
                  string tp=(c2>7)?StringSubstr(cur,c2+1):"";
                  cur=pfx+tp+(StringLen(tp)>0?" ":"")+words[w];
                 }
               else
                 {
                  cur=cur+(StringLen(cur)>0?" ":"")+words[w];
                 }
              }
            else
              {
               //--- Flush current line and begin a new one
               if(StringLen(cur)>0)
                 {
                  int sz=ArraySize(outputLines);
                  ArrayResize(outputLines,sz+1);
                  outputLines[sz]=marker+cur;
                  isFirstLine=false;
                 }
               //--- Apply hanging indent to continuation lines
               if(!isFirstLine&&hangW>0) cur="INDENT:"+IntegerToString(hangW)+":"+words[w];
               else cur=words[w];
              }
           }
         //--- Flush any remaining text as the final line of this paragraph
         if(StringLen(cur)>0)
           {
            int sz=ArraySize(outputLines);
            ArrayResize(outputLines,sz+1);
            outputLines[sz]=marker+cur;
           }
        }
     }
  }

The function iterates through every paragraph in order. Image and logo placeholders are handled first — their scaled pixel heights are divided by the line height using MathCeil to determine how many line slots they occupy, and one encoded placeholder string is emitted per slot. Empty paragraphs emit a single blank line. For all other paragraph types, the font is selected based on whether the paragraph is a heading or body text, and a type marker prefix is built to tag every output line with its paragraph type for the renderer to decode later.

Hanging indent width is computed next. For bullet paragraphs, the bullet character width is measured directly, and for other paragraphs, the plain text is checked for a numbered list prefix pattern — a single alphanumeric character followed by a period and a space — whose pixel width becomes the hang offset applied to all continuation lines.

The wrapping then splits into two paths based on whether the paragraph contains inline markup. For plain text, words are accumulated greedily using TextGetSize measurements until the line overflows, at which point the current line is flushed and a new one begins with a hanging indent prefix if applicable.

The markup-aware path is more involved. Since width measurement must use plain text while the stored lines must preserve the original markup, two parallel cursors track position simultaneously through both the stripped plain version and the original tagged string. For each word boundary, the byte offset into the original markup source is recorded in a cuts array. Additionally, for every word position, the active style state is serialised into a reopening tag sequence — so if a bold or colored span was open when a line break occurs, the continuation line reopens those tags automatically, ensuring styling carries correctly across wrapped lines. When a line is flushed, the corresponding markup slice is extracted from the original string, trailing whitespace and dangling open tags are stripped, the reopening prefix is prepended, and the encoded line is written to the output array with its type marker and optional indent prefix. We can now define the tab's content that we will be showing. This is critical since we need to be careful with the inline markup; it needs to be shown inline with the content. We added all so that it can serve as an example when you are doing real documentation. We used MetaTrader 5 and MQL5 content for this.

Building the Document Content

With the rendering and wrapping infrastructure in place, we now populate the actual documentation that the system will display — the five tab contents that form the body of the rich content document.

//+------------------------------------------------------------------+
//| Populate all per-tab paragraph arrays with documentation        |
//+------------------------------------------------------------------+
void RcdBuildContent()
  {
   //--- Assign display names to each tab slot
   rcdTabTitles[RCD_TAB_LANGUAGE]   = "MQL5 Language";
   rcdTabTitles[RCD_TAB_METAEDITOR] = "MetaEditor";
   rcdTabTitles[RCD_TAB_PLATFORM]   = "MT5 Platform";
   rcdTabTitles[RCD_TAB_TESTER]     = "Strategy Tester";
   rcdTabTitles[RCD_TAB_RESOURCES]  = "Resources";

   //--- Convenience macros to reduce repetition inside content blocks
   #define P(t,s)  RcdAddPara(arr,t,s)
   #define PE      RcdAddPara(arr,RCD_PARA_EMPTY)
   #define PI(n)   RcdAddPara(arr,RCD_PARA_IMG,"",n)
   #define PLOGO   RcdAddPara(arr,RCD_PARA_LOGO)
   #define PB(s)   RcdAddPara(arr,RCD_PARA_BULLET,s)

   //========== TAB 1: MQL5 LANGUAGE ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]What Is MQL5?[/u]"); PE;
      P(RCD_PARA_BODY,    "[b]MQL5[/b] — [i]MetaQuotes Language 5[/i] — is the [c=accent]native programming language[/c] of MetaTrader 5. Developed by [b][c=heading]MetaQuotes Software[/c][/b], it gives traders and developers tools to build [c=gold]Expert Advisors[/c], [c=gold]indicators[/c], [c=gold]scripts[/c], and [c=gold]libraries[/c] that run directly inside the MT5 terminal."); PE;
      P(RCD_PARA_BODY,    "Unlike general-purpose languages, MQL5 has [b][c=accent]trading primitives built into the language itself[/c][/b]. Tick data, order execution, position management, and historical price access are [i]not libraries you import[/i] — they are first-class citizens of the language."); PE;

      P(RCD_PARA_HEADING, "The Four Program Types"); PE;
      P(RCD_PARA_NUMBERED,"[c=gold]1.[/c] [b]Expert Advisors[/b] — [c=accent]Fully automated trading robots.[/c] Attach to a chart and respond to [b]OnInit[/b], [b]OnTick[/b], [b]OnDeinit[/b], and [b]OnChartEvent[/b]. [i]This entire document — every pixel — is rendered by an EA.[/i]");
      P(RCD_PARA_NUMBERED,"[c=gold]2.[/c] [b]Indicators[/b] — Custom technical analysis drawn on the chart via [c=accent]indicator buffers[/c]. They respond to [b]OnCalculate[/b] and can be called from EAs using [c=link]iCustom()[/c].");
      P(RCD_PARA_NUMBERED,"[c=gold]3.[/c] [b]Scripts[/b] — [i]One-shot programs[/i] that run once and stop. No event loop. Ideal for batch operations and utility tasks.");
      P(RCD_PARA_NUMBERED,"[c=gold]4.[/c] [b][c=teal]Libraries[/c][/b] — Reusable compiled [c=accent].ex5[/c] modules imported via [b]#import[/b]. Cannot run standalone but expose functions to any other MQL5 program."); PE;

      P(RCD_PARA_HEADING, "Core Language Capabilities"); PE;
      PB("[b][c=accent]Object-Oriented:[/c][/b] Full class support — inheritance, polymorphism, encapsulation. The entire standard library is built on OOP.");
      PB("[b][c=gold]Strongly Typed:[/c][/b] Every variable has a declared type. [c=teal]int[/c], [c=teal]double[/c], [c=teal]string[/c], [c=teal]bool[/c], [c=teal]datetime[/c], [c=teal]color[/c], [c=teal]long[/c], [c=teal]ulong[/c], [c=teal]uchar[/c] — each serves a specific purpose.");
      PB("[b][c=purple]Canvas API:[/c][/b] The [c=accent]CCanvas[/c] class provides a pixel-level drawing surface. [i][u]Every heading, every color, every block in this document is CCanvas rendered.[/u][/i]");
      PB("[b][c=orange]Built-in Trading Functions:[/c][/b] [c=link]OrderSend()[/c], [c=link]PositionSelect()[/c], [c=link]HistoryDealSelect()[/c] — trading operations are native language functions."); PE;

      P(RCD_PARA_INFO, "[b]Did you know?[/b] MQL5 compiles to [b][c=green]native machine code[/c][/b] via its own compiler. The resulting [c=accent].ex5[/c] binary runs [b]significantly faster[/b] than interpreted languages — critical for high-frequency strategies processing thousands of ticks per second."); PE;

      P(RCD_PARA_HEADING, "The Event System"); PE;
      P(RCD_PARA_BODY,    "MQL5 programs are [b][i]event-driven[/i][/b]. Your code responds to events the platform fires — [s]not a continuous loop[/s]:"); PE;
      PB("[c=accent]OnInit()[/c] — Fires once on attach. Initialise handles, state, and UI here.");
      PB("[c=accent]OnTick()[/c] — Fires on every new price quote. The [b]heartbeat[/b] of any live EA.");
      PB("[c=accent]OnDeinit()[/c] — Fires on removal or terminal close. [u]Clean up objects and resources.[/u]");
      PB("[c=accent]OnChartEvent()[/c] — Fires on mouse moves, clicks, and keyboard input. [i]All tab switching and scrolling in this document live here.[/i]");
      PB("[c=accent]OnTimer()[/c] — Fires at intervals set by [c=link]EventSetTimer()[/c]. Used for animations and periodic checks.");
      PB("[c=accent]OnTradeTransaction()[/c] — Fires on broker-level trade events. [b][u]The most accurate way[/u][/b] to react to position opens and closes."); PE;

      P(RCD_PARA_HEADING, "Data Types in Depth"); PE;
      P(RCD_PARA_WARN, "[b][c=red]Precision Warning:[/c][/b] Always use [c=accent]double[/c] for prices and call [b]NormalizeDouble(price, _Digits)[/b] before passing values to order functions. [s]Integer arithmetic on prices[/s] causes silent rounding errors that corrupt SL and TP calculations."); PE;
      PB("[b][c=teal]int / long / ulong[/c][/b] — Integer types. [c=accent]ulong[/c] is required for ticket numbers because they can exceed the 32-bit int range on some brokers.");
      PB("[b][c=teal]double[/c][/b] — 64-bit floating point. Used for [i]all[/i] price, volume, and ratio calculations.");
      PB("[b][c=teal]string[/c][/b] — Unicode text. Reference-counted and memory-managed. [c=link]StringLen()[/c], [c=link]StringFind()[/c], [c=link]StringSubstr()[/c] are core tools.");
      PB("[b][c=teal]datetime[/c][/b] — Unix timestamp stored as [c=accent]long[/c] internally. [c=link]TimeCurrent()[/c] returns server time.");
      PB("[b][c=teal]color[/c][/b] — RGB stored as [c=accent]int[/c]. [c=link]ColorToARGB()[/c] adds an alpha channel for canvas rendering. Source format: [h=gold][c=black]C'R,G,B'[/c][/h]."); PE;

      P(RCD_PARA_HEADING, "Arrays and Series"); PE;
      P(RCD_PARA_BODY, "Arrays are declared as [c=accent]type name[][/c] and resized with [c=link]ArrayResize()[/c]. When you set [c=link]ArraySetAsSeries(arr, true)[/c], index [h=accent][c=white]0[/c][/h] is the [b]current bar[/b] and index [h=accent][c=white]1[/c][/h] is the [b]previous completed bar[/b]."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Best Practice:[/c][/b] Always set [c=accent]ArraySetAsSeries(true)[/c] on any price buffer before reading it. [u]Without it, index 0 is the oldest bar[/u] — [b][i][c=red]silently inverting your signal logic.[/c][/i][/b]"); PE;

      P(RCD_PARA_HEADING, "Preprocessor Directives"); PE;
      PB("[b][c=gold]#property[/c][/b] — Sets file metadata: [c=accent]copyright[/c], [c=accent]version[/c], [c=accent]link[/c], [c=accent]description[/c].");
      PB("[b][c=gold]#define[/c][/b] — Compile-time text substitution for named constants that never change.");
      PB("[b][c=gold]#include[/c][/b] — Pastes another file at compile time. Enables modular code using [c=accent].mqh[/c] headers.");
      PB("[b][c=gold]#resource[/c][/b] — [i]Embeds a binary file directly into the compiled[/i] [c=accent].ex5[/c]. [b][u]This document embeds all images this way[/u][/b] — they travel with the file.");
      PB("[b][c=gold]sinput vs input[/c][/b] — [c=accent]sinput[/c] parameters [s]cannot be optimised[/s] in the Strategy Tester. Use it for magic numbers and visual settings."); PE;

      RcdCopyParas(arr, rcdTabParasLanguage);
     }

   //========== TAB 2: METAEDITOR ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]MetaEditor — Your MQL5 IDE[/u]"); PE;
      P(RCD_PARA_BODY, "[b]MetaEditor[/b] is the [c=accent]integrated development environment[/c] that ships with every MetaTrader 5 installation. Press [h=gold][c=black]F4[/c][/h] inside MT5 to open it instantly, or navigate to [c=link]Tools → MetaQuotes Language Editor[/c]."); PE;
      PI(RCD_IMG_MQL5IDE_IDX);
      P(RCD_PARA_HEADING, "The Interface Layout"); PE;
      PB("[b][c=gold]Toolbox — Left Panel:[/c][/b] Three tabs — [c=accent]Navigator[/c] for file browsing, [c=accent]Symbols[/c] for instrument lookup, and [c=accent]Projects[/c] for solution management.");
      PB("[b][c=gold]Editor Area — Center:[/c][/b] The main code canvas. [i]Syntax highlighting, code folding, bracket matching, and multi-tab editing.[/i]");
      PB("[b][c=gold]Toolbox — Bottom Panel:[/c][/b] [c=accent]Errors[/c] shows compile errors with exact line numbers. [c=accent]Journal[/c] shows runtime output from [c=link]Print()[/c]."); PE;

      P(RCD_PARA_HEADING, "Writing Your First EA"); PE;
      P(RCD_PARA_NUMBERED,"[c=gold]1.[/c] Go to [c=accent]File → New[/c] or press [h=gold][c=black]Ctrl+N[/c][/h]. Select [b]Expert Advisor[/b] and click Next.");
      P(RCD_PARA_NUMBERED,"[c=gold]2.[/c] Enter a name. MetaEditor creates the file with [b]OnInit[/b], [b]OnDeinit[/b], and [b]OnTick[/b] pre-generated in [c=accent]MQL5/Experts/[/c].");
      P(RCD_PARA_NUMBERED,"[c=gold]3.[/c] Write your logic. Add [c=accent]input[/c] parameters at the top for user-configurable settings.");
      P(RCD_PARA_NUMBERED,"[c=gold]4.[/c] Press [h=gold][c=black]F7[/c][/h] to compile. [c=green]Warnings[/c] are yellow. [c=red]Errors[/c] are red. Both show exact line numbers.");
      P(RCD_PARA_NUMBERED,"[c=gold]5.[/c] Switch to MT5. Your [c=accent].ex5[/c] appears in the Navigator under Experts. [b][i]Drag it onto any chart to run it.[/i][/b]"); PE;

      P(RCD_PARA_HEADING, "Essential Keyboard Shortcuts"); PE;
      PB("[h=gold][c=black]F7[/c][/h] — Compile. [u]Always compile before testing.[/u]");
      PB("[h=gold][c=black]F5[/c][/h] — Start the debugger and attach to a running EA on a chart.");
      PB("[h=gold][c=black]Ctrl+F[/c][/h] — Find in current file. [c=accent]Ctrl+H[/c] to find and replace.");
      PB("[h=gold][c=black]Ctrl+Shift+F[/c][/h] — Find across [i]all files[/i] in the MQL5 directory.");
      PB("[h=gold][c=black]Ctrl+Space[/c][/h] — IntelliSense autocomplete. Shows function signatures and available methods.");
      PB("[h=gold][c=black]Alt+G[/c][/h] — Go to definition. Jump to where a function or variable is declared."); PE;

      P(RCD_PARA_HEADING, "The Debugger"); PE;
      P(RCD_PARA_BODY, "MetaEditor includes a [b][c=accent]source-level debugger[/c][/b]. Set breakpoints by clicking the line number gutter, press [h=gold][c=black]F5[/c][/h] to attach. Execution pauses — [i]every variable is inspectable live.[/i]"); PE;
      PB("[h=gold][c=black]F10[/c][/h] — Step over. Execute current line and move to next.");
      PB("[h=gold][c=black]F11[/c][/h] — Step into. Enter the body of a function call.");
      PB("[h=gold][c=black]Shift+F11[/c][/h] — Step out. Return from current function to its caller.");
      PB("[c=accent]Locals panel[/c] — See the [b]live value[/b] of every local variable in scope."); PE;

      P(RCD_PARA_INFO, "[b]Performance Matters:[/b] The MetaEditor [c=accent]Profiler[/c] shows [b][u]exactly which functions consume the most CPU[/u][/b] — broken down to individual lines. Use it before any live release to eliminate bottlenecks."); PE;

      P(RCD_PARA_HEADING, "File and Folder Structure"); PE;
      PB("[c=accent]MQL5/Experts/[/c] — All Expert Advisors. Subfolders appear as groups in MT5 Navigator.");
      PB("[c=accent]MQL5/Indicators/[/c] — Custom indicators.");
      PB("[c=accent]MQL5/Scripts/[/c] — One-shot scripts.");
      PB("[c=accent]MQL5/Include/[/c] — Header files [c=teal](.mqh)[/c]. Standard library lives in [c=gold]Include/Trade/[/c] and [c=gold]Include/Canvas/[/c].");
      PB("[c=accent]MQL5/Files/[/c] — Sandbox for file I/O. [c=link]FileOpen()[/c] and [c=link]FileWrite()[/c] are restricted here by default."); PE;

      P(RCD_PARA_WARN, "[b][c=red]Important:[/c][/b] [s]Never edit the .ex5 compiled file directly.[/s] Always edit the [c=accent].mq5[/c] source. The compiled binary is regenerated on every [h=gold][c=black]F7[/c][/h] press. Distributing only the [c=accent].ex5[/c] [b][i]protects your source from copying.[/i][/b]"); PE;

      RcdCopyParas(arr, rcdTabParasMetaEditor);
     }

   //========== TAB 3: MT5 PLATFORM ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]MetaTrader 5 — The Platform[/u]"); PE;
      P(RCD_PARA_BODY, "[b]MetaTrader 5[/b] is a [c=accent]multi-asset trading platform[/c] supporting [c=gold]Forex[/c], [c=gold]stocks[/c], [c=gold]futures[/c], [c=gold]options[/c], and [c=gold]CFDs[/c]. It connects to brokers via an encrypted protocol, executes orders, streams live prices, and [b][i]runs your MQL5 programs simultaneously.[/i][/b]"); PE;
      PI(RCD_IMG_MT5CHART_IDX);
      P(RCD_PARA_HEADING, "The Main Interface"); PE;
      PB("[b][c=gold]Menu Bar:[/c][/b] File, View, Insert, Charts, Tools, Window. [c=accent]Tools → Options[/c] configures the platform globally — chart defaults, notifications, server settings.");
      PB("[b][c=red]Algo Trading Button:[/c][/b] The [h=warn][b]smiley face[/b][/h] in the toolbar enables or disables [u]all EA execution globally[/u]. If it is off, [b][c=red]no EA can trade[/c][/b] regardless of its internal settings.");
      PB("[b][c=gold]Market Watch:[/c][/b] The live price feed. [c=accent]Ctrl+M[/c] toggles it. Right-click to add or remove symbols. [i]Drag a symbol onto a chart to switch it instantly.[/i]"); PE;

      P(RCD_PARA_HEADING, "21 Timeframes Available"); PE;
      PB("[c=teal]M1, M2, M3, M4, M5[/c] — Minute timeframes for scalping strategies.");
      PB("[c=teal]M6, M10, M12, M15, M20, M30[/c] — Sub-hourly timeframes.");
      PB("[c=teal]H1, H2, H3, H4, H6, H8, H12[/c] — Hourly timeframes for intraday strategies.");
      PB("[c=teal]D1, W1, MN1[/c] — Daily, weekly, and monthly for [b][i]position and macro trading.[/i][/b]"); PE;

      P(RCD_PARA_HEADING, "Market Watch"); PE;
      PI(RCD_IMG_MWATCH_IDX);
      P(RCD_PARA_BODY, "The [b]Market Watch[/b] window shows live [c=green]Bid[/c] and [c=red]Ask[/c] prices for every symbol. Right-click any row to open a chart, place an order, or view contract specifications."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Tip:[/c][/b] Right-click the [c=accent]column header[/c] to add [b]High[/b], [b]Low[/b], [b][u]Spread[/u][/b], and [b]Volume[/b] columns. The [h=accent][c=white]spread column[/c][/h] is especially useful — it shows live spread in points and reveals [i]true execution costs[/i] before any trade."); PE;

      P(RCD_PARA_HEADING, "The Navigator"); PE;
      PI(RCD_IMG_NAV_IDX);
      P(RCD_PARA_BODY, "Press [h=gold][c=black]Ctrl+N[/c][/h] to open the [b]Navigator[/b]. It shows all MQL5 programs organised by type: [c=gold]Expert Advisors[/c], [c=gold]Indicators[/c], [c=gold]Scripts[/c], and [c=gold]Libraries[/c]."); PE;
      PB("To attach an EA: [c=accent]double-click[/c] it or [b]drag it onto any chart[/b]. The inputs dialog opens automatically.");
      PB("To run a script: drag to chart. [i]Scripts execute immediately[/i] — they cannot be reconfigured after attachment."); PE;

      P(RCD_PARA_HEADING, "Order Types in MT5"); PE;
      PB("[b][c=green]Market Order:[/c][/b] Execute immediately at current price using [c=link]OrderSend()[/c].");
      PB("[b][c=gold]Limit Order:[/c][/b] Execute only at specified price or better.");
      PB("[b][c=orange]Stop Order:[/c][/b] Becomes a market order when price reaches the trigger level.");
      PB("[b][c=purple]Stop-Limit Order:[/c][/b] Becomes a limit order when stop price is hit. [i]Most precise entry control available.[/i]"); PE;

      P(RCD_PARA_WARN, "[b][c=red]Critical — Netting vs Hedging:[/c][/b] Netting accounts allow [u]only one position per symbol[/u]. A second trade in the same direction [s]increases the existing position[/s] instead of opening a new one. [b]Always confirm your account mode[/b] before writing grid or multi-position EAs."); PE;

      RcdCopyParas(arr, rcdTabParasPlatform);
     }

   //========== TAB 4: STRATEGY TESTER ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]The Strategy Tester[/u]"); PE;
      P(RCD_PARA_BODY, "The [b]Strategy Tester[/b] is MT5's [c=accent]built-in backtesting and optimisation engine[/c]. Press [h=gold][c=black]Ctrl+R[/c][/h] to open it. It simulates your EA on historical price data — from [c=dim]open prices only[/c] all the way to [b][c=green]every real tick recorded by the broker[/c][/b]."); PE;
      PI(RCD_IMG_STTEST_IDX);
      P(RCD_PARA_HEADING, "Testing Modes — Fastest to Most Accurate"); PE;
      PB("[b][c=dim]Open Prices Only:[/c][/b] [i]Fastest mode.[/i] Calls [c=accent]OnTick()[/c] only at bar open. [s]Do not use for intrabar strategies.[/s]");
      PB("[b][c=orange]1 Minute OHLC:[/c][/b] Uses M1 bar OHLC to simulate intrabar price movement. Good for most strategies.");
      PB("[b][c=gold]Every Tick (Simulated):[/c][/b] Generates synthetic ticks within each bar using OHLC. Solid accuracy.");
      PB("[b][c=green]Every Tick (Real Ticks):[/c][/b] [b][u]Most accurate.[/u][/b] Uses actual tick data from the broker. [i]Slowest to run — worth it for final validation.[/i]"); PE;

      P(RCD_PARA_HEADING, "Reading the Report"); PE;
      PB("[b][c=gold]Profit Factor[/c][/b] — Gross profit divided by gross loss. Values above [h=accent][c=white]1.5[/c][/h] suggest a robust strategy.");
      PB("[b][c=gold]Max Drawdown[/c][/b] — [b][u]The single most important risk metric.[/u][/b] A strategy with [h=warn][c=black]80% return but 60% drawdown[/c][/h] is [c=red]unusable[/c] for most traders.");
      PB("[b][c=gold]Sharpe Ratio[/c][/b] — Return per unit of risk. Values above [h=accent][c=white]1.0[/c][/h] are acceptable. Above [h=green][c=white]2.0[/c][/h] is excellent.");
      PB("[b][c=gold]Recovery Factor[/c][/b] — Net profit divided by max drawdown. A value of [h=accent][c=white]3.0[/c][/h] or above suggests reliable recovery."); PE;

      P(RCD_PARA_HEADING, "Optimisation"); PE;
      P(RCD_PARA_NUMBERED,"[c=gold]1.[/c] Check the [c=accent]optimise checkbox[/c] next to each parameter you want to sweep. Set range and step.");
      P(RCD_PARA_NUMBERED,"[c=gold]2.[/c] Choose your [c=accent]optimisation criterion[/c] — Profit Factor, Expected Payoff, Drawdown, or Sharpe Ratio.");
      P(RCD_PARA_NUMBERED,"[c=gold]3.[/c] Use [c=accent]Genetic Algorithm[/c] for large parameter spaces — finds near-optimal results [b]without testing every combination[/b].");
      P(RCD_PARA_NUMBERED,"[c=gold]4.[/c] [b][u]Do not simply pick the highest-profit result.[/u][/b] Check robustness across a range of nearby parameter values."); PE;

      P(RCD_PARA_WARN, "[b][c=red]Overfitting Warning:[/c][/b] A strategy optimised to perfection on historical data [s]often fails on live data[/s]. This is [c=accent]curve fitting[/c]. [b]Always validate[/b] on [h=warn][c=black]out-of-sample data[/c][/h] — a period the optimiser never saw."); PE;

      P(RCD_PARA_HEADING, "Forward Testing and Visual Mode"); PE;
      P(RCD_PARA_INFO, "[b]Forward testing[/b] optimises on the [c=accent]in-sample[/c] portion then tests the best result on the [c=accent]out-of-sample[/c] forward period — [b][i]in the same run[/i][/b]. A strategy that performs well on both is a [c=green]strong candidate[/c] for live deployment."); PE;
      P(RCD_PARA_BODY, "Enable [b]Visual Mode[/b] to watch your EA trade bar by bar on an [c=accent]animated chart[/c]. Use it to verify signal logic and drawdown behaviour before running a full statistical backtest."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Rule:[/c][/b] [u]Never go live without a completed backtest.[/u] And [b][c=accent]never go live after just one backtest[/c][/b] — validate across multiple time periods. [i]The market has seen conditions your backtest data has not.[/i]"); PE;

      RcdCopyParas(arr, rcdTabParasTester);
     }

   //========== TAB 5: RESOURCES ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]MQL5 Resources and Community[/u]"); PE;
      P(RCD_PARA_BODY, "The [b][c=accent]MQL5 ecosystem[/c][/b] is one of the largest and most active algorithmic trading communities in the world. Whether you are writing your [i]first indicator[/i] or engineering a [b][c=purple]production-grade multi-basket EA[/c][/b], these resources give you everything you need."); PE;
      PLOGO;
      P(RCD_PARA_HEADING, "Official Documentation"); PE;
      PB("[b][c=gold]MQL5 Reference:[/c][/b] [c=link]https://www.mql5.com/en/docs[/c] — The [u]complete language reference[/u]. Every built-in function, enum, and constant documented with examples. [b][i]Bookmark it. You will use it daily.[/i][/b]");
      PB("[b][c=gold]MQL5 Articles:[/c][/b] [c=link]https://www.mql5.com/en/articles[/c] — Thousands of in-depth tutorials covering [c=accent]basic EA structure[/c] to [c=purple]neural networks[/c], [c=teal]genetic algorithms[/c], and [c=orange]market microstructure[/c].");
      PB("[b][c=gold]MT5 Help:[/c][/b] Press [h=gold][c=black]F1[/c][/h] inside MetaTrader 5 or MetaEditor on any function name to jump [b]directly to its documentation page[/b]."); PE;

      P(RCD_PARA_HEADING, "MQL5.community — The Central Hub"); PE;
      PB("[b][c=gold]Market:[/c][/b] Buy and sell EAs, indicators, and scripts. [i]The largest MT4/MT5 product marketplace worldwide.[/i]");
      PB("[b][c=gold]Freelance:[/c][/b] Post a job to hire a developer, or offer your own services. [c=green]Escrow-protected payments.[/c]");
      PB("[b][c=gold]Forum:[/c][/b] Ask questions, share code, report bugs. [c=accent]The MQL5 development team actively participates.[/c]");
      PB("[b][c=gold]Signals:[/c][/b] Subscribe to copy trades from professional providers [b][u]directly into your MT5 account[/u][/b].");
      PB("[b][c=gold]VPS:[/c][/b] Rent a MetaQuotes virtual private server to run your EA [c=green]24/7[/c] [s]without keeping your PC on[/s]."); PE;

      P(RCD_PARA_HEADING, "Standard Library — Your Head Start"); PE;
      P(RCD_PARA_INFO, "[b]Never reinvent the wheel.[/b] The [c=accent]MQL5 Standard Library[/c] ships with every MetaEditor installation as [b][i]pre-built, tested classes[/i][/b]:"); PE;
      PB("[b][c=teal]CTrade[/c][/b] ([c=gold]Trade/Trade.mqh[/c]) — Execute market orders, modifications, and closures. Handles slippage, deviation, and magic numbers cleanly.");
      PB("[b][c=teal]CPositionInfo[/c][/b] ([c=gold]Trade/PositionInfo.mqh[/c]) — Query open position properties [u]without manual PositionSelect() calls[/u].");
      PB("[b][c=teal]CCanvas[/c][/b] ([c=gold]Canvas/Canvas.mqh[/c]) — [b][i]Pixel-level bitmap drawing.[/i][/b] [h=accent][c=white]Everything rendered in this document uses CCanvas.[/c][/h] Text, shapes, images — all pixel-perfect.");
      PB("[b][c=teal]CChartObject[/c][/b] ([c=gold]ChartObjects/*.mqh[/c]) — Object-oriented wrappers for chart lines, arrows, labels, and rectangles."); PE;

      P(RCD_PARA_HEADING, "Your Learning Path"); PE;
      P(RCD_PARA_NUMBERED,"[c=gold]1.[/c] [b]Read the language reference[/b] at [c=link]https://www.mql5.com/en/docs/basis[/c]. [u]Before writing any EA.[/u]");
      P(RCD_PARA_NUMBERED,"[c=gold]2.[/c] [b]Build a simple indicator.[/c] Understanding [c=accent]OnCalculate()[/c] and indicator buffers is the foundation.");
      P(RCD_PARA_NUMBERED,"[c=gold]3.[/c] [b]Write a simple EA.[/c] Start with a [i]Moving Average crossover[/i]. Hard-code first. Then refactor into [c=accent]input[/c] parameters.");
      P(RCD_PARA_NUMBERED,"[c=gold]4.[/c] [b]Add proper trade management.[/c] Implement [c=accent]CTrade[/c], add SL/TP validation, handle spread and stop-level checks.");
      P(RCD_PARA_NUMBERED,"[c=gold]5.[/c] [b][u]Backtest and optimise.[/u][/b] Run on [c=green]real ticks[/c]. Validate out-of-sample. [b][c=red]Never go live without this step.[/c][/b]");
      P(RCD_PARA_NUMBERED,"[c=gold]6.[/c] [b]Demo first — always.[/c] Minimum [h=warn][c=black]30 days on demo[/c][/h]. [i]Market conditions change. Your backtest cannot capture everything.[/i]"); PE;

      P(RCD_PARA_HEADING, "Tip — It Does Not Have to Live Inside Your Main File"); PE;
      P(RCD_PARA_INFO, "[b]Something we recommend:[/b] we do not have to put the entire rich content system inside our main [c=accent].mq5[/c] file. MQL5's [b][c=gold]#include[/c][/b] directive means we can move all of it — the canvas variables, rendering functions, paragraph arrays, and documentation content — into a dedicated [c=teal].mqh[/c] header file. Then our main EA simply has one line at the top: [b][c=accent]#include \"Rich Content Manual.mqh\"[/c][/b]. Everything compiles in. The program works exactly the same. Our main file stays clean."); PE;
      P(RCD_PARA_BODY, "We think of it this way — our EA is built to trade. Our indicator is built to analyse. [i]Neither of them should be carrying hundreds of lines of documentation rendering logic inside them.[/i] A dedicated [c=teal].mqh[/c] file takes all of that out. The main program stays focused on what it does. The manual stays in its own place, does its own job, and plugs in wherever we need it."); PE;
      PB("[b][c=gold]Separation of concerns:[/c][/b] trading logic stays in [c=accent].mq5[/c], documentation logic lives in [c=teal].mqh[/c]. Each file does one thing.");
      PB("[b][c=gold]Reusable:[/c][/b] we update the [c=teal].mqh[/c] once and every program that includes it picks up the change on the next compile. No duplication.");
      PB("[b][c=gold]Easy to ship:[/c][/b] we distribute the [c=teal].mqh[/c] alongside our [c=accent].ex5[/c], or compile everything into one self-contained binary via [b]#resource[/b]. Either way it works.");
      PB("[b][c=gold]Keeps things organised:[/c][/b] as our documentation grows, the [c=teal].mqh[/c] grows with it. Our [c=accent].mq5[/c] does not change — it just includes the file."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Our take:[/c][/b] whether we embed the documentation directly or separate it into its own include file, [u]what the reader experiences is identical[/u] — a rich, scrollable, formatted manual inside MetaTrader 5. [b][i]The architecture is a choice we make as developers. The result speaks for itself.[/i][/b]"); PE;
      P(RCD_PARA_HEADING, "[u]A Word From Us[/u]"); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]\"[/c][/b] [b]We were inspired by a recurring gap[/b] — one we kept seeing between [u]what a program does[/u] and [u]what its user actually understands about it[/u]. Documentation tends to live outside the tool. A separate file. An external link. Something the user has to go and find. [i]We wanted to change that.[/i] We wanted the knowledge to live exactly where the program lives — inside MetaTrader 5, always available, never separated from the tool it describes. [b][c=green]\"[/c][/b]"); PE;
      P(RCD_PARA_INFO, "[b][c=accent]\"[/c][/b] [b]The idea was straightforward:[/b] everything you can do in a [c=accent]PDF[/c] or a [c=accent]Word document[/c] — [b]bold headings[/b], [i]italic emphasis[/i], [u]underlined terms[/u], [c=gold]colored text[/c], [h=warn][c=black]highlighted warnings[/c][/h], numbered lists, bullet points, embedded images, and clearly separated sections — we wanted all of that to be possible inside an MQL5 program, rendered directly on the chart. No external file. No separate reader. [c=teal]The same reading experience a user gets opening a polished document[/c], delivered natively through [b]CCanvas[/b] — scrollable, tabbed, and always attached to the program it describes. [i]Not as an afterthought. As a feature.[/i] [b][c=accent]\"[/c][/b]"); PE;
      P(RCD_PARA_INFO, "[b][c=accent]\"[/c][/b] [b]For the content itself, we deliberately chose [c=teal]MQL5 and MT5[/c] as the subject matter[/b] — the language, the platform, the IDE, the Strategy Tester. Not because it is the only content this system can hold, but because it [i]serves as a fitting description of the very environment this runs in[/i]. If you are building an [b]Expert Advisor[/b], the content becomes your strategy logic, your input explanations, your risk controls. If you are building an [b]indicator[/b], it becomes your signal documentation. A [b]script[/b] — your usage instructions. [u]We focused on the backbone[/u]. The structure, the rendering engine, the markup system, the scroll and tab behavior — [c=accent]all of it is ready to be adapted[/c]. Swap the content, keep the framework, and you have a professional inbuilt manual for any MQL5 program you deliver. [b][c=accent]\"[/c][/b]"); PE;
      P(RCD_PARA_HEADING, "What We Set Out to Enable"); PE;
      P(RCD_PARA_NUMBERED, "[c=gold]a.[/c] [b][c=accent]Self-served documentation.[/c][/b] Users get the full picture the moment they attach a program — no external PDF, no video, no forum thread. Everything they need is already there, inside the chart.");
      P(RCD_PARA_NUMBERED, "[c=gold]b.[/c] [b][c=teal]Documentation that ships with the program.[/c][/b] Compiled directly into the [c=accent].ex5[/c] binary via [b]#resource[/b], it cannot be lost, separated, or left behind. The manual and the tool are always together.");
      P(RCD_PARA_NUMBERED, "[c=gold]c.[/c] [b][c=purple]Formatting that carries meaning.[/c][/b] A [h=warn][c=black]warning[/c][/h] looks different from a [h=accent][c=white]tip[/c][/h]. [c=red]Critical notes[/c] are visually distinct from [c=green]best practices[/c]. [u]The formatting itself communicates[/u] — not just the words.");
      P(RCD_PARA_NUMBERED, "[c=gold]d.[/c] [b][c=orange]A consistent standard for any program type.[/c][/b] Whether it is an [b]Expert Advisor[/b], an [b]indicator[/b], a [b]script[/b], or a [b]library[/b] — the same rich content system works for all of them. [i]Every MQL5 program can now explain itself.[/i]"); PE;
      P(RCD_PARA_WARN, "[b][c=red]\"[/c][/b] [b]We believe developers deserve better tools for explaining their work[/b] — and users deserve better access to that explanation. This document is our contribution toward that. It is open, it is native to MQL5, and [u]anyone can build on it[/u]. Take the approach, adapt it, and make your own programs more transparent to the people who use them. [b][c=red]\"[/c][/b]"); PE;
      P(RCD_PARA_HEADING, "About This Document"); PE;
      P(RCD_PARA_BODY, "This [b][c=accent]Rich Content Document[/c][/b] is itself a [b][i]demonstration[/i][/b] of MQL5's rendering capabilities — [u]scrollable, tabbed, formatted documentation[/u] inside your MT5 chart. Every [h=gold][c=black]highlight[/c][/h], every [c=red]colored word[/c], every [s]strikethrough[/s], every [u]underline[/u], every [b][i][c=purple]bold italic colored combination[/c][/i][/b] you see here is drawn [c=accent]pixel by pixel[/c] using [b]CCanvas[/b], [b]TextOut()[/b], and [b]ResourceCreate()[/b]."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Our closing thought:[/c][/b] we built this because we believe [b][c=accent]MQL5 is capable of far more than trading logic[/c][/b]. What you are reading right now is rendered entirely in native MQL5. No external libraries. No outside tools. Just [b]CCanvas[/b], [b]TextOut()[/b], and [b]ResourceCreate()[/b] — and the result is a fully formatted, scrollable document living inside your chart. [i]We hope it changes how you think about what your programs can deliver to the people who use them.[/i] [u]Take it. Adapt it. Make it yours.[/u]"); PE;

      RcdCopyParas(arr, rcdTabParasResources);
     }

   //--- Clean up convenience macros
   #undef P
   #undef PE
   #undef PI
   #undef PLOGO
   #undef PB

   //--- Mark content as built so it is not rebuilt on subsequent shows
   rcdContentBuilt = true;
  }

The "RcdBuildContent" function assigns display names to each of the five tab slots, then builds each tab's paragraph array in its own scoped block. To keep the content authoring clean and readable, five convenience macros are defined locally — "P" for adding a typed paragraph, "PE" for an empty spacer, "PI" for an image placeholder, "PLOGO" for the logo placeholder, and "PB" for bullet items — all wrapping calls to "RcdAddPara". Once each tab block finishes building its local array, it is copied into the corresponding global per-tab array using "RcdCopyParas", and the macros are undefined at the end to avoid polluting the global scope.

The first tab covers the MetaQuotes Language 5 (MQL5) language itself — what it is, its four program types, core capabilities, the event system, data types, arrays, and preprocessor directives. The second covers MetaEditor, including the interface layout, writing and compiling a first program, keyboard shortcuts, the debugger, and the file structure. The third walks through the MetaTrader 5 (MetaTrader 5) platform — the main interface, available timeframes, the Market Watch window, the Navigator, and order types. The fourth covers the Strategy Tester, explaining testing modes from fastest to most accurate, how to read the report metrics, optimisation workflow, overfitting risks, and forward testing. The fifth and final tab is a resources and community guide covering official documentation links, the MQL5 community hub, the standard library, a recommended learning path, and a closing section on separating documentation into a dedicated header file for cleaner program architecture.

Throughout all five tabs, the inline markup system is used extensively. Bold, italic, underlined, colored, and highlighted text segments are embedded directly in the paragraph strings using the bracket tag syntax. This gives the finished document the visual richness of a formatted PDF or word-processed document while being authored entirely as plain tagged strings. We use this approach because it matches how markup works. You can define different tags if you prefer. That being said, our content rendering logic is now done. We will define the logic to create the holder where it is rendered and the images embedded in the content. We start with the logo.

Logo Loading and Scaling

The logo appears in two places — as a small icon in the header bar and as a larger display image in the Resources tab body. Each version is handled by its own dedicated function.

//+------------------------------------------------------------------+
//| Load and scale the header logo from the embedded resource        |
//+------------------------------------------------------------------+
bool RcdLoadLogo()
  {
   uint px[];
   uint ow = 0, oh = 0;
   //--- Read raw pixels from the embedded logo resource
   if(!ResourceReadImage(RCD_LOGO_RESOURCE, px, ow, oh)) return false;
   if(ow == 0 || oh == 0) return false;
   //--- Scale to the fixed header logo size
   RcdScaleImage(px, (int)ow, (int)oh, RCD_LOGO_SIZE, RCD_LOGO_SIZE);
   //--- Store as a named in-memory resource for fast header rendering
   return ResourceCreate(rcdLogoScaledResName, px, RCD_LOGO_SIZE, RCD_LOGO_SIZE, 0, 0, RCD_LOGO_SIZE, COLOR_FORMAT_ARGB_NORMALIZE);
  }

//+------------------------------------------------------------------+
//| Load and scale the large body-display logo for Resources tab     |
//+------------------------------------------------------------------+
void RcdLoadLogoDisplay()
  {
   uint px[];
   uint ow = 0, oh = 0;
   //--- Read raw pixels from the embedded logo resource
   if(!ResourceReadImage(RCD_LOGO_RESOURCE, px, ow, oh)) return;
   if(ow == 0 || oh == 0) return;
   int displaySize = 96;
   int dispW, dispH;
   //--- Maintain aspect ratio when scaling to display size
   if((int)ow >= (int)oh)
     {
      dispW = displaySize;
      dispH = (int)MathRound((double)oh / ow * displaySize);
     }
   else
     {
      dispH = displaySize;
      dispW = (int)MathRound((double)ow / oh * displaySize);
     }
   if(dispW < 1) dispW = 1;
   if(dispH < 1) dispH = 1;
   //--- Scale logo pixels to the computed display dimensions
   RcdScaleImage(px, (int)ow, (int)oh, dispW, dispH);
   //--- Store scaled pixel data and mark as ready
   ArrayResize(rcdLogoDisplayPixels, dispW * dispH);
   ArrayCopy(rcdLogoDisplayPixels, px);
   rcdLogoDisplayW     = dispW;
   rcdLogoDisplayH     = dispH;
   rcdLogoDisplayReady = true;
  }

Here, the "RcdLoadLogo" function handles the header version. It reads the raw pixels from the embedded logo resource using ResourceReadImage, then scales them to the fixed header size using "RcdScaleImage". Rather than blitting the pixels directly during every header render, the scaled result is stored as a named in-memory resource using ResourceCreate, which allows the header renderer to read it back quickly without rescaling on each frame. The function returns true or false depending on whether the resource was created successfully.

"RcdLoadLogoDisplay" handles the larger body version shown in the Resources tab. It reads the same embedded logo but scales it to a 96-pixel display size while preserving the original aspect ratio — if the image is wider than it is tall, the width is set to 96, and the height is derived proportionally, and vice versa, with both dimensions clamped to a minimum of 1 pixel to avoid zero-size errors. The scaled pixels are copied into the global display pixel array, and the display dimensions and a ready flag are stored so the body renderer knows the logo is available and exactly how much space it occupies in the line layout. Next, we create the layout.

Panel Layout, Canvas Creation, and Destruction

Before anything can be rendered, the panel geometry must be calculated from the current chart dimensions and the drawing surfaces must be created at the correct positions and sizes.

//+------------------------------------------------------------------+
//| Compute and cache panel geometry from current chart dimensions   |
//+------------------------------------------------------------------+
void RcdCalculateLayout()
  {
   //--- Read chart pixel dimensions with safe fallbacks
   long chartW = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   long chartH = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   if(chartW < 200) chartW = 800;
   if(chartH < 200) chartH = 600;
   //--- Compute panel width clamped between min and max values
   int targetW = (int)(chartW - RCD_SIDE_MARGIN * 2);
   rcdPanelWidth = MathMin(RCD_MAX_WIDTH, MathMax(RCD_MIN_WIDTH, targetW));
   if(rcdPanelWidth > (int)chartW - 20)
      rcdPanelWidth = MathMax(RCD_MIN_WIDTH, (int)chartW - 20);
   //--- Compute body height from available vertical space minus chrome
   int chromeH    = RCD_HEADER_H + RCD_GAP + RCD_TABS_H + RCD_GAP + RCD_GAP + RCD_FOOTER_H;
   int availBodyH = (int)(chartH - RCD_TOP_MARGIN - RCD_BOTTOM_MARGIN - chromeH);
   rcdBodyHeight  = MathMin(RCD_MAX_BODY_H, MathMax(RCD_MIN_BODY_H, availBodyH));
   //--- Compute total panel height as the sum of all sections
   rcdTotalHeight = RCD_HEADER_H + RCD_GAP + RCD_TABS_H + RCD_GAP + rcdBodyHeight + RCD_GAP + RCD_FOOTER_H;
   //--- Centre panel horizontally within the chart
   rcdPanelX = MathMax(RCD_SIDE_MARGIN, (int)((chartW - rcdPanelWidth) / 2));
   rcdPanelY = RCD_TOP_MARGIN;
   //--- Clamp panel Y so it does not overflow the bottom margin
   if(rcdPanelY + rcdTotalHeight > (int)chartH - RCD_BOTTOM_MARGIN)
      rcdPanelY = MathMax(RCD_TOP_MARGIN, (int)chartH - rcdTotalHeight - RCD_BOTTOM_MARGIN);
   //--- Compute absolute Y coordinates for each panel section
   rcdHeaderY = rcdPanelY;
   rcdTabsY   = rcdHeaderY + RCD_HEADER_H + RCD_GAP;
   rcdBodyY   = rcdTabsY   + RCD_TABS_H   + RCD_GAP;
   rcdFooterY = rcdBodyY   + rcdBodyHeight + RCD_GAP;
   //--- Invalidate image caches when the panel width has changed
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++)
      if(rcdImgCacheForWidth[ii] != rcdPanelWidth)
         rcdImgCacheValid[ii] = false;
  }

//+------------------------------------------------------------------+
//| Create all six canvas bitmap label objects                       |
//+------------------------------------------------------------------+
bool RcdCreateCanvases()
  {
   //--- Create header canvas at the computed position and size
   if(!rcdCanvHeader.CreateBitmapLabel(0, 0, rcdHeaderCanvasName,  rcdPanelX, rcdHeaderY, rcdPanelWidth, RCD_HEADER_H,  COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create tabs canvas directly below the header
   if(!rcdCanvTabs.CreateBitmapLabel(0, 0, rcdTabsCanvasName,      rcdPanelX, rcdTabsY,   rcdPanelWidth, RCD_TABS_H,    COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create body canvas for background fill
   if(!rcdCanvBody.CreateBitmapLabel(0, 0, rcdBodyCanvasName,      rcdPanelX, rcdBodyY,   rcdPanelWidth, rcdBodyHeight, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create internal high-resolution body canvas for supersampling
   if(!rcdCanvBodyHR.Create(rcdBodyHRCanvasName, rcdPanelWidth * RCD_SS, rcdBodyHeight * RCD_SS, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create block overlay canvas for text, images, and scrollbar
   if(!rcdCanvBlock.CreateBitmapLabel(0, 0, rcdBlockCanvasName,    rcdPanelX, rcdBodyY,   rcdPanelWidth, rcdBodyHeight, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create footer canvas at the bottom of the panel
   if(!rcdCanvFooter.CreateBitmapLabel(0, 0, rcdFooterCanvasName,  rcdPanelX, rcdFooterY, rcdPanelWidth, RCD_FOOTER_H,  COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy all canvases and remove their chart objects              |
//+------------------------------------------------------------------+
void RcdDestroyCanvases()
  {
   //--- Destroy each canvas and delete its associated chart object
   rcdCanvHeader.Destroy(); ObjectDelete(0, rcdHeaderCanvasName);
   rcdCanvTabs.Destroy();   ObjectDelete(0, rcdTabsCanvasName);
   rcdCanvBody.Destroy();   ObjectDelete(0, rcdBodyCanvasName);
   rcdCanvBodyHR.Destroy();
   rcdCanvBlock.Destroy();  ObjectDelete(0, rcdBlockCanvasName);
   rcdCanvFooter.Destroy(); ObjectDelete(0, rcdFooterCanvasName);
  }

The "RcdCalculateLayout" function reads the current chart width and height using ChartGetInteger, applying safe fallback values for unusually small charts. The panel width is computed by subtracting the side margins from the chart width, then clamped between the defined minimum and maximum width constants, with an additional check ensuring it never exceeds the chart width minus a small safety margin. The body height is derived from the remaining vertical space after subtracting the top and bottom margins plus the combined heights of the header, tabs bar, and footer — again clamped between its minimum and maximum bounds. The total panel height is then the sum of all sections. The panel is centered horizontally, placed at the top margin vertically, and its bottom position is checked to ensure it does not overflow the bottom margin. From the final panel position, the absolute vertical coordinates for the header, tabs, body, and footer are computed in sequence. Finally, all image caches are invalidated if the panel width has changed since they were last built, triggering a rescale on the next render pass.

"RcdCreateCanvases" creates all six drawing surfaces at the positions computed by the layout function. The header, tabs, body background, block overlay, and footer canvases are each created as bitmap label chart objects using CreateBitmapLabel, which makes them visible on the chart at their specified coordinates. The internal high-resolution body canvas is created differently — using Create without a chart object — since it exists purely as an offscreen supersampling buffer that is never displayed directly. If any canvas creation fails, the function returns false immediately.

"RcdDestroyCanvases" mirrors this by calling Destroy on each canvas instance and deleting its associated chart object with ObjectDelete, cleanly removing all visual elements and freeing the underlying pixel memory. Now, when the active tab changes, we need to recompute the content display for changes to take effect per the current layout.

Rebuilding the Wrapped Line Array

Whenever the active tab changes, the panel resizes, or the document is first shown, the entire wrapped line array must be recomputed to match the current layout dimensions and content.

//+------------------------------------------------------------------+
//| Recompute wrapped line array for the active tab content          |
//+------------------------------------------------------------------+
void RcdRebuildWrappedLines()
  {
   //--- Compute available text width excluding scrollbar and padding
   int textAreaW = rcdPanelWidth - RCD_PAD * 2 - RCD_SB_PILL_W - RCD_SB_MARGIN_R * 2;
   //--- Measure line height from a single reference character
   TextSetFont("Calibri", -(RCD_FONT_BODY * 10));
   uint tw = 0, th = 0;
   TextGetSize("x", tw, th);
   rcdLineHeight = (int)(th * 0.65) + RCD_LINE_GAP;
   //--- Ensure all loaded image caches are current for this panel width
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++)
      if(rcdImgLoaded[ii]) RcdEnsureImageCache(ii);
   //--- Load the active tab's paragraph array and wrap it into display lines
   RcdGetTabParas(rcdActiveTab, rcdCurrentParas);
   RcdWrapText(rcdCurrentParas, textAreaW, rcdWrappedLines);
   //--- Compute total content height and scroll bounds
   int numLines          = ArraySize(rcdWrappedLines);
   rcdTotalContentHeight = numLines * rcdLineHeight + RCD_TOP_PAD_BODY * 2;
   rcdScrollVisible      = rcdTotalContentHeight > rcdBodyHeight;
   rcdMaxScroll          = MathMax(0, rcdTotalContentHeight - rcdBodyHeight);
   rcdScrollPos          = MathMax(0, MathMin(rcdScrollPos, rcdMaxScroll));
   //--- Compute scrollbar pill height when scroll is visible
   if(rcdScrollVisible)
     {
      int pillMargin  = 4;
      int trackH      = rcdBodyHeight - pillMargin * 2;
      rcdSliderHeight = RcdCalcSliderHeight(rcdBodyHeight, rcdTotalContentHeight, trackH, 24);
     }
  }

The "RcdRebuildWrappedLines" function begins by computing the available text width — subtracting the left and right padding plus the scrollbar pill width and its margins from the total panel width. The line height is then measured by rendering a reference character using TextGetSize at the body font size, scaling the result to 65% to strip excess internal font leading, and adding the defined line gap constant. This produces a tighter, more visually consistent line spacing than the raw font metrics would give.

Before wrapping, all loaded image caches are verified and refreshed where needed. The active tab's paragraph array is then loaded into the working buffer and passed to "RcdWrapText" along with the computed text width, producing the flat encoded line array that the renderer will consume.

With the wrapped lines available, the total content height is calculated by multiplying the line count by the line height and adding top padding on both ends. This is compared against the visible body height to determine whether the scrollbar needs to be shown and to compute the maximum scroll position. The current scroll offset is clamped to the new maximum to prevent it from sitting beyond the content after a tab switch or resize. Finally, if scrolling is needed, the scrollbar pill height is computed using "RcdCalcSliderHeight", which scales the pill proportionally to the ratio of visible to total content height while enforcing a minimum pill size of 24 pixels for comfortable interaction. Now we can build the layout. We will show how to render the body since the same logic is used for the other parts like the header, tabs, and footer.

//+------------------------------------------------------------------+
//| Render body background, block highlights, text, and scrollbar    |
//+------------------------------------------------------------------+
void RcdRenderBody()
  {
   //--- Fill the HR canvas with the background color
   rcdCanvBodyHR.Erase(0);
   uint bgArgb = ColorToARGB(rcdBg, 255);
   rcdCanvBodyHR.FillRectangle(0, 0, rcdPanelWidth*RCD_SS-1, rcdBodyHeight*RCD_SS-1, bgArgb);
   //--- Downsample to the display body canvas and add border lines
   RcdDownsampleCanvas(rcdCanvBody, rcdCanvBodyHR);
   uint borderArgb = ColorToARGB(rcdBorder, 255);
   rcdCanvBody.Line(0, 0, 0, rcdBodyHeight-1, borderArgb);
   rcdCanvBody.Line(rcdPanelWidth-1, 0, rcdPanelWidth-1, rcdBodyHeight-1, borderArgb);
   rcdCanvBody.Update();
   //--- Clear the block overlay canvas
   rcdCanvBlock.Erase(0x00000000);
   //--- Detect dark theme for block color selection
   bool isDark = (rcdBg == C'20,24,34');

   //--- Draw colored background panels behind WARN / INFO / ANSWER blocks
   {
      int numLines = ArraySize(rcdWrappedLines);
      int topPad   = RCD_TOP_PAD_BODY;
      int blockX   = RCD_PAD - 3;
      int blockW   = rcdPanelWidth - blockX - RCD_SB_PILL_W - RCD_SB_MARGIN_R - 2;
      for(int i = 0; i < numLines; )
        {
         string ln = rcdWrappedLines[i];
         if(RcdIsImgLine(ln) || RcdIsLogoLine(ln)) { i++; continue; }
         if(StringLen(ln) == 0) { i++; continue; }
         RcdParaType lineType = RcdLineType(ln);
         bool isWarn   = (lineType == RCD_PARA_WARN);
         bool isInfo   = (lineType == RCD_PARA_INFO);
         bool isAnswer = (lineType == RCD_PARA_ANSWER);
         if(!isWarn && !isInfo && !isAnswer) { i++; continue; }
         //--- Find the last line that belongs to this block run
         int blockStart = i, blockEnd = i;
         for(int j = i+1; j < numLines; j++)
           {
            if(RcdIsImgLine(rcdWrappedLines[j]) || RcdIsLogoLine(rcdWrappedLines[j])) break;
            if(StringLen(rcdWrappedLines[j]) == 0)
              {
               //--- Look ahead to see if the block continues after a blank line
               bool cont = false;
               for(int k = j+1; k < numLines && k <= j+2; k++)
                 {
                  if(RcdIsImgLine(rcdWrappedLines[k]) || RcdIsLogoLine(rcdWrappedLines[k])) break;
                  RcdParaType tk = RcdLineType(rcdWrappedLines[k]);
                  if((isWarn&&tk==RCD_PARA_WARN)||(isInfo&&tk==RCD_PARA_INFO)||(isAnswer&&tk==RCD_PARA_ANSWER)) { cont=true; break; }
                  if(StringLen(rcdWrappedLines[k]) > 0) break;
                 }
               if(cont) blockEnd = j; else break;
              }
            else
              {
               RcdParaType tj = RcdLineType(rcdWrappedLines[j]);
               if((isWarn&&tj==RCD_PARA_WARN)||(isInfo&&tj==RCD_PARA_INFO)||(isAnswer&&tj==RCD_PARA_ANSWER))
                  blockEnd = j;
               else break;
              }
           }
         //--- Compute visible top/bottom pixel coordinates for this block
         int dtop = MathMax(0, topPad + blockStart * rcdLineHeight - rcdScrollPos - 3);
         int dbot = MathMin(rcdBodyHeight-1, topPad + (blockEnd+1) * rcdLineHeight - rcdScrollPos + 3);
         if(dbot > dtop)
           {
            //--- Choose fill and accent bar colors by block type and theme
            color bgC  = isWarn   ? (isDark ? C'75,18,18'  : C'255,210,210') :
                         isAnswer ? (isDark ? C'14,52,18'  : C'200,240,210') :
                                    (isDark ? C'16,33,68'  : C'210,225,250');
            color barC = isWarn   ? (isDark ? C'195,45,45' : C'175,28,28')   :
                         isAnswer ? (isDark ? C'48,172,75' : C'28,136,58')   :
                                    (isDark ? C'52,120,240': C'28,88,200');
            //--- Fill block background and the left-side accent bar
            rcdCanvBlock.FillRectangle(blockX, dtop, blockX+blockW-1, dbot, ColorToARGB(bgC, 255));
            rcdCanvBlock.FillRectangle(blockX, dtop, blockX+2,        dbot, ColorToARGB(barC, 255));
           }
         i = blockEnd + 1;
        }
     }

   //--- Draw all text lines and inline images onto the block canvas
   int textX    = RCD_PAD;
   int numLines = ArraySize(rcdWrappedLines);
   int topPad   = RCD_TOP_PAD_BODY;
   //--- Track which images have already been drawn this pass
   bool imgDrawnFlags[RCD_IMG_COUNT];
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++) imgDrawnFlags[ii] = false;
   for(int i = 0; i < numLines; i++)
     {
      string ln    = rcdWrappedLines[i];
      int    lineY = topPad + i * rcdLineHeight - rcdScrollPos;
      //--- Handle image placeholder lines
      if(RcdIsImgLine(ln))
        {
         int imgIdx = RcdImgLineIndex(ln);
         int slot   = RcdImgLineSlot(ln);
         //--- Draw image only once, on slot 0, when cache is valid
         if(slot == 0 && imgIdx >= 0 && imgIdx < RCD_IMG_COUNT &&
            rcdImgLoaded[imgIdx] && rcdImgCacheValid[imgIdx] && !imgDrawnFlags[imgIdx])
           {
            imgDrawnFlags[imgIdx] = true;
            int imgY  = lineY + 4;
            int textW = rcdPanelWidth - RCD_PAD * 2 - RCD_SB_PILL_W - RCD_SB_MARGIN_R * 2;
            //--- Centre image horizontally within the text area
            int imgX  = RCD_PAD + (textW - rcdImgScaledW[imgIdx]) / 2;
            if(imgX < 0) imgX = 0;
            uint px[];
            RcdImgGetPixels(imgIdx, px);
            //--- Blit each pixel of the scaled image onto the block canvas
            for(int py = 0; py < rcdImgScaledH[imgIdx]; py++)
              {
               int dstY = imgY + py;
               if(dstY < 0) continue;
               if(dstY >= rcdBodyHeight) break;
               for(int px2 = 0; px2 < rcdImgScaledW[imgIdx]; px2++)
                 {
                  int dstX = imgX + px2;
                  if(dstX < 0 || dstX >= rcdPanelWidth) continue;
                  uint srcPx = px[py * rcdImgScaledW[imgIdx] + px2];
                  uchar sa, sr, sg, sb;
                  RcdArgbSplit(srcPx, sa, sr, sg, sb);
                  if(sa == 0) continue;
                  uint ex = rcdCanvBlock.PixelGet(dstX, dstY);
                  rcdCanvBlock.PixelSet(dstX, dstY, RcdBlendPixel(ex, srcPx));
                 }
              }
           }
         continue;
        }
      //--- Handle logo placeholder lines
      if(RcdIsLogoLine(ln))
        {
         int slot = RcdLogoLineSlot(ln);
         //--- Draw logo only once, on slot 0, when display data is ready
         if(slot == 0 && rcdLogoDisplayReady)
           {
            int imgY  = lineY + 4;
            int textW = rcdPanelWidth - RCD_PAD * 2 - RCD_SB_PILL_W - RCD_SB_MARGIN_R * 2;
            int imgX  = RCD_PAD + (textW - rcdLogoDisplayW) / 2;
            if(imgX < 0) imgX = 0;
            for(int py = 0; py < rcdLogoDisplayH; py++)
              {
               int dstY = imgY + py;
               if(dstY < 0) continue;
               if(dstY >= rcdBodyHeight) break;
               for(int px2 = 0; px2 < rcdLogoDisplayW; px2++)
                 {
                  int dstX = imgX + px2;
                  if(dstX < 0 || dstX >= rcdPanelWidth) continue;
                  uint srcPx = rcdLogoDisplayPixels[py * rcdLogoDisplayW + px2];
                  uchar sa, sr, sg, sb;
                  RcdArgbSplit(srcPx, sa, sr, sg, sb);
                  if(sa == 0) continue;
                  uint ex = rcdCanvBlock.PixelGet(dstX, dstY);
                  rcdCanvBlock.PixelSet(dstX, dstY, RcdBlendPixel(ex, srcPx));
                 }
              }
           }
         continue;
        }
      //--- Skip lines scrolled above or below the visible area
      if(lineY + rcdLineHeight < 0) continue;
      if(lineY >= rcdBodyHeight) break;
      if(StringLen(ln) == 0) continue;
      //--- Compute horizontal text position including hanging indent
      int lineTextX = textX;
      int indentPx  = RcdLineIndent(ln);
      if(indentPx > 0) lineTextX = textX + indentPx;
      //--- Decode paragraph type and display text from wrapped line
      RcdParaType ptype   = RcdLineType(ln);
      string renderText   = RcdLineText(ln);
      bool isBlock        = (ptype == RCD_PARA_WARN || ptype == RCD_PARA_INFO || ptype == RCD_PARA_ANSWER);
      //--- Add extra indent inside block paragraphs for the accent bar
      if(isBlock) lineTextX = textX + 10 + indentPx;
      //--- Measure bullet prefix width and shift text right for first line
      if(ptype == RCD_PARA_BULLET && indentPx == 0)
        {
         TextSetFont("Calibri", -(RCD_FONT_BODY * 10));
         uint bW = 0, bH = 0;
         TextGetSize("• ", bW, bH);
         lineTextX = textX + (int)bW;
        }
      //--- Select the default text color for this paragraph type
      color defaultTextColor;
      switch(ptype)
        {
         case RCD_PARA_HEADING:  defaultTextColor = rcdHeadingText;    break;
         case RCD_PARA_NUMBERED: defaultTextColor = rcdHighlightColor; break;
         default:                defaultTextColor = rcdBodyText;       break;
        }
      //--- Select font face and size based on paragraph type
      string fontName = (ptype == RCD_PARA_HEADING) ? "Calibri Bold" : "Calibri";
      int    fontSize = (ptype == RCD_PARA_HEADING) ? RCD_FONT_HEADING : RCD_FONT_BODY;
      //--- Determine the effective background color for text stamping
      color stampBg;
      if(isBlock)
         stampBg = (ptype == RCD_PARA_WARN)   ? (isDark ? C'75,18,18'  : C'255,210,210') :
                   (ptype == RCD_PARA_ANSWER)  ? (isDark ? C'14,52,18'  : C'200,240,210') :
                                                 (isDark ? C'16,33,68'  : C'210,225,250');
      else
         stampBg = rcdBg;
      //--- Stamp line content using markup-aware or plain path
      if(RcdHasMarkup(renderText))
        {
         RcdRun runs[];
         RcdParseRuns(renderText, runs);
         //--- Stamp the bullet character separately before the run text
         if(ptype == RCD_PARA_BULLET && indentPx == 0)
            RcdStampText(rcdCanvBlock, textX, lineY, "•", "Calibri", fontSize, defaultTextColor, stampBg, true);
         RcdStampRuns(rcdCanvBlock, lineTextX, lineY, rcdLineHeight, runs, defaultTextColor, stampBg, fontSize);
        }
      else
        {
         //--- Stamp bullet character then plain text
         if(ptype == RCD_PARA_BULLET && indentPx == 0)
            RcdStampText(rcdCanvBlock, textX, lineY, "•", "Calibri", fontSize, defaultTextColor, stampBg, true);
         RcdStampText(rcdCanvBlock, lineTextX, lineY, renderText, fontName, fontSize, defaultTextColor, stampBg, true);
        }
     }

   //--- Draw the scrollbar pill when scroll is visible and mouse is nearby
   if(rcdScrollVisible && (rcdMouseInBody || rcdIsDraggingSlider))
     {
      int pillMargin = 4;
      int trackH     = rcdBodyHeight - pillMargin * 2;
      int thumbY     = pillMargin;
      //--- Compute the pill's vertical position proportional to scroll offset
      if(rcdMaxScroll > 0)
         thumbY = pillMargin + (int)(((double)rcdScrollPos / rcdMaxScroll) * (trackH - rcdSliderHeight));
      thumbY = MathMax(pillMargin, MathMin(rcdBodyHeight - rcdSliderHeight - pillMargin, thumbY));
      int pillX = rcdPanelWidth - RCD_SB_MARGIN_R - RCD_SB_PILL_W;
      //--- Choose pill color based on drag/hover state
      color pillColor;
      uchar pillAlpha;
      if(rcdIsDraggingSlider)  { pillColor = rcdScrollSliderDrag;  pillAlpha = 255; }
      else if(rcdHoverSlider)  { pillColor = rcdScrollSliderHover; pillAlpha = 255; }
      else                     { pillColor = rcdScrollSlider;       pillAlpha = 180; }
      uint thumbArgb = ColorToARGB(pillColor, pillAlpha);
      //--- Build and downsample an HR pill onto the block canvas
      int pWS = RCD_SB_PILL_W * RCD_SS, pHS = rcdSliderHeight * RCD_SS;
      CCanvas pillHR;
      pillHR.Create("RCD_PillHR_scroll_tmp", pWS, pHS, COLOR_FORMAT_ARGB_NORMALIZE);
      pillHR.Erase(0x00000000);
      RcdFillRoundRectHR(pillHR, 0, 0, pWS, pHS, MathMax(1, pWS/2), thumbArgb);
      int ss2 = RCD_SS * RCD_SS;
      for(int py = 0; py < rcdSliderHeight; py++)
         for(int px2 = 0; px2 < RCD_SB_PILL_W; px2++)
           {
            double sumA=0,sumR=0,sumG=0,sumB=0,wc=0;
            for(int dy=0;dy<RCD_SS;dy++)
               for(int dx=0;dx<RCD_SS;dx++)
                 {
                  int sx=px2*RCD_SS+dx, sy=py*RCD_SS+dy;
                  if(sx>=pWS||sy>=pHS) continue;
                  uint p=pillHR.PixelGet(sx,sy);
                  uchar pa,pr,pg,pb;
                  RcdArgbSplit(p,pa,pr,pg,pb);
                  sumA+=pa;
                  if(pa>0){sumR+=pr; sumG+=pg; sumB+=pb; wc+=1.0;}
                 }
            uchar fa=(uchar)(sumA/ss2);
            if(fa>0&&wc>0)
              {
               uint blended=((uint)fa<<24)|((uint)(uchar)(sumR/wc)<<16)|((uint)(uchar)(sumG/wc)<<8)|(uint)(uchar)(sumB/wc);
               int dstX=pillX+px2, dstY=thumbY+py;
               if(dstX>=0&&dstX<rcdPanelWidth&&dstY>=0&&dstY<rcdBodyHeight)
                 {
                  uint ex=rcdCanvBlock.PixelGet(dstX,dstY);
                  rcdCanvBlock.PixelSet(dstX,dstY,RcdBlendPixel(ex,blended));
                 }
              }
           }
      pillHR.Destroy();
     }

   rcdCanvBlock.Update();
  }

The function opens by filling the high-resolution body canvas with the background color, downsampling it to the display canvas using "RcdDownsampleCanvas", drawing the left and right border lines, then clearing the block overlay canvas to fully transparent — ready to receive all the content drawn on top.

The first content pass draws the colored background panels behind warning, info, and answer block paragraphs. The function walks the wrapped line array looking for block-typed lines, then scans forward to find where each contiguous block run ends — accounting for blank lines that may sit between wrapped lines of the same block. Once the full vertical extent of a block is known, its pixel top and bottom are computed relative to the current scroll position and clamped to the visible area. The fill color and left-side accent bar color are then chosen based on the block type and whether the dark or light theme is active, and both are drawn as filled rectangles onto the block canvas.

The second pass iterates every wrapped line and renders its content. Image placeholder lines are handled by blitting the cached scaled pixel data centered horizontally within the text area, compositing each pixel using "RcdBlendPixel" and skipping fully transparent ones, with a drawn flag per image slot preventing the same image from being blitted more than once per pass. Logo placeholders follow the same approach using the body display pixel array. Text lines that fall entirely outside the visible scroll window are skipped, and each visible line has its horizontal position resolved from its hanging indent value and paragraph type — block paragraphs receive additional left offset to clear the accent bar, and bullet lines have the bullet character stamped separately at the base text position before the main text is shifted right by the bullet width. The default text color and font are selected by paragraph type, the matching block background color is determined for correct alpha reconstruction, and the line is stamped either through the markup-aware run path or the plain text path, depending on whether markup tags are present.

Finally, the scrollbar pill is drawn when the scroll is active, and the mouse is inside the body, or a drag is in progress. Its vertical position is computed proportionally from the current scroll offset within the available track height. A temporary high-resolution canvas is created, filled with a fully rounded rectangle using "RcdFillRoundRectHR", then manually downsampled and composited pixel by pixel onto the block canvas, giving the pill smooth, anti-aliased, rounded ends. The pill color shifts between its normal, hover, and drag states based on interaction flags, and the temporary canvas is destroyed before the block canvas is flushed to the screen with the Update method. The same logic is used for the other layout parts. We will now wire all the parts for the initial run.

Showing the Document and Initializing the Program

With all the rendering, wrapping, and content systems in place, the final step is wiring everything together into the show sequence and the program entry point.

//+------------------------------------------------------------------+
//| Show document — initialise state, canvases, and render           |
//+------------------------------------------------------------------+
void RcdShow()
  {
   //--- Prevent double-initialisation
   if(rcdIsActive) return;
   //--- Apply theme colors and compute layout geometry
   RcdApplyTheme(rcdTheme);
   RcdCalculateLayout();
   //--- Reset all interaction and display state
   rcdActiveTab        = RCD_TAB_LANGUAGE;
   rcdScrollPos        = 0;
   rcdDontShowAgain    = false;
   rcdIsDraggingSlider = false;
   rcdHoverClose       = false;
   rcdHoverOK          = false;
   rcdHoverCancel      = false;
   rcdHoverCheckbox    = false;
   rcdHoverSlider      = false;
   rcdMouseInBody      = false;
   rcdPrevMouseInBody  = false;
   rcdPrevMouseState   = 0;
   for(int i = 0; i < RCD_TAB_COUNT; i++) rcdHoverTabs[i] = false;
   //--- Create canvases; abort and clean up on failure
   if(!RcdCreateCanvases()) { RcdDestroyCanvases(); return; }
   //--- Load header logo and the large body-display logo
   rcdLogoLoaded = RcdLoadLogo();
   RcdLoadLogoDisplay();
   //--- Load all content images from embedded resources
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++)
     {
      rcdImgLoaded[ii]     = false;
      rcdImgCacheValid[ii] = false;
      RcdLoadImage(ii);
     }
   //--- Build documentation content on first show
   if(!rcdContentBuilt) RcdBuildContent();
   //--- Enable mouse move and wheel chart events
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
   //--- Mark document active then render all sections
   rcdIsActive = true;
   RcdRebuildWrappedLines();
   RcdRenderAll();
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Enable mouse move and wheel events on the chart
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
   //--- Show document on attach if requested by the input parameter
   if(rcdShowOnAttach)
     {
      RcdShow();
      Print("Rich Content Document: Displaying canvas-rendered documentation. Theme=",
            (rcdTheme == 0 ? "Dark" : "Light"));
     }
   else
     {
      Print("Rich Content Document: Attached. Set rcdShowOnAttach=true to display, or call RcdShow() manually.");
     }
   return(INIT_SUCCEEDED);
  }

The "RcdShow" function is the single entry point that brings the entire document to life. It opens with a guard check to prevent double initialization if called while the document is already visible. The theme colors are then applied, and the panel geometry computed. Every interaction and display state variable is reset to its default — the active tab is set to the first tab, the scroll position cleared, the checkbox unchecked, and all hover and drag flags set to false — ensuring the document always opens in a clean, consistent state regardless of any prior interaction. Canvas creation is attempted next, and if any canvas fails to initialize, the entire set is destroyed, and the function exits cleanly.

With the canvases ready, both logo variants are loaded, and all five content images are loaded from their embedded resources with their cache flags cleared. The documentation content is built on the very first show call only, since the "rcdContentBuilt" flag prevents it from being rebuilt on subsequent opens. Mouse move and wheel chart events are enabled, the document is marked active, the wrapped lines are built for the initial tab, all four panel sections are rendered, and the chart is redrawn to make everything visible.

In the OnInit event handler, mouse move and wheel events are enabled on the chart, and the "rcdShowOnAttach" input is checked. If true, "RcdShow" is called immediately, and a confirmation message is printed to the journal, noting the active theme. If false, a message is printed instead informing the user how to display the document manually. The event handler concludes by returning INIT_SUCCEEDED to confirm the program initialized correctly. Upon compilation, we get the following outcome.

INITIAL RUN RENDER

Now that we have rendered the document panel, we need to hide it when not needed. Here is the logic we used to achieve that.

//+------------------------------------------------------------------+
//| Hide document — destroy canvases and free all resources          |
//+------------------------------------------------------------------+
void RcdHide()
  {
   if(!rcdIsActive) return;
   //--- Destroy all canvas objects and their chart labels
   RcdDestroyCanvases();
   //--- Free the scaled logo resource if it was created
   if(rcdLogoLoaded)
     {
      ResourceFree(rcdLogoScaledResName);
      rcdLogoLoaded = false;
     }
   //--- Reset image slot state and release pixel data arrays
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++)
     {
      rcdImgLoaded[ii]     = false;
      rcdImgCacheValid[ii] = false;
     }
   ArrayResize(rcdImgPixels0, 0);
   ArrayResize(rcdImgPixels1, 0);
   ArrayResize(rcdImgPixels2, 0);
   ArrayResize(rcdImgPixels3, 0);
   ArrayResize(rcdImgPixels4, 0);
   //--- Release logo display pixel data
   rcdLogoDisplayReady = false;
   ArrayResize(rcdLogoDisplayPixels, 0);
   rcdIsActive = false;
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Destroy all canvases and release all resources cleanly
   RcdHide();
   Print("Rich Content Document: Deinitialized cleanly.");
  }

The "RcdHide" function opens with a guard check mirroring the one in "RcdShow", returning immediately if the document is not currently active. It then destroys all canvas objects and removes their chart labels. If the scaled header logo resource was successfully created during the show sequence, it is freed using ResourceFree, and the loaded flag is reset. All five image slot flags are cleared, and their flat pixel arrays are resized to zero, releasing the memory that was holding the scaled image data. The logo display pixel array is similarly released, and its ready flag is reset. With all resources freed, the active flag is set to false, and the chart is redrawn to clear the panel from the screen.

The OnDeinit event handler keeps things simple — it calls "RcdHide" to handle the full teardown sequence and prints a confirmation message to the journal. This ensures that whether the document is closed by the user clicking a button or by the program being removed from the chart, all allocated memory and chart objects are cleaned up correctly without leaving orphaned resources behind. Finally, we need to handle mouse movement and clicks on the chart.

Event Handling — Interaction, Resize, and Scroll

All user interaction with the document flows through a single central event handler, keeping the OnChartEvent event handler itself completely clean and delegating everything to one dedicated function.

//+------------------------------------------------------------------+
//| Route and process all chart events for the document UI           |
//+------------------------------------------------------------------+
void RcdHandleChartEvent(const int eventId, const long &lParam, const double &dParam, const string &sParam)
  {
   if(!rcdIsActive) return;

   //--- Handle chart resize or reflow events
   if(eventId == CHARTEVENT_CHART_CHANGE)
     {
      //--- Recompute geometry and move all canvas objects to new positions
      RcdCalculateLayout();
      ObjectSetInteger(0, rcdHeaderCanvasName, OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdHeaderCanvasName, OBJPROP_YDISTANCE, rcdHeaderY);
      ObjectSetInteger(0, rcdTabsCanvasName,   OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdTabsCanvasName,   OBJPROP_YDISTANCE, rcdTabsY);
      ObjectSetInteger(0, rcdBodyCanvasName,   OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdBodyCanvasName,   OBJPROP_YDISTANCE, rcdBodyY);
      ObjectSetInteger(0, rcdBlockCanvasName,  OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdBlockCanvasName,  OBJPROP_YDISTANCE, rcdBodyY);
      ObjectSetInteger(0, rcdFooterCanvasName, OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdFooterCanvasName, OBJPROP_YDISTANCE, rcdFooterY);
      //--- Resize canvases when the panel width has changed
      if(rcdCanvHeader.Width() != rcdPanelWidth)
        {
         rcdCanvHeader.Resize(rcdPanelWidth, RCD_HEADER_H);
         rcdCanvTabs.Resize(rcdPanelWidth, RCD_TABS_H);
         rcdCanvBody.Resize(rcdPanelWidth, rcdBodyHeight);
         rcdCanvBodyHR.Resize(rcdPanelWidth * RCD_SS, rcdBodyHeight * RCD_SS);
         rcdCanvBlock.Resize(rcdPanelWidth, rcdBodyHeight);
         rcdCanvFooter.Resize(rcdPanelWidth, RCD_FOOTER_H);
         ObjectSetInteger(0, rcdHeaderCanvasName, OBJPROP_XSIZE, rcdPanelWidth);
         ObjectSetInteger(0, rcdTabsCanvasName,   OBJPROP_XSIZE, rcdPanelWidth);
         ObjectSetInteger(0, rcdBodyCanvasName,   OBJPROP_XSIZE, rcdPanelWidth);
         ObjectSetInteger(0, rcdBodyCanvasName,   OBJPROP_YSIZE, rcdBodyHeight);
         ObjectSetInteger(0, rcdBlockCanvasName,  OBJPROP_XSIZE, rcdPanelWidth);
         ObjectSetInteger(0, rcdBlockCanvasName,  OBJPROP_YSIZE, rcdBodyHeight);
         ObjectSetInteger(0, rcdFooterCanvasName, OBJPROP_XSIZE, rcdPanelWidth);
        }
      else if(rcdCanvBody.Height() != rcdBodyHeight)
        {
         //--- Resize only the body canvases when height alone has changed
         rcdCanvBody.Resize(rcdPanelWidth, rcdBodyHeight);
         rcdCanvBodyHR.Resize(rcdPanelWidth * RCD_SS, rcdBodyHeight * RCD_SS);
         rcdCanvBlock.Resize(rcdPanelWidth, rcdBodyHeight);
         ObjectSetInteger(0, rcdBodyCanvasName,  OBJPROP_YSIZE, rcdBodyHeight);
         ObjectSetInteger(0, rcdBlockCanvasName, OBJPROP_YSIZE, rcdBodyHeight);
        }
      //--- Rewrap content and re-render everything after resize
      RcdRebuildWrappedLines();
      RcdRenderAll();
      ChartRedraw();
      return;
     }

   //--- Handle mouse move and click events
   if(eventId == CHARTEVENT_MOUSE_MOVE)
     {
      int mx     = (int)lParam;
      int my     = (int)dParam;
      int mstate = (int)sParam;
      //--- Snapshot previous hover state for dirty-region detection
      bool pClose = rcdHoverClose, pOK = rcdHoverOK, pCancel = rcdHoverCancel;
      bool pChk = rcdHoverCheckbox, pSL = rcdHoverSlider;
      bool pInBody = rcdMouseInBody;
      bool pTabs[RCD_TAB_COUNT];
      for(int i = 0; i < RCD_TAB_COUNT; i++) pTabs[i] = rcdHoverTabs[i];
      //--- Recompute all hover flags for the new mouse position
      RcdUpdateHovers(mx, my);
      //--- Toggle chart scroll lock when the mouse enters or leaves the body
      if(rcdMouseInBody != rcdPrevMouseInBody)
        {
         ChartSetInteger(0, CHART_MOUSE_SCROLL, !rcdMouseInBody);
         rcdPrevMouseInBody = rcdMouseInBody;
        }
      //--- Compute which regions need to be redrawn
      bool hdrChanged    = (pClose != rcdHoverClose);
      bool tabsChanged   = false;
      bool footerChanged = (pOK != rcdHoverOK) || (pCancel != rcdHoverCancel) || (pChk != rcdHoverCheckbox);
      bool bodyChanged   = (pSL != rcdHoverSlider) || (pInBody != rcdMouseInBody);
      for(int i = 0; i < RCD_TAB_COUNT; i++)
         if(pTabs[i] != rcdHoverTabs[i]) tabsChanged = true;
      //--- Process mouse-down (button pressed) actions
      if(mstate == 1 && rcdPrevMouseState == 0)
        {
         //--- Close on click of close button or OK button
         if(rcdHoverClose || rcdHoverOK) { RcdHide(); rcdPrevMouseState = mstate; return; }
         //--- Close on click of Cancel button
         if(rcdHoverCancel)              { RcdHide(); rcdPrevMouseState = mstate; return; }
         //--- Toggle "don't show again" checkbox state
         if(rcdHoverCheckbox)
           {
            rcdDontShowAgain = !rcdDontShowAgain;
            footerChanged = true;
           }
         //--- Switch active tab on tab click
         for(int i = 0; i < RCD_TAB_COUNT; i++)
           {
            if(rcdHoverTabs[i] && i != rcdActiveTab)
              {
               rcdActiveTab = i;
               rcdScrollPos = 0;
               RcdRebuildWrappedLines();
               tabsChanged = true;
               bodyChanged = true;
               break;
              }
           }
         //--- Handle scrollbar track and pill interactions
         if(rcdScrollVisible && rcdMouseInBody)
           {
            int pillMargin = 4;
            int bLx = mx - rcdPanelX;
            int bLy = my - rcdBodyY;
            int pillX  = rcdPanelWidth - RCD_SB_MARGIN_R - RCD_SB_PILL_W;
            int trackH = rcdBodyHeight - pillMargin * 2;
            int thumbY = pillMargin;
            if(rcdMaxScroll > 0)
               thumbY = pillMargin + (int)(((double)rcdScrollPos / rcdMaxScroll) * (trackH - rcdSliderHeight));
            thumbY = MathMax(pillMargin, MathMin(rcdBodyHeight - rcdSliderHeight - pillMargin, thumbY));
            if(rcdHoverSlider)
              {
               //--- Begin dragging the scrollbar pill
               rcdIsDraggingSlider   = true;
               rcdDragStartMouseY    = bLy;
               rcdDragStartScrollPos = rcdScrollPos;
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
              }
            else if(RcdPointInRect(bLx, bLy, pillX-4, pillMargin, RCD_SB_PILL_W+8, trackH))
              {
               //--- Jump scroll to the clicked track position
               int newTop = bLy - pillMargin - rcdSliderHeight / 2;
               double ratio = (trackH - rcdSliderHeight > 0)
                  ? MathMax(0.0, MathMin(1.0, (double)newTop / (trackH - rcdSliderHeight))) : 0.0;
               rcdScrollPos = MathMax(0, MathMin(rcdMaxScroll, (int)MathRound(ratio * rcdMaxScroll)));
               bodyChanged = true;
              }
           }
        }
      else if(mstate == 1 && rcdPrevMouseState == 1 && rcdIsDraggingSlider)
        {
         //--- Update scroll position while dragging the pill
         int pillMargin = 4;
         int bLy  = my - rcdBodyY;
         int dy   = bLy - rcdDragStartMouseY;
         int travel = (rcdBodyHeight - pillMargin*2) - rcdSliderHeight;
         if(travel > 0)
           {
            int np = rcdDragStartScrollPos + (int)MathRound((double)dy / travel * rcdMaxScroll);
            np = MathMax(0, MathMin(rcdMaxScroll, np));
            if(np != rcdScrollPos) { rcdScrollPos = np; bodyChanged = true; }
           }
        }
      else if(mstate == 0 && rcdPrevMouseState == 1 && rcdIsDraggingSlider)
        {
         //--- Release scrollbar pill and restore chart scroll
         rcdIsDraggingSlider = false;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, !rcdMouseInBody);
         bodyChanged = true;
        }
      //--- Re-render only the dirty regions
      if(hdrChanged)    RcdRenderHeader();
      if(tabsChanged)   RcdRenderTabs();
      if(bodyChanged)   RcdRenderBody();
      if(footerChanged) RcdRenderFooter();
      if(hdrChanged || tabsChanged || bodyChanged || footerChanged) ChartRedraw();
      rcdPrevMouseState = mstate;
      return;
     }

   //--- Handle mouse wheel scroll events
   if(eventId == CHARTEVENT_MOUSE_WHEEL)
     {
      if(!rcdScrollVisible) return;
      int delta = (int)dParam;
      int mx    = (int)(short)lParam;
      int my    = (int)(short)(lParam >> 16);
      int bLx   = mx - rcdPanelX;
      int bLy   = my - rcdBodyY;
      //--- Only scroll when the wheel event originates inside the body area
      if(RcdPointInRect(bLx, bLy, 0, 0, rcdPanelWidth, rcdBodyHeight))
        {
         int step = 3 * rcdLineHeight;
         //--- Scroll up on positive delta, down on negative
         rcdScrollPos += (delta > 0 ? -step : step);
         rcdScrollPos = MathMax(0, MathMin(rcdMaxScroll, rcdScrollPos));
         RcdRenderBody();
         ChartRedraw();
        }
     }
  }

//+------------------------------------------------------------------+
//| Chart event function                                             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Route all chart events to the document's central event handler
   RcdHandleChartEvent(id, lparam, dparam, sparam);
  }

The "RcdHandleChartEvent" function opens with a guard that ignores all events when the document is not active. It then branches into three event paths. When a chart resize event arrives, the layout is recomputed, and all canvas chart objects are repositioned using ObjectSetInteger to their new coordinates. If the panel width has changed, all six canvases are resized and their chart object size properties updated accordingly. If only the height has changed, only the body and block canvases are resized — avoiding unnecessary work on the header, tabs, and footer, which are unaffected by vertical resizes. The wrapped lines are then rebuilt, and everything is re-rendered.

Mouse move events carry the current coordinates and button state. On each event, a snapshot of the previous hover flags is taken, all hover regions are recomputed by calling "RcdUpdateHovers", and when the mouse crosses the body boundary, the chart's native scroll is toggled off to prevent the chart from scrolling underneath the document while the user interacts with it. Rather than re-rendering the entire panel on every mouse move, only the regions whose hover state actually changed are redrawn — the header if the close button hover changed, the tabs bar if any tab hover changed, the footer if any button or checkbox hover changed, and the body if the scrollbar slider hover or the mouse-in-body state changed. This dirty-region approach keeps the rendering efficient.

On a mouse press, the close button in the header and the OK and Cancel buttons in the footer all call "RcdHide" to dismiss the document. A tab click switches the active tab, resets the scroll position, and rebuilds the wrapped lines. A click on the scrollbar pill begins a drag by recording the starting mouse position and scroll offset, and a click on the scrollbar track outside the pill jumps the scroll position proportionally to the click location. While a drag is in progress, the displacement from the drag start is mapped proportionally across the available track travel to compute the new scroll position. On mouse release, the drag state is cleared, and the chart scroll is restored.

Mouse wheel events are only processed when the wheel originates inside the body area, scrolling by three line heights per notch in the direction of the delta, clamping to the valid scroll range, and re-rendering the body. The OnChartEvent event handler itself contains a single line — forwarding all events directly to "RcdHandleChartEvent". That completes our objectives. Next, we backtest the program.


Backtesting

We attached the program to a chart inside MetaTrader 5 to verify that the document renders correctly and that all interactive elements behave as expected. Below is a single Graphics Interchange Format (GIF) image showing the result.

BACKTEST GIF

During testing, the document opened automatically on attachment with the light theme active, tab switching loaded each content section correctly with the scroll position resetting on every switch, and the scrollbar pill tracked mouse drag interactions accurately across the full content height.


Conclusion

In conclusion, we have built a fully canvas-rendered, rich content documentation system for MQL5 programs — a scrollable, tabbed, formatted in-chart manual that any program can adopt to deliver professional, self-contained documentation directly inside MetaTrader 5. Starting from the simple setup wizard we built in Part 9, we have come a long way — replacing plain chart object labels with a pixel-level rendering pipeline, a lightweight inline markup system, supersampled anti-aliased shapes, bicubic-interpolated image scaling, and a fully interactive event-driven interface. The result is a documentation experience that feels like opening a PDF or a Word document, but lives natively inside the chart, compiled directly into the program file it describes. After reading this article, you will be able to:

  • Embed a fully formatted, scrollable, tabbed documentation panel into any MQL5 program you build, replacing plain text guides with a rich content experience
  • Author document content using the inline markup system to produce bold, italic, colored, highlighted, and structured paragraphs that render with the visual quality of a word-processed document
  • Separate the documentation engine into a dedicated header file using the #include directive, keeping your main program file focused on its core logic while the manual plugs in cleanly at compile time


Attachments

S/N
Name
Type
Description
 1 Rich Content Document.mq5 Main Expert Advisor file Main file for handling documentation logic
 2 Rich Content Document BMP files.zip ZIP file Contains referenced BMP image files
Building Volatility Models in MQL5 (Part III): Implementing the SLSQP Algorithm for Model Estimation Building Volatility Models in MQL5 (Part III): Implementing the SLSQP Algorithm for Model Estimation
An SLSQP optimizer is implemented in MQL5 to resolve parameter discrepancies between a volatility library and Python's ARCH module. The article details constraint handling, gradient options, configuration, and convergence controls and shows how to integrate the solver into existing code. Practical examples and comparisons demonstrate matched log‑likelihoods and parameters on shared datasets.
Beyond the Clock (Part 2): Building Runs Bars in MQL5 Beyond the Clock (Part 2): Building Runs Bars in MQL5
We implement tick-, volume-, and dollar-runs bars in Python and MQL5 and align them with the existing bar‑building framework. The article details the dual‑accumulator update, offline calibration with per‑side seeds, state persistence for EAs, and parity verification to match Python and MQL5 outputs. Runs bars expose one‑sided bursts that net imbalance can hide, improving coverage during quiet sessions and for mean‑reversion models.
Beyond GARCH (Part IV): Partition Analysis in MQL5 Beyond GARCH (Part IV): Partition Analysis in MQL5
In this article, we shift from Python research to native MQL5 engineering. We build the first module of the MMAR library: a shared constants header, an SVD-based OLS regression class, a Generalized Hurst Exponent estimator, and the partition analysis engine that computes the partition function, extracts tau(q), estimates H via zero-crossing interpolation, and scores multifractality through three diagnostic tests. Tested on 500,000 bars of EURUSD M10, the engine correctly classifies the data as multifractal in under four seconds. Part 4 of an eight-part series. Part 5 fits the tau(q) curve to four candidate distributions via the Legendre transform.
Application of the Grey Model in Technical Analysis of Financial Time Series Application of the Grey Model in Technical Analysis of Financial Time Series
This article explores the grey model, a promising tool that can expand trader's capabilities. We will look at some options for applying this model to technical analysis and building trading strategies.