1043 lines
52 KiB
MQL5
1043 lines
52 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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 logo, 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
|