1043 lines
No EOL
52 KiB
MQL5
1043 lines
No EOL
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 |