Article-22597-MQL5-Economic.../News Render.mqh

1043 lines
52 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| News Render.mqh |
//| 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"
//--- Include guard
#ifndef NEWS_RENDER_MQH
#define NEWS_RENDER_MQH
//--- Include core data definitions and state
#include "News Core.mqh"
//--- Include event loading and filter logic
#include "News Logic.mqh"
//+------------------------------------------------------------------+
//| Global Canvas Instances |
//+------------------------------------------------------------------+
CCanvas g_news_canv; // Main dashboard canvas
CCanvas g_news_canvSep; // Separator lines overlay canvas (separate chart object)
bool g_news_canvSepExists = false; // Separator canvas created flag
#define NEWS_CANVAS_NAME_SEP "NewsCanvasSeparators" // Separator canvas object name
//+------------------------------------------------------------------+
//| Header Button Rectangle Caches |
//+------------------------------------------------------------------+
int g_news_closeL = 0, g_news_closeT = 0, g_news_closeR = 0, g_news_closeB = 0; // Close button bounds
int g_news_themeL = 0, g_news_themeT = 0, g_news_themeR = 0, g_news_themeB = 0; // Theme button bounds
//+------------------------------------------------------------------+
//| Filter Toggle Rectangle Caches |
//+------------------------------------------------------------------+
int g_news_currTglL = 0, g_news_currTglT = 0, g_news_currTglR = 0, g_news_currTglB = 0; // Currency toggle bounds
int g_news_impTglL = 0, g_news_impTglT = 0, g_news_impTglR = 0, g_news_impTglB = 0; // Impact toggle bounds
int g_news_timeTglL = 0, g_news_timeTglT = 0, g_news_timeTglR = 0, g_news_timeTglB = 0; // Time toggle bounds
//+------------------------------------------------------------------+
//| Currency Chip Rectangle Caches |
//+------------------------------------------------------------------+
int g_news_currL[NEWS_CURR_COUNT]; // Currency chip left bounds
int g_news_currT[NEWS_CURR_COUNT]; // Currency chip top bounds
int g_news_currR[NEWS_CURR_COUNT]; // Currency chip right bounds
int g_news_currB[NEWS_CURR_COUNT]; // Currency chip bottom bounds
//+------------------------------------------------------------------+
//| Impact Chip Rectangle Caches |
//+------------------------------------------------------------------+
int g_news_impL[NEWS_IMPACT_COUNT]; // Impact chip left bounds
int g_news_impT[NEWS_IMPACT_COUNT]; // Impact chip top bounds
int g_news_impR[NEWS_IMPACT_COUNT]; // Impact chip right bounds
int g_news_impB[NEWS_IMPACT_COUNT]; // Impact chip bottom bounds
//+------------------------------------------------------------------+
//| Event Row Rectangle Caches |
//+------------------------------------------------------------------+
//--- Sized for max dashboard height (~900px yields about 25 rows); keep headroom
#define NEWS_MAX_VISIBLE_ROWS 32
int g_news_rowL[NEWS_MAX_VISIBLE_ROWS]; // Row left bounds
int g_news_rowT[NEWS_MAX_VISIBLE_ROWS]; // Row top bounds
int g_news_rowR[NEWS_MAX_VISIBLE_ROWS]; // Row right bounds
int g_news_rowB[NEWS_MAX_VISIBLE_ROWS]; // Row bottom bounds
int g_news_rowEventIdx[NEWS_MAX_VISIBLE_ROWS]; // Row index into displayableEvents
int g_news_visibleRowCount = 0; // Count of currently visible rows
//+------------------------------------------------------------------+
//| Revised Triangle Hot Zone Caches |
//+------------------------------------------------------------------+
//--- cx == -1 means the row has no revised marker; interact layer sets the MT5 tooltip
int g_news_revTriCx[NEWS_MAX_VISIBLE_ROWS]; // Triangle center X per row (-1 if none)
int g_news_revTriCy[NEWS_MAX_VISIBLE_ROWS]; // Triangle center Y per row
int g_news_revisedHoverRow = -1; // Visible row index currently hovered (-1 if none)
//+------------------------------------------------------------------+
//| Cursor Position State |
//+------------------------------------------------------------------+
//--- Updated by interact layer on every mouse move; used to float resize handles
int g_news_cursorX = -1; // Last cursor X in dashboard-local coordinates
int g_news_cursorY = -1; // Last cursor Y in dashboard-local coordinates
//+------------------------------------------------------------------+
//| Remain Cell Fast-Path Cache |
//+------------------------------------------------------------------+
//--- Lets the timer repaint only cells whose displayed string changed
int g_news_remainCellL[NEWS_MAX_VISIBLE_ROWS]; // Remain cell left edge
int g_news_remainCellT[NEWS_MAX_VISIBLE_ROWS]; // Remain cell top edge
int g_news_remainCellW[NEWS_MAX_VISIBLE_ROWS]; // Remain cell width
int g_news_remainCellH[NEWS_MAX_VISIBLE_ROWS]; // Remain cell height
color g_news_remainCellBg[NEWS_MAX_VISIBLE_ROWS]; // Remain cell row background for repaint
string g_news_remainLastStr[NEWS_MAX_VISIBLE_ROWS]; // Last displayed remain string
datetime g_news_remainEvTime[NEWS_MAX_VISIBLE_ROWS]; // Event datetime driving recompute
//+------------------------------------------------------------------+
//| HR Drawing Canvas Instances |
//+------------------------------------------------------------------+
CCanvas g_news_hrFillCanv; // HR fill offscreen canvas
bool g_news_hrFillReady = false; // HR fill canvas ready flag
int g_news_hrFillW = 0; // HR fill canvas width
int g_news_hrFillH = 0; // HR fill canvas height
CCanvas g_news_hrBordCanv; // HR border offscreen canvas
bool g_news_hrBordReady = false; // HR border canvas ready flag
int g_news_hrBordW = 0; // HR border canvas width
int g_news_hrBordH = 0; // HR border canvas height
//+------------------------------------------------------------------+
//| Footer and Padding Constants |
//+------------------------------------------------------------------+
#define NEWS_FOOTER_H 26 // Countdown banner height in pixels
#define NEWS_FOOTER_PAD 8 // Gap between row area bottom and footer top
//+------------------------------------------------------------------+
//| Compute table section vertical positions |
//+------------------------------------------------------------------+
int News_TableTop()
{
//--- Sum all fixed-height bands above the table
return NEWS_HEADER_H + NEWS_FILTER_H + NEWS_CURR_ROW_H + NEWS_IMPACT_ROW_H + NEWS_VERT_GAP;
}
//+------------------------------------------------------------------+
//| Return table header top Y coordinate |
//+------------------------------------------------------------------+
int News_TableHeaderTop()
{
//--- Table header starts at the same Y as the table top
return News_TableTop();
}
//+------------------------------------------------------------------+
//| Return table rows viewport top Y coordinate |
//+------------------------------------------------------------------+
int News_TableRowsTop()
{
//--- Rows start below the table column header
return News_TableTop() + NEWS_TABLE_HDR_H;
}
//+------------------------------------------------------------------+
//| Return table rows viewport bottom Y coordinate |
//+------------------------------------------------------------------+
int News_TableRowsBottom()
{
//--- Viewport extends to the footer top with no whole-row snap
const int footerTop = NEWS_DASHBOARD_H - NEWS_FOOTER_H - NEWS_FOOTER_PAD;
const int rowsTop = News_TableRowsTop();
//--- Guard against pathologically small dashboard heights
if(footerTop <= rowsTop + 1) return rowsTop + NEWS_ROW_H;
return footerTop;
}
//+------------------------------------------------------------------+
//| Return count of fully visible rows in the viewport |
//+------------------------------------------------------------------+
int News_VisibleRowCount()
{
//--- Compute how many whole rows fit in the available viewport height
const int rowsTop = News_TableRowsTop();
const int rowsBot = News_TableRowsBottom();
return MathMax(1, (rowsBot - rowsTop) / NEWS_ROW_H);
}
//+------------------------------------------------------------------+
//| Stamp normal-weight text onto the main canvas |
//+------------------------------------------------------------------+
void News_StampTextAA_Wrapper(const string txt, int x, int y, string font, int size, color clr)
{
//--- Delegate to shared News_StampText helper
News_StampText(g_news_canv, x, y, txt, font, size, clr);
}
//+------------------------------------------------------------------+
//| Render dashboard header with title, server time, buttons |
//+------------------------------------------------------------------+
void News_RenderHeader()
{
//--- Fill header strip with flat background rectangle
g_news_canv.FillRectangle(0, 0, NEWS_DASHBOARD_W - 1, NEWS_HEADER_H - 1,
ColorToARGB(g_news_headerBg, 255));
//--- Compute button dimensions anchored to the right edge
const int btnH = NEWS_HEADER_H - 7;
const int themeW = btnH;
const int closeW = (int)(btnH * 1.5);
//--- Set close button rect (rightmost, flush to dashboard right edge)
g_news_closeR = NEWS_DASHBOARD_W;
g_news_closeL = g_news_closeR - closeW;
g_news_closeT = 0;
g_news_closeB = g_news_closeT + btnH;
//--- Set theme toggle rect (immediately left of close button)
g_news_themeR = g_news_closeL;
g_news_themeL = g_news_themeR - themeW;
g_news_themeT = 0;
g_news_themeB = btnH;
//--- Compute title bounds and available width for truncation
const string titleFull = "MQL5 Economic Calendar";
const int titleStartX = NEWS_SIDE_PAD;
const int midGap = 12;
const int rightAreaL = g_news_themeL - 4;
const int titleMaxW = (rightAreaL - titleStartX) / 2 - midGap;
const int titleH = News_TextHeight("Arial Bold", NEWS_FONT_TITLE);
const int titleY = (NEWS_HEADER_H - titleH) / 2;
//--- Stamp title with ellipsis fallback if space is tight
const string titleFit = News_FitTextToWidth(titleFull, "Arial Bold", NEWS_FONT_TITLE, MathMax(0, titleMaxW));
News_StampTextAA_Wrapper(titleFit, titleStartX, titleY, "Arial Bold", NEWS_FONT_TITLE, g_news_titleText);
const int titleEndX = titleStartX + News_TextWidth(titleFit, "Arial Bold", NEWS_FONT_TITLE);
//--- Render middle status text (server time + event counts) when there is room
const int midAvailL = titleEndX + midGap;
const int midAvailR = rightAreaL - 4;
const int midAvailW = midAvailR - midAvailL;
if(midAvailW > 40)
{
//--- Build status string and center it in the available band
string countsStr = "Total: " + IntegerToString(g_news_totalFiltered) + "/" + IntegerToString(g_news_totalConsidered);
string serverStr = "Server: " + TimeToString(TimeCurrent(), TIME_DATE | TIME_SECONDS);
string mid = serverStr + " | " + countsStr;
string midFit = News_FitTextToWidth(mid, "Arial", NEWS_FONT_HEADING, midAvailW);
const int midW = News_TextWidth(midFit, "Arial", NEWS_FONT_HEADING);
const int midX = midAvailL + (midAvailW - midW) / 2;
const int midY = (NEWS_HEADER_H - News_TextHeight("Arial", NEWS_FONT_HEADING)) / 2;
News_StampTextAA_Wrapper(midFit, midX, midY, "Arial", NEWS_FONT_HEADING, g_news_subText);
}
//--- Draw theme button hover fill
if(g_news_hover == NEWS_HOV_THEME)
{
const color hovBg = News_HoverForBg(g_news_headerBg);
g_news_canv.FillRectangle(g_news_themeL, g_news_themeT, g_news_themeR - 1, g_news_themeB - 1,
ColorToARGB(hovBg, 255));
}
//--- Stamp theme toggle glyph centered in its button area
const string themeGlyph = "\x5B";
const int thW = News_TextWidth(themeGlyph, "Wingdings", 14);
const int thH = News_TextHeight("Wingdings", 14);
News_StampTextAA_Wrapper(themeGlyph, g_news_themeL + (themeW - thW) / 2,
g_news_themeT + (btnH - thH) / 2, "Wingdings", 14, g_news_titleText);
//--- Draw close button hover fill and set foreground color
color cFg = (g_news_hover == NEWS_HOV_CLOSE) ? g_news_closeColorHover : g_news_closeColor;
if(g_news_hover == NEWS_HOV_CLOSE)
g_news_canv.FillRectangle(g_news_closeL, g_news_closeT, g_news_closeR - 1, g_news_closeB - 1,
ColorToARGB(g_news_closeBgHover, 255));
//--- Stamp close glyph centered in its button area
const int xW = News_TextWidth(NEWS_GLYPH_CLOSE, "Webdings", 14);
const int xH = News_TextHeight("Webdings", 14);
News_StampTextAA_Wrapper(NEWS_GLYPH_CLOSE, g_news_closeL + (closeW - xW) / 2,
g_news_closeT + (btnH - xH) / 2, "Webdings", 14, cFg);
//--- Draw header bottom border line
g_news_canv.LineHorizontal(0, NEWS_DASHBOARD_W - 1, NEWS_HEADER_H - 1,
ColorToARGB(g_news_border, 255));
}
//+------------------------------------------------------------------+
//| Render three filter master toggle buttons |
//+------------------------------------------------------------------+
void News_RenderFilterToggles()
{
const int rowY = NEWS_HEADER_H + 4;
const int btnH = NEWS_FILTER_H - 8;
const int gap = 6;
const int btnMaxW = 110;
const int btnMinW = 50;
//--- Stamp "Filters:" label on the left
const int lblW = News_TextWidth("Filters:", "Arial Bold", NEWS_FONT_HEADING);
News_StampTextAA_Wrapper("Filters:", NEWS_SIDE_PAD,
rowY + (btnH - News_TextHeight("Arial Bold", NEWS_FONT_HEADING)) / 2,
"Arial Bold", NEWS_FONT_HEADING, g_news_subText);
//--- Compute per-button width within available area, clamped to min/max
const int areaL = NEWS_SIDE_PAD + lblW + 8;
const int areaR = NEWS_DASHBOARD_W - NEWS_SIDE_PAD;
const int areaW = areaR - areaL;
int btnW = (areaW - 2 * gap) / 3;
if(btnW > btnMaxW) btnW = btnMaxW;
if(btnW < btnMinW) btnW = btnMinW;
//--- Right-align the three-button cluster within the available area
const int totalW = btnW * 3 + gap * 2;
int startX = areaR - totalW;
//--- Define per-button label sets and state flags
const string nounsOn[] = {"ON Currency", "ON Impact", "ON Time"};
const string nounsOff[] = {"OFF Currency", "OFF Impact", "OFF Time"};
const string shortOn[] = {"Currency", "Impact", "Time"};
const string shortOff[] = {"Currency", "Impact", "Time"};
const bool onFlags[] = {g_news_filterCurrencyOn, g_news_filterImpactOn, g_news_filterTimeOn};
const int hovCodes[] = {NEWS_HOV_FILTER_CURR, NEWS_HOV_FILTER_IMP, NEWS_HOV_FILTER_TIME};
//--- Render each of the three toggle buttons
for(int i = 0; i < 3; i++)
{
const int bL = startX;
const int bT = rowY;
const int bR = startX + btnW;
const int bB = rowY + btnH;
//--- Cache button rect for hit-tester
if(i == 0) { g_news_currTglL = bL; g_news_currTglT = bT; g_news_currTglR = bR; g_news_currTglB = bB; }
if(i == 1) { g_news_impTglL = bL; g_news_impTglT = bT; g_news_impTglR = bR; g_news_impTglB = bB; }
if(i == 2) { g_news_timeTglL = bL; g_news_timeTglT = bT; g_news_timeTglR = bR; g_news_timeTglB = bB; }
//--- Compute background and foreground colors based on on/hover state
color bg = onFlags[i] ? g_news_chipOnBg : g_news_chipOffBg;
color fg = onFlags[i] ? g_news_chipOnText : g_news_chipOffText;
if(g_news_hover == hovCodes[i]) bg = News_HoverForBg(bg);
//--- Draw button background and border
News_FillRoundRect(g_news_canv, bL, bT, btnW, btnH, 6, ColorToARGB(bg, 255));
News_DrawRoundRectBorder(g_news_canv, bL, bT, btnW, btnH, 6, 1,
ColorToARGB(News_BorderForBg(bg), 255));
//--- Select the best label that fits: full, short, then ellipsized
const string lblFull = onFlags[i] ? nounsOn[i] : nounsOff[i];
const string lblShort = onFlags[i] ? shortOn[i] : shortOff[i];
const int innerPad = 8;
const int avail = btnW - 2 * innerPad;
string lbl;
if(News_TextWidth(lblFull, "Arial Bold", NEWS_FONT_BUTTON) <= avail)
lbl = lblFull;
else if(News_TextWidth(lblShort, "Arial Bold", NEWS_FONT_BUTTON) <= avail)
lbl = lblShort;
else
lbl = News_FitTextToWidth(lblShort, "Arial Bold", NEWS_FONT_BUTTON, avail);
//--- Center label inside button
const int lW = News_TextWidth(lbl, "Arial Bold", NEWS_FONT_BUTTON);
const int lH = News_TextHeight("Arial Bold", NEWS_FONT_BUTTON);
News_StampTextAA_Wrapper(lbl, bL + (btnW - lW) / 2, bT + (btnH - lH) / 2,
"Arial Bold", NEWS_FONT_BUTTON, fg);
startX += btnW + gap;
}
}
//+------------------------------------------------------------------+
//| Render the 8 currency filter chips in a row |
//+------------------------------------------------------------------+
void News_RenderCurrencyChips()
{
const int rowY = NEWS_HEADER_H + NEWS_FILTER_H + 4;
const int chipH = NEWS_CURR_ROW_H - 8;
const int chipMaxW = 70;
const int chipMinW = 36;
const int gap = 6;
//--- Compute per-chip width within available area, clamped to min/max
const int areaW = NEWS_DASHBOARD_W - 2 * NEWS_SIDE_PAD;
int chipW = (areaW - gap * (NEWS_CURR_COUNT - 1)) / NEWS_CURR_COUNT;
if(chipW > chipMaxW) chipW = chipMaxW;
if(chipW < chipMinW) chipW = chipMinW;
//--- Center the chip row within available dashboard width
const int totalW = chipW * NEWS_CURR_COUNT + gap * (NEWS_CURR_COUNT - 1);
int startX = (NEWS_DASHBOARD_W - totalW) / 2;
//--- Render each currency chip
for(int i = 0; i < NEWS_CURR_COUNT; i++)
{
//--- Cache chip rect for hit-tester
g_news_currL[i] = startX;
g_news_currT[i] = rowY;
g_news_currR[i] = startX + chipW;
g_news_currB[i] = rowY + chipH;
//--- Compute chip colors based on selection and hover state
const bool sel = g_news_currSelected[i];
const bool hov = (g_news_hover == NEWS_HOV_CURR_BASE + i);
color bg = sel ? g_news_currOnBg : g_news_currOffBg;
if(hov) bg = sel ? News_HoverForBg(bg) : g_news_chipOffBg;
color fg = sel ? g_news_currOnText : g_news_currOffText;
//--- Draw chip background and border
News_FillRoundRect(g_news_canv, g_news_currL[i], g_news_currT[i], chipW, chipH, 6,
ColorToARGB(bg, 255));
News_DrawRoundRectBorder(g_news_canv, g_news_currL[i], g_news_currT[i], chipW, chipH, 6, 1,
ColorToARGB(News_BorderForBg(bg), 255));
//--- Truncate 3-letter code if chip is unusually narrow then center
const string lblFit = News_FitTextToWidth(NEWS_CURRENCIES[i], "Arial Bold", NEWS_FONT_BUTTON, chipW - 8);
const int lW = News_TextWidth(lblFit, "Arial Bold", NEWS_FONT_BUTTON);
const int lH = News_TextHeight("Arial Bold", NEWS_FONT_BUTTON);
News_StampTextAA_Wrapper(lblFit, g_news_currL[i] + (chipW - lW) / 2,
g_news_currT[i] + (chipH - lH) / 2, "Arial Bold", NEWS_FONT_BUTTON, fg);
startX += chipW + gap;
}
}
//+------------------------------------------------------------------+
//| Render the 4 impact filter chips in a row |
//+------------------------------------------------------------------+
void News_RenderImpactChips()
{
const int rowY = NEWS_HEADER_H + NEWS_FILTER_H + NEWS_CURR_ROW_H + 4;
const int chipH = NEWS_IMPACT_ROW_H - 8;
const int chipMaxW = 110;
const int chipMinW = 60;
const int gap = 6;
//--- Compute per-chip width within available area, clamped to min/max
const int areaW = NEWS_DASHBOARD_W - 2 * NEWS_SIDE_PAD;
int chipW = (areaW - gap * (NEWS_IMPACT_COUNT - 1)) / NEWS_IMPACT_COUNT;
if(chipW > chipMaxW) chipW = chipMaxW;
if(chipW < chipMinW) chipW = chipMinW;
//--- Center the chip row within available dashboard width
const int totalW = chipW * NEWS_IMPACT_COUNT + gap * (NEWS_IMPACT_COUNT - 1);
int startX = (NEWS_DASHBOARD_W - totalW) / 2;
//--- Pre-populate base semantic colors for each impact level
color baseColors[];
ArrayResize(baseColors, NEWS_IMPACT_COUNT);
baseColors[0] = g_news_impNone;
baseColors[1] = g_news_impLow;
baseColors[2] = g_news_impMed;
baseColors[3] = g_news_impHigh;
//--- Render each impact chip
for(int i = 0; i < NEWS_IMPACT_COUNT; i++)
{
//--- Cache chip rect for hit-tester
g_news_impL[i] = startX;
g_news_impT[i] = rowY;
g_news_impR[i] = startX + chipW;
g_news_impB[i] = rowY + chipH;
//--- Compute chip colors based on selection and hover state
const bool sel = g_news_impactSelected[i];
const bool hov = (g_news_hover == NEWS_HOV_IMP_BASE + i);
color baseClr = baseColors[i];
color bg = sel ? baseClr : g_news_currOffBg;
if(hov) bg = sel ? News_HoverForBg(bg) : g_news_chipOffBg;
color fg = sel ? clrWhite : g_news_currOffText;
if(i == 0 && sel) fg = clrWhite;
//--- Draw chip background and border
News_FillRoundRect(g_news_canv, g_news_impL[i], g_news_impT[i], chipW, chipH, 6,
ColorToARGB(bg, 255));
News_DrawRoundRectBorder(g_news_canv, g_news_impL[i], g_news_impT[i], chipW, chipH, 6, 1,
ColorToARGB(News_BorderForBg(bg), 255));
//--- Truncate label if chip is narrow then center
const string lblFit = News_FitTextToWidth(NEWS_IMPACT_LABELS[i], "Arial Bold", NEWS_FONT_BUTTON, chipW - 8);
const int lW = News_TextWidth(lblFit, "Arial Bold", NEWS_FONT_BUTTON);
const int lH = News_TextHeight("Arial Bold", NEWS_FONT_BUTTON);
News_StampTextAA_Wrapper(lblFit, g_news_impL[i] + (chipW - lW) / 2,
g_news_impT[i] + (chipH - lH) / 2, "Arial Bold", NEWS_FONT_BUTTON, fg);
startX += chipW + gap;
}
}
//+------------------------------------------------------------------+
//| Render table column header strip |
//+------------------------------------------------------------------+
void News_RenderTableHeader()
{
const int hT = News_TableHeaderTop();
const int hL = NEWS_SIDE_PAD;
const int hR = NEWS_DASHBOARD_W - NEWS_SIDE_PAD;
const int hW = hR - hL;
const int hH = NEWS_TABLE_HDR_H;
//--- Fill header with flat background; radius 0 aligns cleanly with rows below
News_FillRoundRect(g_news_canv, hL, hT, hW, hH, 0, ColorToARGB(g_news_tableHdrBg, 255));
//--- Stamp each column label truncated to fit its allocated width
int x = hL + 6;
for(int i = 0; i < NEWS_COL_COUNT; i++)
{
const int colW = g_news_colW[i];
const int tH = News_TextHeight("Arial Bold", NEWS_FONT_HEADING);
const int textY = hT + (hH - tH) / 2;
string label = News_FitTextToWidth(NEWS_COL_LABELS[i], "Arial Bold", NEWS_FONT_HEADING, colW - 8);
News_StampTextAA_Wrapper(label, x + 2, textY, "Arial Bold", NEWS_FONT_HEADING, g_news_tableHdrText);
x += colW;
}
}
//+------------------------------------------------------------------+
//| Render column separators onto the overlay canvas |
//+------------------------------------------------------------------+
void News_RenderSeparatorsOverlay()
{
//--- Skip if separator canvas was not created
if(!g_news_canvSepExists) return;
//--- Clear overlay to fully transparent
g_news_canvSep.Erase(0x00000000);
const int hT = News_TableHeaderTop();
const int hH = NEWS_TABLE_HDR_H;
//--- Inset separator lines slightly from header top and bottom edges
const int sepTopHdr = hT + 6;
const int sepBotHdr = hT + hH - 6;
//--- Blend border and subText halfway for a readable but subtle separator color
const uchar bR = (uchar)((g_news_border ) & 0xFF);
const uchar bG = (uchar)((g_news_border >> 8) & 0xFF);
const uchar bB = (uchar)((g_news_border >> 16) & 0xFF);
const uchar sR = (uchar)((g_news_subText ) & 0xFF);
const uchar sG = (uchar)((g_news_subText >> 8) & 0xFF);
const uchar sB = (uchar)((g_news_subText >> 16) & 0xFF);
const color sepCol = (color)((((uint)((bB + sB) / 2)) << 16) |
(((uint)((bG + sG) / 2)) << 8) |
((uint)((bR + sR) / 2)));
const uint sepArgb = ColorToARGB(sepCol, 255);
//--- Draw 1px separator after each column except the last
const int hL = NEWS_SIDE_PAD;
int x = hL + 6;
for(int i = 0; i < NEWS_COL_COUNT; i++)
{
const int colW = g_news_colW[i];
if(i < NEWS_COL_COUNT - 1)
{
const int sepX = x + colW - 1;
g_news_canvSep.FillRectangle(sepX, sepTopHdr, sepX, sepBotHdr, sepArgb);
}
x += colW;
}
//--- Push separator overlay to chart
g_news_canvSep.Update();
}
//+------------------------------------------------------------------+
//| Render visible event and day-separator rows with clipping |
//+------------------------------------------------------------------+
void News_RenderEventRows()
{
const int hL = NEWS_SIDE_PAD;
const int rowsTop = News_TableRowsTop();
const int rowsBot = News_TableRowsBottom();
const int totalRows = ArraySize(g_news_rowPlan);
//--- Determine whether the scrollbar will be shown and reserve right margin
const int viewportH = rowsBot - rowsTop;
const int contentH = totalRows * NEWS_ROW_H;
const bool scrollNeeded = (contentH > viewportH);
const int scrollReserve = scrollNeeded ? 12 : 0;
const int hR = NEWS_DASHBOARD_W - NEWS_SIDE_PAD - scrollReserve;
const int hW = hR - hL;
//--- Fill the rows viewport region with the panel background
News_FillRoundRect(g_news_canv, hL, rowsTop, hW, rowsBot - rowsTop, 0,
ColorToARGB(g_news_panelAlt, 255));
//--- Render empty-state message when no events match the current filters
if(totalRows == 0)
{
const string empty = "No events match the current filters.";
const int eW = News_TextWidth(empty, "Arial", NEWS_FONT_BODY);
const int eH = News_TextHeight("Arial", NEWS_FONT_BODY);
News_StampTextAA_Wrapper(empty, hL + (hW - eW) / 2,
rowsTop + ((rowsBot - rowsTop) - eH) / 2,
"Arial", NEWS_FONT_BODY, g_news_subText);
g_news_visibleRowCount = 0;
g_news_tableScroll.totalH = 0;
g_news_tableScroll.viewportH = rowsBot - rowsTop;
News_ScrollClamp(g_news_tableScroll);
return;
}
//--- Update scroll state dimensions and clamp offset
g_news_tableScroll.totalH = totalRows * NEWS_ROW_H;
g_news_tableScroll.viewportH = rowsBot - rowsTop;
News_ScrollClamp(g_news_tableScroll);
//--- Allocate or grow the persistent scratch canvas to main canvas dimensions
const int cw = g_news_canv.Width();
const int ch = g_news_canv.Height();
if(!g_news_tableTmpReady || g_news_tableTmpW < cw || g_news_tableTmpH < ch)
{
if(g_news_tableTmpReady) g_news_tableTmp.Destroy();
const int newW = MathMax(cw, g_news_tableTmpW);
const int newH = MathMax(ch, g_news_tableTmpH);
if(!g_news_tableTmp.CreateBitmap("NewsTableTmpPersistent", 0, 0, newW, newH,
COLOR_FORMAT_ARGB_NORMALIZE)) return;
g_news_tableTmpW = newW;
g_news_tableTmpH = newH;
g_news_tableTmpReady = true;
}
//--- Define the clip rectangle bounding the rows viewport
const int clipL = hL;
const int clipT = rowsTop;
const int clipR = NEWS_DASHBOARD_W - NEWS_SIDE_PAD;
const int clipB = rowsBot;
const int seedL = MathMax(clipL, 0);
const int seedT = MathMax(clipT, 0);
const int seedR = MathMin(clipR, cw);
const int seedB = MathMin(clipB, ch);
//--- Seed scratch canvas with current main pixels for the clip region
g_news_tableTmp.CopyRectFromCanvas(g_news_canv, seedL, seedT, seedR, seedB);
//--- Compute the first visible row index and its Y position
const int firstIdx = MathMax(0, g_news_tableScroll.scrollPx / NEWS_ROW_H - 1);
const int firstY = rowsTop + firstIdx * NEWS_ROW_H - g_news_tableScroll.scrollPx;
const int maxRows = MathMin(News_VisibleRowCount() + 3, totalRows - firstIdx);
g_news_visibleRowCount = 0;
//--- Render each row in the visible window onto the scratch canvas
for(int v = 0; v < maxRows; v++)
{
const int planIdx = firstIdx + v;
if(planIdx >= totalRows) break;
const int rT = firstY + v * NEWS_ROW_H;
const int rB = rT + NEWS_ROW_H;
//--- Skip rows that fall entirely outside the clip viewport
if(rB <= rowsTop || rT >= rowsBot) continue;
//--- Cache hit-test rect clamped to the visible viewport
const int hitT = MathMax(rT, rowsTop);
const int hitB = MathMin(rB, rowsBot);
g_news_rowL[g_news_visibleRowCount] = hL;
g_news_rowT[g_news_visibleRowCount] = hitT;
g_news_rowR[g_news_visibleRowCount] = hR;
g_news_rowB[g_news_visibleRowCount] = hitB;
g_news_rowEventIdx[g_news_visibleRowCount] = planIdx;
const NewsRowEntry entry = g_news_rowPlan[planIdx];
//--- Render day separator row
if(entry.kind == NEWS_ROW_KIND_DAY)
{
//--- Apply hover color when this separator row is hovered
const bool dayHov = (g_news_hover == NEWS_HOV_ROW_BASE + g_news_visibleRowCount);
const color dayBg = dayHov ? g_news_rowHover : g_news_dayHeaderBg;
const color dayText = dayHov ? g_news_rowText : g_news_dayHeaderText;
g_news_tableTmp.FillRectangle(hL, rT, hR - 1, rB - 1, ColorToARGB(dayBg, 255));
//--- Draw right-pointing chevron when collapsed, down-pointing when expanded
const bool collapsed = News_IsDayCollapsed(entry.dateKey);
const int chvCx = hL + 14;
const int chvCy = rT + NEWS_ROW_H / 2;
const int chvDir = collapsed ? NEWS_CHEVRON_RIGHT : NEWS_CHEVRON_DOWN;
News_DrawChevron(g_news_tableTmp, chvCx, chvCy, chvDir, ColorToARGB(dayText, 255));
//--- Center bold day label in the separator row
const int tH = News_TextHeight("Arial Bold", NEWS_FONT_HEADING);
const int tW = News_TextWidth(entry.label, "Arial Bold", NEWS_FONT_HEADING);
const int textY = rT + (NEWS_ROW_H - tH) / 2;
const int textX = hL + (hW - tW) / 2;
News_StampText(g_news_tableTmp, textX, textY, entry.label, "Arial Bold", NEWS_FONT_HEADING, dayText);
//--- Mark Remain cache slot inactive for this day row
if(g_news_visibleRowCount < NEWS_MAX_VISIBLE_ROWS)
{
g_news_remainEvTime[g_news_visibleRowCount] = 0;
g_news_remainLastStr[g_news_visibleRowCount] = "";
}
g_news_visibleRowCount++;
continue;
}
//--- Render event row
const int evIdx = entry.eventIdx;
if(evIdx < 0 || evIdx >= ArraySize(g_news_displayableEvents))
{
//--- Mark bad event slot inactive for the fast path
if(g_news_visibleRowCount < NEWS_MAX_VISIBLE_ROWS)
{
g_news_remainEvTime[g_news_visibleRowCount] = 0;
g_news_remainLastStr[g_news_visibleRowCount] = "";
}
g_news_visibleRowCount++;
continue;
}
const NewsEvent ev = g_news_displayableEvents[evIdx];
//--- Choose row background color based on hover and alternating pattern
const bool hov = (g_news_hover == NEWS_HOV_ROW_BASE + g_news_visibleRowCount);
color bg = hov ? g_news_rowHover : ((planIdx % 2 == 0) ? g_news_rowAlt : g_news_panelAlt);
g_news_tableTmp.FillRectangle(hL, rT, hR - 1, rB - 1, ColorToARGB(bg, 255));
//--- Determine actual-value directional color based on forecast comparison
color actualColor = g_news_rowText;
if(ev.hasActual && ev.hasForecast)
{
if(ev.actual > ev.forecast) actualColor = g_news_actualUp;
else if(ev.actual < ev.forecast) actualColor = g_news_actualDown;
}
//--- Build actual value string: blank for future unreleased, dash for past unreleased
const bool isFuture = (ev.eventDateTime > TimeCurrent());
string actualStr;
if(ev.hasActual) actualStr = News_FormatValue(true, ev.actual, ev.unit, ev.multiplier, ev.digits);
else if(isFuture) actualStr = "";
else actualStr = "-";
//--- Use revised previous value when available, otherwise use original previous
const bool prevHas = ev.hasRevised || ev.hasPrevious;
const double prevToShow = ev.hasRevised ? ev.revisedPrevious : ev.previous;
//--- Populate all nine column value strings
string vals[9];
vals[0] = ev.eventDate;
vals[1] = ev.eventTime;
vals[2] = ev.currency;
vals[3] = "";
vals[4] = ev.event;
vals[5] = actualStr;
vals[6] = News_FormatValue(ev.hasForecast, ev.forecast, ev.unit, ev.multiplier, ev.digits);
vals[7] = News_FormatValue(prevHas, prevToShow, ev.unit, ev.multiplier, ev.digits);
vals[8] = News_FormatRemain(ev.eventDateTime, TimeCurrent());
//--- Render each column cell
int x = hL + 6;
const int tH = News_TextHeight("Arial", NEWS_FONT_BODY);
const int textY = rT + (NEWS_ROW_H - tH) / 2;
for(int c = 0; c < NEWS_COL_COUNT; c++)
{
const int colW = g_news_colW[c];
if(c == 3)
{
//--- Draw impact dot as a filled anti-aliased circle
const color impClr = News_GetImpactColor(ev.importance);
const int dotR = 5;
const int dotX = x + colW / 2;
const int dotY = rT + NEWS_ROW_H / 2;
News_DrawFilledCircle(g_news_tableTmp, dotX, dotY, dotR, ColorToARGB(impClr, 255));
}
else
{
//--- Pick per-column text color
color cellColor = g_news_rowText;
if(c == 5)
cellColor = actualColor;
else if(c == 8)
{
//--- Remain column: subText for past, red for imminent, accent for future
const datetime now = TimeCurrent();
if(ev.eventDateTime < now) cellColor = g_news_subText;
else if(News_RemainIsImminent(ev.eventDateTime, now)) cellColor = g_news_remainSoon;
else cellColor = g_news_accent;
}
//--- Truncate cell text, reserving extra space on Previous column when revised marker is present
string txt = News_FitTextToWidth(vals[c], "Arial", NEWS_FONT_BODY,
colW - (c == 7 && ev.hasRevised ? 16 : 4));
News_StampText(g_news_tableTmp, x + 2, textY, txt, "Arial", NEWS_FONT_BODY, cellColor);
//--- Cache Remain cell geometry for the timer-tick fast path
if(c == 8 && g_news_visibleRowCount < NEWS_MAX_VISIBLE_ROWS)
{
g_news_remainCellL[g_news_visibleRowCount] = x;
g_news_remainCellT[g_news_visibleRowCount] = rT;
g_news_remainCellW[g_news_visibleRowCount] = colW;
g_news_remainCellH[g_news_visibleRowCount] = NEWS_ROW_H;
g_news_remainCellBg[g_news_visibleRowCount] = bg;
g_news_remainLastStr[g_news_visibleRowCount] = vals[c];
g_news_remainEvTime[g_news_visibleRowCount] = ev.eventDateTime;
}
//--- Draw revised-value gold triangle on Previous column when source revised it
if(c == 7 && ev.hasRevised)
{
const int tW = News_TextWidth(txt, "Arial", NEWS_FONT_BODY);
const int triCy = rT + NEWS_ROW_H / 2;
const int triCx = x + 2 + tW + 7;
News_DrawTriangle(g_news_tableTmp, triCx, triCy, NEWS_CHEVRON_LEFT,
ColorToARGB(g_news_revisedMark, 255));
//--- Record triangle center for revised-value tooltip hit-test
if(g_news_visibleRowCount < NEWS_MAX_VISIBLE_ROWS)
{
g_news_revTriCx[g_news_visibleRowCount] = triCx;
g_news_revTriCy[g_news_visibleRowCount] = triCy;
}
}
else if(c == 7)
{
//--- Clear triangle slot for rows with no revised marker
if(g_news_visibleRowCount < NEWS_MAX_VISIBLE_ROWS)
g_news_revTriCx[g_news_visibleRowCount] = -1;
}
}
x += colW;
}
g_news_visibleRowCount++;
}
//--- Copy clipped rows region from scratch canvas back to main canvas
g_news_tableTmp.CopyRectToCanvas(g_news_canv, seedL, seedT, seedR, seedB);
}
//+------------------------------------------------------------------+
//| Render scrollbar thumb for the events table |
//+------------------------------------------------------------------+
void News_RenderTableScrollbar()
{
const int rowsTop = News_TableRowsTop();
const int rowsBot = News_TableRowsBottom();
//--- Skip rendering when no scrollbar is needed
if(!News_ScrollVisible(g_news_tableScroll)) return;
//--- Position the 4px-wide thumb track at the right edge of the table
const int laneR = NEWS_DASHBOARD_W - NEWS_SIDE_PAD - 4;
const int laneL = laneR - 4;
g_news_tableScroll.trackL = laneL;
g_news_tableScroll.trackR = laneR;
g_news_tableScroll.trackT = rowsTop;
g_news_tableScroll.trackB = rowsBot;
News_ScrollDraw(g_news_canv, g_news_tableScroll);
}
//+------------------------------------------------------------------+
//| Render countdown and trade status banner at dashboard bottom |
//+------------------------------------------------------------------+
void News_RenderCountdown()
{
//--- Determine banner text, background, and foreground based on trade mode and state
string bannerText = "";
bool active = false;
color bg = g_news_panelAlt;
color fg = g_news_subText;
//--- Show disabled or paused message when auto-trading is off
if(tradeMode == NEWS_NO_TRADE || tradeMode == NEWS_PAUSE_TRADING)
{
bannerText = (tradeMode == NEWS_PAUSE_TRADING) ? "Auto-trading paused" : "Auto-trading disabled";
active = true;
}
else
{
string nextName = News_NextNewsName();
//--- Show post-release countdown when a trade was just executed
if(g_news_tradeExecuted && g_news_tradedNewsTime > 0)
{
const int elapsed = (int)(TimeCurrent() - g_news_tradedNewsTime);
if(elapsed < 0)
{
//--- Show active trading banner before news time
bannerText = "Trading: " + nextName;
bg = g_news_countdownBg;
fg = g_news_countdownText;
active = true;
}
else if(elapsed < 15)
{
//--- Show reset countdown banner for 15 seconds after news
bannerText = "News released - resetting in " + IntegerToString(15 - elapsed) + "s";
bg = g_news_countdownReleaseBg;
fg = g_news_countdownText;
active = true;
}
}
else if(StringLen(nextName) > 0)
{
//--- Show next eligible event name in the banner
bannerText = "Next eligible: " + nextName;
bg = g_news_countdownBg;
fg = g_news_countdownText;
active = true;
}
else
{
//--- No eligible event found; show waiting message
bannerText = "Awaiting next eligible event";
active = true;
}
}
if(!active) return;
//--- Compute footer banner bounds at the bottom of the dashboard
const int bH = 26;
const int bL = NEWS_SIDE_PAD;
const int bR = NEWS_DASHBOARD_W - NEWS_SIDE_PAD;
const int bW = bR - bL;
const int bY = NEWS_DASHBOARD_H - bH - 8;
//--- Draw banner background and center the text label
News_FillRoundRect(g_news_canv, bL, bY, bW, bH, 6, ColorToARGB(bg, 230));
const int tW = News_TextWidth(bannerText, "Arial Bold", NEWS_FONT_LABEL);
const int tH = News_TextHeight("Arial Bold", NEWS_FONT_LABEL);
News_StampTextAA_Wrapper(bannerText, bL + (bW - tW) / 2, bY + (bH - tH) / 2,
"Arial Bold", NEWS_FONT_LABEL, fg);
}
//+------------------------------------------------------------------+
//| Render toast notification with shrinking progress bar |
//+------------------------------------------------------------------+
void News_RenderToast()
{
//--- Skip when no toast is active or it has expired
if(StringLen(g_news_toastText) == 0) return;
const ulong now = GetTickCount64();
if(now > g_news_toastExpiryMs) return;
//--- Compute toast box dimensions based on text size
const int padX = 16;
const int padY = 8;
const string font = "Arial Bold";
const int fsz = 10;
const int textW = News_TextWidth(g_news_toastText, font, fsz);
const int textH = News_TextHeight(font, fsz);
const int barH = 2;
const int barGap = 6;
const int boxW = textW + 2 * padX;
const int boxH = textH + barGap + barH + 2 * padY;
const int boxL = (NEWS_DASHBOARD_W - boxW) / 2;
const int boxT = NEWS_HEADER_H + 6;
//--- Draw toast box with rounded background and border
News_FillRoundRect(g_news_canv, boxL, boxT, boxW, boxH, 8,
ColorToARGB(g_news_toastBg, 240));
News_DrawRoundRectBorder(g_news_canv, boxL, boxT, boxW, boxH, 8, 1,
ColorToARGB(g_news_toastBorder, 255));
//--- Stamp toast message text in success or error color
const color textCol = g_news_toastIsError ? g_news_toastError : g_news_toastSuccess;
News_StampTextAA_Wrapper(g_news_toastText, boxL + padX, boxT + padY, font, fsz, textCol);
//--- Compute progress bar fill ratio from remaining lifetime
const int trackL = boxL + padX;
const int trackR = boxL + boxW - padX;
const int trackW = trackR - trackL;
const int trackY = boxT + padY + textH + barGap;
const long remaining = (long)g_news_toastExpiryMs - (long)now;
double ratio = (double)remaining / 5000.0;
if(ratio < 0.0) ratio = 0.0;
if(ratio > 1.0) ratio = 1.0;
const int fillW = (int)(trackW * ratio);
const int fillL = trackL + (trackW - fillW) / 2;
//--- Draw full track bar then overlay the shrinking fill from center
g_news_canv.FillRectangle(trackL, trackY, trackR - 1, trackY + barH - 1,
ColorToARGB(g_news_toastBorder, 255));
if(fillW > 0)
g_news_canv.FillRectangle(fillL, trackY, fillL + fillW - 1, trackY + barH - 1,
ColorToARGB(textCol, 255));
}
//+------------------------------------------------------------------+
//| Fast-path timer tick: refresh only changed Remain cells |
//+------------------------------------------------------------------+
bool News_TickRemainCells()
{
//--- Skip if canvas does not exist or no rows are visible
if(!g_news_canvasExists) return false;
if(g_news_visibleRowCount <= 0) return false;
//--- Establish viewport clip bounds to prevent painting outside the table area
const int vpTop = News_TableRowsTop();
const int vpBottom = News_TableRowsBottom();
const datetime now = TimeCurrent();
bool any = false;
//--- Process each visible row's Remain cell
for(int r = 0; r < g_news_visibleRowCount && r < NEWS_MAX_VISIBLE_ROWS; r++)
{
//--- Skip day separator slots and invalid event slots
if(g_news_remainEvTime[r] == 0) continue;
const string newStr = News_FormatRemain(g_news_remainEvTime[r], now);
if(newStr == g_news_remainLastStr[r]) continue;
//--- Clip cell rect vertically to the table viewport
const int cL = g_news_remainCellL[r];
int cT = g_news_remainCellT[r];
const int cW = g_news_remainCellW[r];
int cH = g_news_remainCellH[r];
int cB = cT + cH;
if(cT < vpTop) cT = vpTop;
if(cB > vpBottom) cB = vpBottom;
if(cB - cT <= 0)
{
//--- Cell is fully outside viewport; update string to suppress retry
g_news_remainLastStr[r] = newStr;
continue;
}
cH = cB - cT;
//--- Repaint cell background and stamp the updated remain string
g_news_canv.FillRectangle(cL, cT, cL + cW - 1, cB - 1,
ColorToARGB(g_news_remainCellBg[r], 255));
//--- Select same color logic as the full render
color cellColor;
if(g_news_remainEvTime[r] < now) cellColor = g_news_subText;
else if(News_RemainIsImminent(g_news_remainEvTime[r], now)) cellColor = g_news_remainSoon;
else cellColor = g_news_accent;
const string txt = News_FitTextToWidth(newStr, "Arial", NEWS_FONT_BODY, cW - 4);
const int tH = News_TextHeight("Arial", NEWS_FONT_BODY);
//--- Center text on the full row height (partial rows render the same partial text as full render)
const int textY = g_news_remainCellT[r] + (g_news_remainCellH[r] - tH) / 2;
//--- Only stamp when text baseline falls within the clipped viewport band
if(textY >= vpTop && textY + tH <= vpBottom)
News_StampTextAA_Wrapper(txt, cL + 2, textY, "Arial", NEWS_FONT_BODY, cellColor);
g_news_remainLastStr[r] = newStr;
any = true;
}
//--- Push updated canvas to chart only when at least one cell changed
if(any) g_news_canv.Update();
return any;
}
//+------------------------------------------------------------------+
//| Render entire dashboard from scratch |
//+------------------------------------------------------------------+
void News_RenderAll()
{
//--- Skip if main canvas has not been created
if(!g_news_canvasExists) return;
//--- Recompute column widths from current dashboard width
News_ComputeColumnWidths();
const int dW = NEWS_DASHBOARD_W;
const int dH = NEWS_DASHBOARD_H;
const int crClear = 8;
//--- Erase canvas and fill rounded background shape
g_news_canv.Erase(0x00000000);
g_news_prim.FillRoundRectSharp(g_news_canv, 0, 0, dW, dH, crClear,
ColorToARGB(g_news_bg, 255), 4);
//--- Render all interior sections in z-order
News_RenderHeader();
News_RenderCountdown();
News_RenderFilterToggles();
News_RenderCurrencyChips();
News_RenderImpactChips();
News_RenderTableHeader();
News_RenderEventRows();
News_RenderTableScrollbar();
//--- Render toast overlay on top of all content
News_RenderToast();
//--- Draw right-edge resize handle floating at cursor Y position
const bool resizeHov = (g_news_hover == NEWS_HOV_RESIZE_R) || g_news_resizing;
if(resizeHov)
{
const int gripperW = 3;
const int gripperH = NEWS_RESIZE_HANDLE_H;
const int gripperX = dW - 4;
const int safeMin = 8 + 4 + gripperH / 2;
const int safeMax = dH - 8 - 4 - gripperH / 2;
int cy = g_news_cursorY;
if(cy < safeMin) cy = safeMin;
if(cy > safeMax) cy = safeMax;
const int gripperT = cy - gripperH / 2;
const color gripCol = g_news_resizing ? g_news_accent : g_news_borderAccent;
News_FillRoundRect(g_news_canv, gripperX, gripperT, gripperW, gripperH,
gripperW / 2, ColorToARGB(gripCol, 220));
}
//--- Draw bottom-edge resize handle floating at cursor X position
const bool resizeHovV = (g_news_hover == NEWS_HOV_RESIZE_B) || g_news_resizingV;
if(resizeHovV)
{
const int gripperH = 3;
const int gripperW = NEWS_RESIZE_HANDLE_W;
const int gripperY = dH - 4;
const int safeMin = 8 + 4 + gripperW / 2;
const int safeMax = dW - 8 - 4 - gripperW / 2;
int cx = g_news_cursorX;
if(cx < safeMin) cx = safeMin;
if(cx > safeMax) cx = safeMax;
const int gripperL = cx - gripperW / 2;
const color gripColV = g_news_resizingV ? g_news_accent : g_news_borderAccent;
News_FillRoundRect(g_news_canv, gripperL, gripperY, gripperW, gripperH,
gripperH / 2, ColorToARGB(gripColV, 220));
}
//--- Build or reuse the cached corner-clear pixel mask
static int s_cornerClearX[];
static int s_cornerClearY[];
static int s_cornerClearN = 0;
static int s_cornerClearW = -1;
static int s_cornerClearH = -1;
if(s_cornerClearW != dW || s_cornerClearH != dH)
{
//--- Pre-size at worst-case capacity to avoid per-pixel ArrayResize
const double rdC = (double)crClear;
const int cap = 4 * (crClear + 1) * (crClear + 1);
ArrayResize(s_cornerClearX, cap);
ArrayResize(s_cornerClearY, cap);
int n = 0;
//--- Walk each of the four corners and collect pixels outside the arc
for(int corner = 0; corner < 4; corner++)
{
int cxC = (corner == 0 || corner == 2) ? crClear : (dW - 1 - crClear);
int cyC = (corner == 0 || corner == 1) ? crClear : (dH - 1 - crClear);
int xMin = (corner == 0 || corner == 2) ? 0 : (dW - crClear - 1);
int xMax = (corner == 0 || corner == 2) ? crClear : (dW - 1);
int yMin = (corner == 0 || corner == 1) ? 0 : (dH - crClear - 1);
int yMax = (corner == 0 || corner == 1) ? crClear : (dH - 1);
for(int yy = yMin; yy <= yMax; yy++)
{
for(int xx = xMin; xx <= xMax; xx++)
{
if(xx < 0 || xx >= dW || yy < 0 || yy >= dH) continue;
const double ddx = (double)(xx - cxC);
const double ddy = (double)(yy - cyC);
const double dist = MathSqrt(ddx * ddx + ddy * ddy);
if(dist > rdC + 0.5)
{
s_cornerClearX[n] = xx;
s_cornerClearY[n] = yy;
n++;
}
}
}
}
s_cornerClearN = n;
s_cornerClearW = dW;
s_cornerClearH = dH;
}
//--- Zero out all corner pixels outside the rounded radius
for(int cci = 0; cci < s_cornerClearN; cci++)
g_news_canv.PixelSet(s_cornerClearX[cci], s_cornerClearY[cci], 0x00000000);
//--- Draw outer rounded border after corner clear so arcs align precisely
const uint borderArgb = ColorToARGB(g_news_borderAccent, 255);
g_news_prim.DrawRoundRectBorderObStyle(g_news_canv, 0, 0, dW, dH, crClear, borderArgb);
//--- Push main canvas to chart
g_news_canv.Update();
//--- Render column separators on the overlay canvas
News_RenderSeparatorsOverlay();
}
#endif // NEWS_RENDER_MQH