3053 lines
No EOL
167 KiB
MQL5
3053 lines
No EOL
167 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 ¶Array[], 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 ¶Array[], 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);
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Split ARGB pixel into four byte channels |
|
|
//+------------------------------------------------------------------+
|
|
void RcdArgbSplit(uint pixelValue, uchar &alphaOut, uchar &redOut, uchar &greenOut, uchar &blueOut)
|
|
{
|
|
//--- Extract alpha channel from bits 31-24
|
|
alphaOut = (uchar)((pixelValue >> 24) & 0xFF);
|
|
//--- Extract red channel from bits 23-16
|
|
redOut = (uchar)((pixelValue >> 16) & 0xFF);
|
|
//--- Extract green channel from bits 15-8
|
|
greenOut = (uchar)((pixelValue >> 8) & 0xFF);
|
|
//--- Extract blue channel from bits 7-0
|
|
blueOut = (uchar)( pixelValue & 0xFF);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Alpha-composite source pixel over destination pixel |
|
|
//+------------------------------------------------------------------+
|
|
uint RcdBlendPixel(uint destPixel, uint srcPixel)
|
|
{
|
|
//--- Normalise source channels to 0-1 range
|
|
double srcA = ((srcPixel >> 24) & 0xFF) / 255.0;
|
|
double srcR = ((srcPixel >> 16) & 0xFF) / 255.0;
|
|
double srcG = ((srcPixel >> 8) & 0xFF) / 255.0;
|
|
double srcB = ( srcPixel & 0xFF) / 255.0;
|
|
//--- Normalise destination channels to 0-1 range
|
|
double dstA = ((destPixel >> 24) & 0xFF) / 255.0;
|
|
double dstR = ((destPixel >> 16) & 0xFF) / 255.0;
|
|
double dstG = ((destPixel >> 8) & 0xFF) / 255.0;
|
|
double dstB = ( destPixel & 0xFF) / 255.0;
|
|
//--- Compute composited alpha via Porter-Duff over formula
|
|
double outA = srcA + dstA * (1.0 - srcA);
|
|
//--- Return transparent black for fully transparent result
|
|
if(outA == 0.0) return 0;
|
|
//--- Pack and return blended ARGB pixel
|
|
return ((uint)(uchar)(outA * 255.0 + 0.5) << 24) |
|
|
((uint)(uchar)((srcR*srcA + dstR*dstA*(1.0-srcA)) / outA * 255.0 + 0.5) << 16) |
|
|
((uint)(uchar)((srcG*srcA + dstG*dstA*(1.0-srcA)) / outA * 255.0 + 0.5) << 8) |
|
|
(uint)(uchar)((srcB*srcA + dstB*dstA*(1.0-srcA)) / outA * 255.0 + 0.5);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Test whether a point lies within a rectangle |
|
|
//+------------------------------------------------------------------+
|
|
bool RcdPointInRect(int pointX, int pointY, int rectX, int rectY, int rectW, int rectH)
|
|
{
|
|
//--- Check horizontal and vertical bounds inclusively
|
|
return (pointX >= rectX && pointX <= rectX + rectW - 1 &&
|
|
pointY >= rectY && pointY <= rectY + rectH - 1);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Compute proportional scrollbar pill height |
|
|
//+------------------------------------------------------------------+
|
|
int RcdCalcSliderHeight(int visibleH, int totalH, int trackH, int minH)
|
|
{
|
|
//--- Return full track height when all content fits in view
|
|
if(totalH <= visibleH) return trackH;
|
|
//--- Calculate visible-to-total ratio
|
|
double ratio = (double)visibleH / totalH;
|
|
//--- Compute proportional pill height
|
|
int computedH = (int)MathFloor(trackH * ratio);
|
|
//--- Enforce minimum pill height
|
|
return MathMax(minH, computedH);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Fill one corner quadrant of a round rectangle with AA |
|
|
//+------------------------------------------------------------------+
|
|
void RcdFillCornerQuadrantHR(CCanvas &targetCanvas, int centerX, int centerY, int radius, uint fillArgb, int signX, int signY)
|
|
{
|
|
double rd = (double)radius; // Radius as double for distance math
|
|
uchar bA = (uchar)((fillArgb >> 24) & 0xFF); // Alpha component of fill colour
|
|
uint rgb = fillArgb & 0x00FFFFFF; // RGB portion without alpha
|
|
int sub = 4; // Sub-pixel sample count per axis
|
|
double step = 1.0 / sub; // Sub-pixel step size
|
|
int subSq = sub * sub; // Total sub-pixel sample count
|
|
//--- Iterate over bounding box pixels around this corner
|
|
for(int dy = -(radius+1); dy <= (radius+1); dy++)
|
|
{
|
|
for(int dx = -(radius+1); dx <= (radius+1); dx++)
|
|
{
|
|
//--- Skip pixels not in this quadrant's signed direction
|
|
bool inQuadrant = ((signX > 0) ? (dx >= 0) : (dx <= 0)) &&
|
|
((signY > 0) ? (dy >= 0) : (dy <= 0));
|
|
if(!inQuadrant) continue;
|
|
//--- Compute distance from arc centre
|
|
double dist = MathSqrt((double)(dx*dx + dy*dy));
|
|
//--- Discard pixels too far outside the arc
|
|
if(dist > rd + 1.0) continue;
|
|
//--- Fill pixels clearly inside the arc directly
|
|
if(dist <= rd - 1.0) { targetCanvas.PixelSet(centerX+dx, centerY+dy, fillArgb); continue; }
|
|
//--- Count sub-pixel samples inside the arc for anti-aliasing
|
|
int insideCount = 0;
|
|
for(int sy = 0; sy < sub; sy++)
|
|
for(int sx = 0; sx < sub; sx++)
|
|
{
|
|
double sdx = (double)dx - 0.5 + (sx+0.5)*step; // Sub-pixel x offset
|
|
double sdy = (double)dy - 0.5 + (sy+0.5)*step; // Sub-pixel y offset
|
|
//--- Accumulate samples that fall inside the circle
|
|
if(sdx*sdx + sdy*sdy <= rd*rd) insideCount++;
|
|
}
|
|
if(insideCount == 0) continue;
|
|
//--- Build pixel with alpha scaled by coverage fraction
|
|
uint blendedPixel = (((uint)(uchar)((int)bA * insideCount / subSq)) << 24) | rgb;
|
|
//--- Composite over existing canvas pixel
|
|
uint existingPixel = targetCanvas.PixelGet(centerX+dx, centerY+dy);
|
|
targetCanvas.PixelSet(centerX+dx, centerY+dy, RcdBlendPixel(existingPixel, blendedPixel));
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Fill a round rectangle on a high-resolution canvas with AA |
|
|
//+------------------------------------------------------------------+
|
|
void RcdFillRoundRectHR(CCanvas &targetCanvas, int rectX, int rectY, int rectW, int rectH, int cornerRadius, uint fillArgb)
|
|
{
|
|
//--- Clamp corner radius to half the shortest dimension
|
|
cornerRadius = MathMin(cornerRadius, MathMin(rectW/2, rectH/2));
|
|
//--- Fall back to plain fill when no rounding is needed
|
|
if(cornerRadius <= 0)
|
|
{
|
|
targetCanvas.FillRectangle(rectX, rectY, rectX+rectW-1, rectY+rectH-1, fillArgb);
|
|
return;
|
|
}
|
|
//--- Fill centre column spanning full height
|
|
targetCanvas.FillRectangle(rectX+cornerRadius, rectY, rectX+rectW-cornerRadius-1, rectY+rectH-1, fillArgb);
|
|
//--- Fill left margin strip between corners
|
|
targetCanvas.FillRectangle(rectX, rectY+cornerRadius, rectX+cornerRadius-1, rectY+rectH-cornerRadius-1, fillArgb);
|
|
//--- Fill right margin strip between corners
|
|
targetCanvas.FillRectangle(rectX+rectW-cornerRadius, rectY+cornerRadius, rectX+rectW-1, rectY+rectH-cornerRadius-1, fillArgb);
|
|
//--- Fill top-left corner quadrant with anti-aliasing
|
|
RcdFillCornerQuadrantHR(targetCanvas, rectX+cornerRadius, rectY+cornerRadius, cornerRadius, fillArgb, -1, -1);
|
|
//--- Fill top-right corner quadrant with anti-aliasing
|
|
RcdFillCornerQuadrantHR(targetCanvas, rectX+rectW-cornerRadius, rectY+cornerRadius, cornerRadius, fillArgb, 1, -1);
|
|
//--- Fill bottom-left corner quadrant with anti-aliasing
|
|
RcdFillCornerQuadrantHR(targetCanvas, rectX+cornerRadius, rectY+rectH-cornerRadius, cornerRadius, fillArgb, -1, 1);
|
|
//--- Fill bottom-right corner quadrant with anti-aliasing
|
|
RcdFillCornerQuadrantHR(targetCanvas, rectX+rectW-cornerRadius, rectY+rectH-cornerRadius, cornerRadius, fillArgb, 1, 1);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Draw anti-aliased rounded rectangle border on HR canvas |
|
|
//+------------------------------------------------------------------+
|
|
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)
|
|
{
|
|
//--- Clamp corner radius to half the shortest dimension
|
|
cornerRadius = MathMin(cornerRadius, MathMin(rectW/2, rectH/2));
|
|
if(cornerRadius <= 0) return;
|
|
//--- Unpack border colour components
|
|
uchar bA = (uchar)((borderArgb >> 24) & 0xFF);
|
|
uint rgb = borderArgb & 0x00FFFFFF;
|
|
//--- Sub-pixel sampling configuration
|
|
int sub = 4;
|
|
double step = 1.0 / sub;
|
|
int subSq = sub * sub;
|
|
double rd = (double)cornerRadius;
|
|
//--- Draw straight edge lines where requested
|
|
if(drawTop) targetCanvas.Line(rectX+cornerRadius, rectY, rectX+rectW-cornerRadius-1, rectY, borderArgb);
|
|
if(drawBottom) targetCanvas.Line(rectX+cornerRadius, rectY+rectH-1, rectX+rectW-cornerRadius-1, rectY+rectH-1, borderArgb);
|
|
if(drawLeft) targetCanvas.Line(rectX, rectY+cornerRadius, rectX, rectY+rectH-cornerRadius-1, borderArgb);
|
|
if(drawRight) targetCanvas.Line(rectX+rectW-1, rectY+cornerRadius, rectX+rectW-1, rectY+rectH-cornerRadius-1, borderArgb);
|
|
//--- Macro: draw one arc quadrant of the border with AA sub-pixel sampling
|
|
#define DRAW_ARC_BORDER(cx, cy, sx, sy) \
|
|
{ \
|
|
for(int _ady = 0; _ady <= cornerRadius+1; _ady++) { \
|
|
for(int _adx = 0; _adx <= cornerRadius+1; _adx++) { \
|
|
double _dist = MathSqrt((double)(_adx*_adx + _ady*_ady)); \
|
|
if(_dist > rd + 1.0 || _dist < rd - 1.0) continue; \
|
|
int _in = 0; \
|
|
for(int _sy2 = 0; _sy2 < sub; _sy2++) \
|
|
for(int _sx2 = 0; _sx2 < sub; _sx2++) { \
|
|
double _sdx = (double)_adx - 0.5 + (_sx2+0.5)*step; \
|
|
double _sdy = (double)_ady - 0.5 + (_sy2+0.5)*step; \
|
|
double _sd = MathSqrt(_sdx*_sdx + _sdy*_sdy); \
|
|
if(_sd >= rd - 0.5 && _sd <= rd + 0.5) _in++; \
|
|
} \
|
|
if(_in == 0) continue; \
|
|
uint _bpx = (((uint)(uchar)((int)bA * _in / subSq)) << 24) | rgb; \
|
|
int _px = (cx) + _adx * (sx); \
|
|
int _py = (cy) + _ady * (sy); \
|
|
uint _ex = targetCanvas.PixelGet(_px, _py); \
|
|
targetCanvas.PixelSet(_px, _py, RcdBlendPixel(_ex, _bpx)); \
|
|
} \
|
|
} \
|
|
}
|
|
//--- Draw each requested arc quadrant
|
|
if(arcTL) DRAW_ARC_BORDER(rectX+cornerRadius, rectY+cornerRadius, -1, -1)
|
|
if(arcTR) DRAW_ARC_BORDER(rectX+rectW-1-cornerRadius, rectY+cornerRadius, 1, -1)
|
|
if(arcBL) DRAW_ARC_BORDER(rectX+cornerRadius, rectY+rectH-1-cornerRadius, -1, 1)
|
|
if(arcBR) DRAW_ARC_BORDER(rectX+rectW-1-cornerRadius, rectY+rectH-1-cornerRadius, 1, 1)
|
|
#undef DRAW_ARC_BORDER
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Downsample supersampled HR canvas into target display canvas |
|
|
//+------------------------------------------------------------------+
|
|
void RcdDownsampleCanvas(CCanvas &targetCanvas, CCanvas &sourceCanvas)
|
|
{
|
|
int targetW = targetCanvas.Width(); // Target canvas width
|
|
int targetH = targetCanvas.Height(); // Target canvas height
|
|
int sourceW = sourceCanvas.Width(); // Source canvas width
|
|
int sourceH = sourceCanvas.Height(); // Source canvas height
|
|
int ss2 = RCD_SS * RCD_SS; // Total supersample count per pixel
|
|
//--- Average each block of RCD_SS x RCD_SS source pixels into one target pixel
|
|
for(int y = 0; y < targetH; y++)
|
|
{
|
|
for(int x = 0; x < targetW; x++)
|
|
{
|
|
double sumA=0, sumR=0, sumG=0, sumB=0, weightCount=0;
|
|
for(int dy = 0; dy < RCD_SS; dy++)
|
|
{
|
|
for(int dx = 0; dx < RCD_SS; dx++)
|
|
{
|
|
//--- Map target pixel to source grid position
|
|
int srcX = x * RCD_SS + dx;
|
|
int srcY = y * RCD_SS + dy;
|
|
if(srcX < 0 || srcX >= sourceW || srcY < 0 || srcY >= sourceH) continue;
|
|
//--- Read and split source pixel channels
|
|
uint srcPixel = sourceCanvas.PixelGet(srcX, srcY);
|
|
uchar pa, pr, pg, pb;
|
|
RcdArgbSplit(srcPixel, pa, pr, pg, pb);
|
|
sumA += pa;
|
|
//--- Accumulate RGB only for non-transparent pixels
|
|
if(pa > 0) { sumR += pr; sumG += pg; sumB += pb; weightCount += 1.0; }
|
|
}
|
|
}
|
|
//--- Compute averaged alpha
|
|
uchar finalA = (uchar)(sumA / ss2);
|
|
//--- Write transparent black for fully transparent result
|
|
if(finalA == 0 || weightCount == 0) { targetCanvas.PixelSet(x, y, 0); continue; }
|
|
//--- Pack and write averaged pixel to target canvas
|
|
targetCanvas.PixelSet(x, y,
|
|
((uint)finalA << 24) |
|
|
((uint)(uchar)(sumR / weightCount) << 16) |
|
|
((uint)(uchar)(sumG / weightCount) << 8) |
|
|
(uint)(uchar)(sumB / weightCount));
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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));
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Evaluate one cubic spline component for bicubic interpolation |
|
|
//+------------------------------------------------------------------+
|
|
double RcdBicubicComponent(uchar &componentValues[], double fracX, double fracY)
|
|
{
|
|
//--- Compute cubic weights along the X axis
|
|
double wx[4];
|
|
double t = fracX;
|
|
wx[0] = (-0.5*t*t*t + t*t - 0.5*t); // Cubic weight for neighbour -1
|
|
wx[1] = ( 1.5*t*t*t - 2.5*t*t + 1.0); // Cubic weight for neighbour 0
|
|
wx[2] = (-1.5*t*t*t + 2.0*t*t + 0.5*t); // Cubic weight for neighbour +1
|
|
wx[3] = ( 0.5*t*t*t - 0.5*t*t); // Cubic weight for neighbour +2
|
|
//--- Interpolate each row of the 4x4 neighbourhood along X
|
|
double yv[4];
|
|
for(int j = 0; j < 4; j++)
|
|
yv[j] = wx[0]*componentValues[j*4+0] + wx[1]*componentValues[j*4+1] +
|
|
wx[2]*componentValues[j*4+2] + wx[3]*componentValues[j*4+3];
|
|
//--- Compute cubic weights along the Y axis
|
|
double wy[4];
|
|
t = fracY;
|
|
wy[0] = (-0.5*t*t*t + t*t - 0.5*t);
|
|
wy[1] = ( 1.5*t*t*t - 2.5*t*t + 1.0);
|
|
wy[2] = (-1.5*t*t*t + 2.0*t*t + 0.5*t);
|
|
wy[3] = ( 0.5*t*t*t - 0.5*t*t);
|
|
//--- Interpolate the four row results along Y and clamp
|
|
double result = wy[0]*yv[0] + wy[1]*yv[1] + wy[2]*yv[2] + wy[3]*yv[3];
|
|
return MathMax(0.0, MathMin(255.0, result));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Sample a pixel from an image array using bicubic interpolation |
|
|
//+------------------------------------------------------------------+
|
|
uint RcdBicubicInterp(uint &pixelArray[], int imgW, int imgH, double sampleX, double sampleY)
|
|
{
|
|
//--- Split sample position into integer and fractional parts
|
|
int x0 = (int)sampleX, y0 = (int)sampleY;
|
|
double fx = sampleX - x0, fy = sampleY - y0;
|
|
//--- Clamp the four neighbouring pixel coordinates on each axis
|
|
int xi[4], yi[4];
|
|
for(int i = -1; i <= 2; i++)
|
|
{
|
|
xi[i+1] = (int)MathMin(MathMax(x0+i, 0), imgW-1); // Clamped X neighbour
|
|
yi[i+1] = (int)MathMin(MathMax(y0+i, 0), imgH-1); // Clamped Y neighbour
|
|
}
|
|
//--- Gather the 4x4 neighbourhood pixels
|
|
uint neighborhood[16];
|
|
for(int j = 0; j < 4; j++)
|
|
for(int i = 0; i < 4; i++)
|
|
neighborhood[j*4+i] = pixelArray[yi[j]*imgW + xi[i]];
|
|
//--- Split neighbourhood pixels into channel arrays
|
|
uchar aC[16], rC[16], gC[16], bC[16];
|
|
for(int i = 0; i < 16; i++)
|
|
RcdArgbSplit(neighborhood[i], aC[i], rC[i], gC[i], bC[i]);
|
|
//--- Interpolate each channel independently and pack result
|
|
uchar oA = (uchar)RcdBicubicComponent(aC, fx, fy);
|
|
uchar oR = (uchar)RcdBicubicComponent(rC, fx, fy);
|
|
uchar oG = (uchar)RcdBicubicComponent(gC, fx, fy);
|
|
uchar oB = (uchar)RcdBicubicComponent(bC, fx, fy);
|
|
return (((uint)oA) << 24) | (((uint)oR) << 16) | (((uint)oG) << 8) | ((uint)oB);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Append a paragraph entry to a paragraph array |
|
|
//+------------------------------------------------------------------+
|
|
void RcdAddPara(RcdPara ¶Array[], 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 colour token string to its colour value |
|
|
//+------------------------------------------------------------------+
|
|
color RcdResolveColorToken(const string colorToken)
|
|
{
|
|
//--- Return the theme colour 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 colour 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 colour, 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 colour 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;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Wrap paragraph array into display lines within pixel width |
|
|
//+------------------------------------------------------------------+
|
|
void RcdWrapText(const RcdPara ¶Array[], 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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Apply theme colour palette for dark or light mode |
|
|
//+------------------------------------------------------------------+
|
|
void RcdApplyTheme(int theme)
|
|
{
|
|
if(theme == 0)
|
|
{
|
|
//--- Dark theme colour assignments
|
|
rcdBg = C'20,24,34'; // Deep dark navy background
|
|
rcdHeaderBg = C'16,20,30'; // Darker header background
|
|
rcdTabsBg = C'24,29,42'; // Slightly lighter tabs background
|
|
rcdPanelAlt = C'28,33,47'; // Footer panel background
|
|
rcdBorder = C'52,62,85'; // Subtle border colour
|
|
rcdHeaderText = C'235,242,255'; // Bright header title text
|
|
rcdSubText = C'140,154,178'; // Muted secondary text
|
|
rcdBodyText = C'210,218,235'; // Readable body text
|
|
rcdHeadingText = C'72,144,220'; // Bright blue heading text
|
|
rcdAccentColor = C'52,120,240'; // Primary accent blue
|
|
rcdLinkColor = C'110,175,255'; // Hyperlink colour
|
|
rcdHighlightColor = C'255,205,100'; // Gold highlight / numbered items
|
|
rcdTabInactive = C'140,154,178'; // Inactive tab text
|
|
rcdTabActive = C'235,242,255'; // Active tab text
|
|
rcdTabHover = C'200,214,240'; // Hovered tab text
|
|
rcdButtonBg = C'52,120,240'; // Primary button background
|
|
rcdButtonBgHover = C'80,145,255'; // Primary button hover background
|
|
rcdButtonText = C'255,255,255'; // Button text
|
|
rcdCancelBg = C'55,65,88'; // Cancel button background
|
|
rcdCancelBgHover = C'75,88,115'; // Cancel button hover background
|
|
rcdCheckboxBg = C'28,33,47'; // Checkbox background
|
|
rcdCheckboxBorder = C'100,115,145'; // Checkbox border
|
|
rcdCheckboxChecked = C'52,120,240'; // Checkbox checked colour
|
|
rcdCloseColor = C'160,174,200'; // Close button colour
|
|
rcdCloseHoverColor = C'196,43,28'; // Close button hover colour (red)
|
|
rcdScrollSlider = C'95,108,135'; // Scrollbar slider normal
|
|
rcdScrollSliderHover = C'140,158,192'; // Scrollbar slider hover
|
|
rcdScrollSliderDrag = C'52,120,240'; // Scrollbar slider drag
|
|
}
|
|
else
|
|
{
|
|
//--- Light theme colour assignments
|
|
rcdBg = C'248,251,255'; // Clean white-blue background
|
|
rcdHeaderBg = C'225,232,248'; // Light header background
|
|
rcdTabsBg = C'235,241,252'; // Light tabs background
|
|
rcdPanelAlt = C'242,246,255'; // Light footer panel
|
|
rcdBorder = C'170,182,210'; // Visible light border
|
|
rcdHeaderText = C'18,26,48'; // Dark header title
|
|
rcdSubText = C'95,108,135'; // Muted secondary text
|
|
rcdBodyText = C'38,46,68'; // Dark readable body text
|
|
rcdHeadingText = C'28,96,200'; // Blue heading text
|
|
rcdAccentColor = C'28,96,220'; // Primary accent blue
|
|
rcdLinkColor = C'0,88,200'; // Hyperlink blue
|
|
rcdHighlightColor = C'155,85,0'; // Dark gold for light theme
|
|
rcdTabInactive = C'95,108,135'; // Inactive tab text
|
|
rcdTabActive = C'18,26,48'; // Active tab text
|
|
rcdTabHover = C'28,38,68'; // Hovered tab text
|
|
rcdButtonBg = C'28,96,220'; // Primary button background
|
|
rcdButtonBgHover = C'55,125,240'; // Primary button hover background
|
|
rcdButtonText = C'255,255,255'; // Button text
|
|
rcdCancelBg = C'208,216,232'; // Cancel button background
|
|
rcdCancelBgHover = C'178,190,215'; // Cancel button hover background
|
|
rcdCheckboxBg = C'255,255,255'; // Checkbox background
|
|
rcdCheckboxBorder = C'145,158,185'; // Checkbox border
|
|
rcdCheckboxChecked = C'28,96,220'; // Checkbox checked colour
|
|
rcdCloseColor = C'95,108,135'; // Close button colour
|
|
rcdCloseHoverColor = C'196,43,28'; // Close button hover colour (red)
|
|
rcdScrollSlider = C'135,148,175'; // Scrollbar slider normal
|
|
rcdScrollSliderHover = C'88,102,132'; // Scrollbar slider hover
|
|
rcdScrollSliderDrag = C'28,96,220'; // Scrollbar slider drag
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Render the header canvas with logo, title, and close button |
|
|
//+------------------------------------------------------------------+
|
|
void RcdRenderHeader()
|
|
{
|
|
rcdCanvHeader.Erase(0x00000000);
|
|
//--- Create a temporary HR canvas for the rounded header background
|
|
int hsW = rcdPanelWidth * RCD_SS;
|
|
int hsH = RCD_HEADER_H * RCD_SS;
|
|
int cornerR = 10 * RCD_SS;
|
|
CCanvas hrHeader;
|
|
hrHeader.Create("RCD_HdrHR_tmp", hsW, hsH, COLOR_FORMAT_ARGB_NORMALIZE);
|
|
hrHeader.Erase(0x00000000);
|
|
uint bgArgb = ColorToARGB(rcdHeaderBg, 255);
|
|
//--- Draw the top-rounded, flat-bottomed header shape
|
|
RcdFillRoundRectHR(hrHeader, 0, 0, hsW, hsH, cornerR, bgArgb);
|
|
hrHeader.FillRectangle(0, hsH/2, hsW-1, hsH-1, bgArgb);
|
|
//--- Draw close button hover highlight when mouse is over it
|
|
int closeBtnH = RCD_HEADER_H - 8;
|
|
int closeBtnW = (int)(closeBtnH * 1.5);
|
|
if(rcdHoverClose)
|
|
{
|
|
uint hoverArgb = ColorToARGB(rcdCloseHoverColor, 255);
|
|
hrHeader.FillRectangle(hsW - closeBtnW * RCD_SS, 0, hsW-1, closeBtnH * RCD_SS - 1, hoverArgb);
|
|
//--- Erase pixels outside the top-right rounded corner of the header
|
|
for(int dy2 = 0; dy2 < cornerR+2; dy2++)
|
|
for(int dx2 = 0; dx2 < cornerR+2; dx2++)
|
|
{
|
|
int px2 = hsW - cornerR + dx2, py2 = dy2;
|
|
if(px2 < 0 || px2 >= hsW || py2 < 0 || py2 >= hsH) continue;
|
|
double ddx = (double)(dx2), ddy = (double)(dy2 - cornerR);
|
|
double dist = MathSqrt(ddx*ddx + ddy*ddy);
|
|
//--- Clear pixels that fall outside the corner arc
|
|
if(dist > (double)cornerR) hrHeader.PixelSet(px2, py2, 0x00000000);
|
|
}
|
|
//--- Refill the corner arc with hover colour
|
|
RcdFillCornerQuadrantHR(hrHeader, hsW-cornerR, cornerR, cornerR, hoverArgb, 1, -1);
|
|
}
|
|
//--- Downsample HR header to the display canvas
|
|
RcdDownsampleCanvas(rcdCanvHeader, hrHeader);
|
|
hrHeader.Destroy();
|
|
//--- Draw border lines along header edges
|
|
uint borderArgb = ColorToARGB(rcdBorder, 255);
|
|
int crD = 10;
|
|
rcdCanvHeader.Line(crD, 0, rcdPanelWidth - crD - 1, 0, borderArgb);
|
|
rcdCanvHeader.Line(0, crD, 0, RCD_HEADER_H - 1, borderArgb);
|
|
rcdCanvHeader.Line(rcdPanelWidth-1, crD, rcdPanelWidth-1, RCD_HEADER_H-1, borderArgb);
|
|
//--- Draw anti-aliased arcs for the top-left and top-right corners
|
|
RcdDrawRoundRectBorderHR(rcdCanvHeader, 0, 0, rcdPanelWidth, RCD_HEADER_H, crD, borderArgb,
|
|
false, false, false, false, true, false, false, false);
|
|
RcdDrawRoundRectBorderHR(rcdCanvHeader, 0, 0, rcdPanelWidth, RCD_HEADER_H, crD, borderArgb,
|
|
false, false, false, false, false, true, false, false);
|
|
//--- Compute logo and text layout positions
|
|
int tPad = 10;
|
|
int logoY = (RCD_HEADER_H - RCD_LOGO_SIZE) / 2;
|
|
int logoX = tPad;
|
|
//--- Blit the scaled logo pixels onto the header canvas
|
|
if(rcdLogoLoaded)
|
|
{
|
|
uint lp[];
|
|
uint lw = 0, lh = 0;
|
|
if(ResourceReadImage(rcdLogoScaledResName, lp, lw, lh))
|
|
{
|
|
for(int yy = 0; yy < (int)lh; yy++)
|
|
for(int xx = 0; xx < (int)lw; xx++)
|
|
{
|
|
uint srcPx = lp[yy * lw + xx];
|
|
uchar a, r, g, b;
|
|
RcdArgbSplit(srcPx, a, r, g, b);
|
|
if(a == 0) continue;
|
|
//--- Composite logo pixel over the header background
|
|
uint ex = rcdCanvHeader.PixelGet(logoX+xx, logoY+yy);
|
|
rcdCanvHeader.PixelSet(logoX+xx, logoY+yy, RcdBlendPixel(ex, srcPx));
|
|
}
|
|
}
|
|
}
|
|
//--- Compute text X start position accounting for logo presence
|
|
int textX = tPad + (rcdLogoLoaded ? RCD_LOGO_SIZE + tPad : 0);
|
|
//--- Measure and stamp the main title string
|
|
string titleStr = "Rich Content Document";
|
|
TextSetFont("Arial Bold", -(RCD_FONT_TITLE * 10));
|
|
uint tW = 0, tH = 0;
|
|
TextGetSize(titleStr, tW, tH);
|
|
int titleY = (RCD_HEADER_H / 2) - (int)tH - 1;
|
|
RcdStampText(rcdCanvHeader, textX, titleY, titleStr, "Arial Bold", RCD_FONT_TITLE, rcdHeaderText, rcdHeaderBg, true);
|
|
//--- Measure and stamp the subtitle string below the title
|
|
string subtitleStr = "MQL5 & MT5 — Canvas-Rendered Documentation";
|
|
TextSetFont("Arial", -(RCD_FONT_SUBTITLE * 10));
|
|
uint sW = 0, sH = 0;
|
|
TextGetSize(subtitleStr, sW, sH);
|
|
int subtitleY = (RCD_HEADER_H / 2) + 2;
|
|
RcdStampText(rcdCanvHeader, textX, subtitleY, subtitleStr, "Arial", RCD_FONT_SUBTITLE, rcdSubText, rcdHeaderBg, true);
|
|
//--- Stamp the close button icon using the Webdings X glyph
|
|
color closeCol = rcdHoverClose ? clrWhite : rcdCloseColor;
|
|
color closeBgStamp = rcdHoverClose ? rcdCloseHoverColor : rcdHeaderBg;
|
|
TextSetFont("Webdings", -(RCD_FONT_CLOSE * 10));
|
|
uint icW = 0, icH = 0;
|
|
TextGetSize(CharToString((uchar)114), icW, icH);
|
|
RcdStampText(rcdCanvHeader,
|
|
(rcdPanelWidth - closeBtnW) + (closeBtnW - (int)icW)/2,
|
|
(closeBtnH - (int)icH)/2,
|
|
CharToString((uchar)114), "Webdings", RCD_FONT_CLOSE,
|
|
closeCol, closeBgStamp, true);
|
|
//--- Flush the header canvas to the screen
|
|
rcdCanvHeader.Update();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Render the tabs bar canvas with labels and active pill |
|
|
//+------------------------------------------------------------------+
|
|
void RcdRenderTabs()
|
|
{
|
|
rcdCanvTabs.Erase(0);
|
|
//--- Fill the tab bar background
|
|
uint bgArgb = ColorToARGB(rcdTabsBg, 255);
|
|
rcdCanvTabs.FillRectangle(0, 0, rcdPanelWidth-1, RCD_TABS_H-1, bgArgb);
|
|
uint borderArgb = ColorToARGB(rcdBorder, 255);
|
|
//--- Draw left and right border lines
|
|
rcdCanvTabs.Line(0, 0, 0, RCD_TABS_H-1, borderArgb);
|
|
rcdCanvTabs.Line(rcdPanelWidth-1, 0, rcdPanelWidth-1, RCD_TABS_H-1, borderArgb);
|
|
//--- Compute tab slot width from available inner width
|
|
int innerW = rcdPanelWidth - RCD_PAD * 2;
|
|
int tabW = innerW / RCD_TAB_COUNT;
|
|
//--- Render each tab label and the active underline pill
|
|
for(int i = 0; i < RCD_TAB_COUNT; i++)
|
|
{
|
|
int tabX = RCD_PAD + i * tabW;
|
|
bool isActive = (i == rcdActiveTab);
|
|
//--- Choose text colour based on active/hover/inactive state
|
|
color textColor;
|
|
if(isActive) textColor = rcdTabActive;
|
|
else if(rcdHoverTabs[i]) textColor = rcdTabHover;
|
|
else textColor = rcdTabInactive;
|
|
//--- Centre and stamp the tab label text
|
|
TextSetFont("Arial Bold", -(RCD_FONT_TAB * 10));
|
|
uint tW = 0, tH = 0;
|
|
TextGetSize(rcdTabTitles[i], tW, tH);
|
|
int labelX = tabX + (tabW - (int)tW) / 2;
|
|
int labelY = (RCD_TABS_H - (int)tH) / 2;
|
|
RcdStampText(rcdCanvTabs, labelX, labelY, rcdTabTitles[i], "Arial Bold", RCD_FONT_TAB, textColor, rcdTabsBg, true);
|
|
//--- Draw the accent underline pill for the active tab
|
|
if(isActive)
|
|
{
|
|
int pillW = 36, pillH = 3;
|
|
int ulX = tabX + tabW/2 - pillW/2;
|
|
int ulY = RCD_TABS_H - pillH - 2;
|
|
int pWS = pillW * RCD_SS, pHS = pillH * RCD_SS;
|
|
//--- Build HR pill canvas and fill with a rounded rectangle
|
|
CCanvas pillHR;
|
|
pillHR.Create("RCD_PillHR_tmp", pWS, pHS, COLOR_FORMAT_ARGB_NORMALIZE);
|
|
pillHR.Erase(0x00000000);
|
|
RcdFillRoundRectHR(pillHR, 0, 0, pWS, pHS, MathMax(1, pHS/2), ColorToARGB(rcdAccentColor, 255));
|
|
//--- Downsample the HR pill directly onto the tabs canvas
|
|
int ss2 = RCD_SS * RCD_SS;
|
|
for(int py = 0; py < pillH; py++)
|
|
for(int px = 0; px < pillW; px++)
|
|
{
|
|
double sA=0, sR=0, sG=0, sB=0, wc=0;
|
|
for(int dy = 0; dy < RCD_SS; dy++)
|
|
for(int dx = 0; dx < RCD_SS; dx++)
|
|
{
|
|
int sx = px*RCD_SS+dx, sy = py*RCD_SS+dy;
|
|
if(sx>=pWS||sy>=pHS) continue;
|
|
uint p2 = pillHR.PixelGet(sx, sy);
|
|
uchar pa, pr, pg, pb;
|
|
RcdArgbSplit(p2, pa, pr, pg, pb);
|
|
sA+=pa;
|
|
if(pa>0){sR+=pr; sG+=pg; sB+=pb; wc+=1.0;}
|
|
}
|
|
uchar fa=(uchar)(sA/ss2);
|
|
if(fa>0&&wc>0)
|
|
{
|
|
uint bpx=((uint)fa<<24)|((uint)(uchar)(sR/wc)<<16)|((uint)(uchar)(sG/wc)<<8)|(uint)(uchar)(sB/wc);
|
|
int dX=ulX+px, dY=ulY+py;
|
|
if(dX>=0&&dX<rcdPanelWidth&&dY>=0&&dY<RCD_TABS_H)
|
|
{
|
|
uint ex=rcdCanvTabs.PixelGet(dX,dY);
|
|
rcdCanvTabs.PixelSet(dX,dY,RcdBlendPixel(ex,bpx));
|
|
}
|
|
}
|
|
}
|
|
pillHR.Destroy();
|
|
}
|
|
}
|
|
//--- Draw the bottom border line spanning the full tab bar width
|
|
rcdCanvTabs.Line(0, RCD_TABS_H-1, rcdPanelWidth-1, RCD_TABS_H-1, borderArgb);
|
|
rcdCanvTabs.Update();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Render body background, block highlights, text, and scrollbar |
|
|
//+------------------------------------------------------------------+
|
|
void RcdRenderBody()
|
|
{
|
|
//--- Fill the HR canvas with the background colour
|
|
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 colour selection
|
|
bool isDark = (rcdBg == C'20,24,34');
|
|
|
|
//--- Draw coloured 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 colours 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 colour 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 colour 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 colour 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();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Render the footer canvas with buttons and checkbox |
|
|
//+------------------------------------------------------------------+
|
|
void RcdRenderFooter()
|
|
{
|
|
rcdCanvFooter.Erase(0x00000000);
|
|
//--- Build HR footer canvas with a bottom-rounded background shape
|
|
int fsW = rcdPanelWidth * RCD_SS;
|
|
int fsH = RCD_FOOTER_H * RCD_SS;
|
|
CCanvas hrFooter;
|
|
hrFooter.Create("RCD_FtrHR_tmp", fsW, fsH, COLOR_FORMAT_ARGB_NORMALIZE);
|
|
hrFooter.Erase(0x00000000);
|
|
uint bgArgb = ColorToARGB(rcdPanelAlt, 255);
|
|
RcdFillRoundRectHR(hrFooter, 0, 0, fsW, fsH, 10 * RCD_SS, bgArgb);
|
|
//--- Flatten the top half so only the bottom corners are rounded
|
|
hrFooter.FillRectangle(0, 0, fsW-1, fsH/2, bgArgb);
|
|
RcdDownsampleCanvas(rcdCanvFooter, hrFooter);
|
|
hrFooter.Destroy();
|
|
//--- Draw border lines around the footer edges
|
|
uint borderArgb = ColorToARGB(rcdBorder, 255);
|
|
int crD = 10;
|
|
rcdCanvFooter.Line(0, 0, rcdPanelWidth-1, 0, borderArgb);
|
|
rcdCanvFooter.Line(0, 0, 0, RCD_FOOTER_H-1-crD, borderArgb);
|
|
rcdCanvFooter.Line(rcdPanelWidth-1, 0, rcdPanelWidth-1, RCD_FOOTER_H-1-crD, borderArgb);
|
|
rcdCanvFooter.Line(crD, RCD_FOOTER_H-1, rcdPanelWidth-crD-1, RCD_FOOTER_H-1, borderArgb);
|
|
//--- Draw anti-aliased arcs for the bottom-left and bottom-right corners
|
|
RcdDrawRoundRectBorderHR(rcdCanvFooter, 0, 0, rcdPanelWidth, RCD_FOOTER_H, crD, borderArgb,
|
|
false,false,false,false,false,false,true,false);
|
|
RcdDrawRoundRectBorderHR(rcdCanvFooter, 0, 0, rcdPanelWidth, RCD_FOOTER_H, crD, borderArgb,
|
|
false,false,false,false,false,false,false,true);
|
|
//--- Compute button layout positions
|
|
int fPad = 10;
|
|
int okX = rcdPanelWidth - fPad - RCD_BUTTON_W;
|
|
int okY = (RCD_FOOTER_H - RCD_BUTTON_H) / 2;
|
|
int caX = okX - RCD_BUTTON_W - fPad;
|
|
int caY = okY;
|
|
//--- Resolve Cancel button colours based on hover state
|
|
color caBg = rcdHoverCancel ? rcdCloseHoverColor : rcdCancelBg;
|
|
color caTextC = rcdHoverCancel ? clrWhite : rcdBodyText;
|
|
|
|
//--- Render the OK / Close button with a rounded filled background
|
|
{
|
|
uchar okAlpha = rcdHoverOK ? (uchar)128 : (uchar)76;
|
|
int bWS = RCD_BUTTON_W * RCD_SS, bHS = RCD_BUTTON_H * RCD_SS;
|
|
CCanvas btnHR;
|
|
btnHR.Create("RCD_OkBtnHR_tmp", bWS, bHS, COLOR_FORMAT_ARGB_NORMALIZE);
|
|
btnHR.Erase(0x00000000);
|
|
RcdFillRoundRectHR(btnHR, 0, 0, bWS, bHS, 5*RCD_SS, ColorToARGB(rcdButtonBg, okAlpha));
|
|
int ss2b = RCD_SS*RCD_SS;
|
|
for(int py=0;py<RCD_BUTTON_H;py++)
|
|
for(int px2=0;px2<RCD_BUTTON_W;px2++)
|
|
{
|
|
double sA=0,sR=0,sG=0,sB=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>=bWS||sy>=bHS) continue;
|
|
uint p2=btnHR.PixelGet(sx,sy);
|
|
uchar pa,pr,pg,pb;
|
|
RcdArgbSplit(p2,pa,pr,pg,pb);
|
|
sA+=pa;
|
|
if(pa>0){sR+=pr; sG+=pg; sB+=pb; wc+=1.0;}
|
|
}
|
|
uchar fa=(uchar)(sA/ss2b);
|
|
if(fa>0&&wc>0)
|
|
{
|
|
uint bpx=((uint)fa<<24)|((uint)(uchar)(sR/wc)<<16)|((uint)(uchar)(sG/wc)<<8)|(uint)(uchar)(sB/wc);
|
|
int dX=okX+px2, dY=okY+py;
|
|
if(dX>=0&&dX<rcdPanelWidth&&dY>=0&&dY<RCD_FOOTER_H)
|
|
rcdCanvFooter.PixelSet(dX,dY,RcdBlendPixel(rcdCanvFooter.PixelGet(dX,dY),bpx));
|
|
}
|
|
}
|
|
btnHR.Destroy();
|
|
//--- Draw the button border using an anti-aliased round rectangle
|
|
RcdDrawRoundRectBorderHR(rcdCanvFooter, okX, okY, RCD_BUTTON_W, RCD_BUTTON_H, 5,
|
|
ColorToARGB(rcdButtonBg, 255), true,true,true,true,true,true,true,true);
|
|
}
|
|
|
|
//--- Render the Cancel button with a rounded filled background
|
|
{
|
|
int bWS=RCD_BUTTON_W*RCD_SS, bHS=RCD_BUTTON_H*RCD_SS;
|
|
CCanvas btnHR;
|
|
btnHR.Create("RCD_CaBtnHR_tmp", bWS, bHS, COLOR_FORMAT_ARGB_NORMALIZE);
|
|
btnHR.Erase(0x00000000);
|
|
RcdFillRoundRectHR(btnHR, 0, 0, bWS, bHS, 5*RCD_SS, ColorToARGB(caBg, 255));
|
|
int ss2b=RCD_SS*RCD_SS;
|
|
for(int py=0;py<RCD_BUTTON_H;py++)
|
|
for(int px2=0;px2<RCD_BUTTON_W;px2++)
|
|
{
|
|
double sA=0,sR=0,sG=0,sB=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>=bWS||sy>=bHS) continue;
|
|
uint p2=btnHR.PixelGet(sx,sy);
|
|
uchar pa,pr,pg,pb;
|
|
RcdArgbSplit(p2,pa,pr,pg,pb);
|
|
sA+=pa;
|
|
if(pa>0){sR+=pr; sG+=pg; sB+=pb; wc+=1.0;}
|
|
}
|
|
uchar fa=(uchar)(sA/ss2b);
|
|
if(fa>0&&wc>0)
|
|
{
|
|
uint bpx=((uint)fa<<24)|((uint)(uchar)(sR/wc)<<16)|((uint)(uchar)(sG/wc)<<8)|(uint)(uchar)(sB/wc);
|
|
int dX=caX+px2, dY=caY+py;
|
|
if(dX>=0&&dX<rcdPanelWidth&&dY>=0&&dY<RCD_FOOTER_H)
|
|
rcdCanvFooter.PixelSet(dX,dY,RcdBlendPixel(rcdCanvFooter.PixelGet(dX,dY),bpx));
|
|
}
|
|
}
|
|
btnHR.Destroy();
|
|
}
|
|
|
|
//--- Stamp "Close" label centred inside the OK button
|
|
TextSetFont("Arial Bold", -(RCD_FONT_BUTTON * 10));
|
|
uint okTW=0,okTH=0;
|
|
TextGetSize("Close",okTW,okTH);
|
|
RcdStampText(rcdCanvFooter, okX+(RCD_BUTTON_W-(int)okTW)/2, okY+(RCD_BUTTON_H-(int)okTH)/2,
|
|
"Close","Arial Bold",RCD_FONT_BUTTON,rcdButtonBg,rcdPanelAlt,true);
|
|
//--- Stamp "Cancel" label centred inside the Cancel button
|
|
uint caTW=0,caTH=0;
|
|
TextGetSize("Cancel",caTW,caTH);
|
|
RcdStampText(rcdCanvFooter, caX+(RCD_BUTTON_W-(int)caTW)/2, caY+(RCD_BUTTON_H-(int)caTH)/2,
|
|
"Cancel","Arial Bold",RCD_FONT_BUTTON,caTextC,caBg,true);
|
|
|
|
//--- Draw the "Don't show again" checkbox
|
|
int cbX = fPad;
|
|
int cbY = (RCD_FOOTER_H - RCD_CHECKBOX_SIZE) / 2;
|
|
rcdCanvFooter.FillRectangle(cbX, cbY, cbX+RCD_CHECKBOX_SIZE-1, cbY+RCD_CHECKBOX_SIZE-1, ColorToARGB(rcdCheckboxBg, 255));
|
|
//--- Highlight border on hover
|
|
color cbBorderColor = rcdHoverCheckbox ? rcdAccentColor : rcdCheckboxBorder;
|
|
rcdCanvFooter.Rectangle(cbX, cbY, cbX+RCD_CHECKBOX_SIZE-1, cbY+RCD_CHECKBOX_SIZE-1, ColorToARGB(cbBorderColor, 255));
|
|
//--- Fill inner check mark when the box is ticked
|
|
if(rcdDontShowAgain)
|
|
rcdCanvFooter.FillRectangle(cbX+3, cbY+3, cbX+RCD_CHECKBOX_SIZE-4, cbY+RCD_CHECKBOX_SIZE-4, ColorToARGB(rcdCheckboxChecked, 255));
|
|
//--- Stamp checkbox label with hover-sensitive colour
|
|
TextSetFont("Arial", -(RCD_FONT_LABEL * 10));
|
|
uint lbW=0,lbH=0;
|
|
TextGetSize("Don't show again",lbW,lbH);
|
|
color labelColor = rcdHoverCheckbox ? rcdTabActive : rcdBodyText;
|
|
RcdStampText(rcdCanvFooter, cbX+RCD_CHECKBOX_SIZE+6, cbY+(RCD_CHECKBOX_SIZE-(int)lbH)/2,
|
|
"Don't show again","Arial",RCD_FONT_LABEL,labelColor,rcdPanelAlt,true);
|
|
rcdCanvFooter.Update();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Render all four panel sections in order |
|
|
//+------------------------------------------------------------------+
|
|
void RcdRenderAll()
|
|
{
|
|
//--- Skip render when document is not visible
|
|
if(!rcdIsActive) return;
|
|
RcdRenderHeader();
|
|
RcdRenderTabs();
|
|
RcdRenderBody();
|
|
RcdRenderFooter();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update hover flags for all interactive regions |
|
|
//+------------------------------------------------------------------+
|
|
void RcdUpdateHovers(int mouseX, int mouseY)
|
|
{
|
|
//--- Test close button hit area in header-relative coordinates
|
|
int closeBtnH = RCD_HEADER_H - 8;
|
|
int closeBtnW = (int)(closeBtnH * 1.5);
|
|
int hLx = mouseX - rcdPanelX;
|
|
int hLy = mouseY - rcdHeaderY;
|
|
rcdHoverClose = RcdPointInRect(hLx, hLy, rcdPanelWidth - closeBtnW, 0, closeBtnW, closeBtnH);
|
|
//--- Test each tab hit area in tabs-relative coordinates
|
|
int innerW = rcdPanelWidth - RCD_PAD * 2;
|
|
int tabW = innerW / RCD_TAB_COUNT;
|
|
int tLx = mouseX - rcdPanelX;
|
|
int tLy = mouseY - rcdTabsY;
|
|
for(int i = 0; i < RCD_TAB_COUNT; i++)
|
|
{
|
|
int tabX = RCD_PAD + i * tabW;
|
|
rcdHoverTabs[i] = RcdPointInRect(tLx, tLy, tabX, 0, tabW, RCD_TABS_H);
|
|
}
|
|
//--- Test body area hit in body-relative coordinates
|
|
int bLx = mouseX - rcdPanelX;
|
|
int bLy = mouseY - rcdBodyY;
|
|
rcdMouseInBody = RcdPointInRect(bLx, bLy, 0, 0, rcdPanelWidth, rcdBodyHeight);
|
|
//--- Test scrollbar slider hit area when scroll is visible
|
|
rcdHoverSlider = false;
|
|
if(rcdScrollVisible && rcdMouseInBody)
|
|
{
|
|
int pillMargin = 4;
|
|
int pillX = rcdPanelWidth - RCD_SB_MARGIN_R - RCD_SB_PILL_W;
|
|
int thumbY = pillMargin;
|
|
if(rcdMaxScroll > 0)
|
|
thumbY = pillMargin + (int)(((double)rcdScrollPos / rcdMaxScroll) * (rcdBodyHeight - pillMargin*2 - rcdSliderHeight));
|
|
thumbY = MathMax(pillMargin, MathMin(rcdBodyHeight - rcdSliderHeight - pillMargin, thumbY));
|
|
//--- Widen the hit area by 4 pixels on each side for easier grabbing
|
|
rcdHoverSlider = RcdPointInRect(bLx, bLy, pillX-4, thumbY, RCD_SB_PILL_W+8, rcdSliderHeight);
|
|
}
|
|
//--- Test footer interactive elements in footer-relative coordinates
|
|
int fLx = mouseX - rcdPanelX;
|
|
int fLy = mouseY - rcdFooterY;
|
|
int cbX = RCD_PAD/2 + 4;
|
|
int cbY = (RCD_FOOTER_H - RCD_CHECKBOX_SIZE) / 2;
|
|
//--- Checkbox hit includes the label text width
|
|
rcdHoverCheckbox = RcdPointInRect(fLx, fLy, cbX, cbY, RCD_CHECKBOX_SIZE + 6 + 110, RCD_CHECKBOX_SIZE);
|
|
int fPad = 10;
|
|
int okX = rcdPanelWidth - fPad - RCD_BUTTON_W;
|
|
int okY = (RCD_FOOTER_H - RCD_BUTTON_H) / 2;
|
|
rcdHoverOK = RcdPointInRect(fLx, fLy, okX, okY, RCD_BUTTON_W, RCD_BUTTON_H);
|
|
rcdHoverCancel = RcdPointInRect(fLx, fLy, okX - RCD_BUTTON_W - fPad, okY, RCD_BUTTON_W, RCD_BUTTON_H);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Show document — initialise state, canvases, and render |
|
|
//+------------------------------------------------------------------+
|
|
void RcdShow()
|
|
{
|
|
//--- Prevent double-initialisation
|
|
if(rcdIsActive) return;
|
|
//--- Apply theme colours 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();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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();
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert deinitialization function |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
//--- Destroy all canvases and release all resources cleanly
|
|
RcdHide();
|
|
Print("Rich Content Document: Deinitialized cleanly.");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert tick function |
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
//--- We left this empty intentionary — it is a pure documentation renderer, but you can add your logic here
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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);
|
|
}
|
|
//+------------------------------------------------------------------+ |