Article-22608-News-Calendar.../News Core.mqh

1683 lines
No EOL
77 KiB
MQL5

//+------------------------------------------------------------------+
//| News Core.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_CORE_MQH
#define NEWS_CORE_MQH
//--- Include canvas library
#include <Canvas/Canvas.mqh>
//+------------------------------------------------------------------+
//| Layout Constants |
//+------------------------------------------------------------------+
//--- Runtime dashboard width driven by g_news_dashW; bounds prevent unusable layouts
#define NEWS_DASHBOARD_W_DEFAULT 700 // Default starting width
#define NEWS_DASHBOARD_W_MIN 400 // Minimum allowed width
#define NEWS_DASHBOARD_W_MAX 1400 // Maximum allowed width
//--- Vertical bounds; flexible section is the events table
#define NEWS_DASHBOARD_H_DEFAULT 420 // Default starting height
#define NEWS_DASHBOARD_H_MIN 320 // Minimum height for at least 3 rows
#define NEWS_DASHBOARD_H_MAX 900 // Maximum allowed height
#define NEWS_HEADER_H 40 // Header strip height
#define NEWS_FILTER_H 36 // Filter toggle row height
#define NEWS_CURR_ROW_H 32 // Currency chip row height
#define NEWS_IMPACT_ROW_H 32 // Impact pill row height
#define NEWS_TABLE_HDR_H 28 // Table header row height
#define NEWS_ROW_H 26 // Event row height
#define NEWS_VISIBLE_ROWS 8 // Default visible row count (computed at runtime)
#define NEWS_SIDE_PAD 10 // Horizontal padding inside dashboard
#define NEWS_VERT_GAP 6 // Vertical gap between sections
#define NEWS_RESIZE_HOT_W 6 // Right-edge resize hot zone width
#define NEWS_RESIZE_HOT_H 6 // Bottom-edge resize hot zone height
#define NEWS_RESIZE_HANDLE_H 30 // Visible right-edge handle length (centered)
#define NEWS_RESIZE_HANDLE_W 30 // Visible bottom-edge handle length (centered)
//+------------------------------------------------------------------+
//| Runtime Dashboard Size and Resize State |
//+------------------------------------------------------------------+
int g_news_dashW = NEWS_DASHBOARD_W_DEFAULT; // Current dashboard width
int g_news_dashH = NEWS_DASHBOARD_H_DEFAULT; // Current dashboard height
bool g_news_resizing = false; // Right-edge drag active flag
int g_news_resizeStartMouseX = 0; // Mouse X at resize start
int g_news_resizeStartW = 0; // Dashboard width at resize start
bool g_news_resizingV = false; // Bottom-edge drag active flag
int g_news_resizeStartMouseY = 0; // Mouse Y at resize start
int g_news_resizeStartH = 0; // Dashboard height at resize start
//--- Backwards-compatibility aliases
#define NEWS_DASHBOARD_W g_news_dashW
#define NEWS_DASHBOARD_H g_news_dashH
//+------------------------------------------------------------------+
//| Table Column Reference Widths |
//+------------------------------------------------------------------+
//--- Reference widths at default dashboard width 700; all columns scale proportionally
const int NEWS_COL_W_REF[] = {68, 48, 42, 28, 215, 52, 60, 60, 70};
const string NEWS_COL_LABELS[] = {"Date", "Time", "Cur.", "Imp.", "Event", "Actual", "Forecast", "Previous", "Remain"};
#define NEWS_COL_COUNT 9
//--- Actual current column widths after proportional scaling; filled by News_ComputeColumnWidths
int g_news_colW[NEWS_COL_COUNT];
//+------------------------------------------------------------------+
//| Font Size Constants |
//+------------------------------------------------------------------+
#define NEWS_FONT_TITLE 12 // Title text size
#define NEWS_FONT_HEADING 10 // Heading text size
#define NEWS_FONT_BODY 9 // Body text size
#define NEWS_FONT_LABEL 9 // Label text size
#define NEWS_FONT_BUTTON 9 // Button text size
#define NEWS_FONT_TIMESTAMP 8 // Timestamp text size
//+------------------------------------------------------------------+
//| Glyph Codes |
//+------------------------------------------------------------------+
#define NEWS_GLYPH_CLOSE "r" // Webdings X close glyph
#define NEWS_GLYPH_CHECK "\x6FC" // Wingdings checkmark glyph
#define NEWS_GLYPH_CROSS "\x71B" // Wingdings X mark glyph
#define NEWS_GLYPH_DOT "l" // Wingdings filled dot glyph
#define NEWS_GLYPH_ARROW_UP "5" // Webdings up arrow glyph
#define NEWS_GLYPH_ARROW_DOWN "6" // Webdings down arrow glyph
//+------------------------------------------------------------------+
//| Canvas Object Names |
//+------------------------------------------------------------------+
#define NEWS_CANVAS_NAME "NewsCanvasMain" // Main canvas object name
//+------------------------------------------------------------------+
//| Theme Color Globals |
//+------------------------------------------------------------------+
color g_news_bg; // Dashboard background
color g_news_panelAlt; // Alternate panel color
color g_news_headerBg; // Header strip background
color g_news_border; // Subtle border color
color g_news_borderAccent; // Strong outer border color
color g_news_titleText; // Primary text color
color g_news_subText; // Secondary text color
color g_news_accent; // Accent blue color
//--- Filter chip colors
color g_news_chipOnBg; // Active chip background
color g_news_chipOnText; // Active chip text
color g_news_chipOffBg; // Inactive chip background
color g_news_chipOffText; // Inactive chip text
color g_news_chipHoverTint; // Hover overlay tint
//--- Currency chip colors
color g_news_currOnBg; // Selected currency background
color g_news_currOnText; // Selected currency text
color g_news_currOffBg; // Unselected currency background
color g_news_currOffText; // Unselected currency text
//--- Impact pill colors (semantic; same in both themes)
color g_news_impNone; // Impact none (gray)
color g_news_impLow; // Impact low (yellow)
color g_news_impMed; // Impact medium (orange)
color g_news_impHigh; // Impact high (red)
//--- Table colors
color g_news_tableHdrBg; // Table header strip background
color g_news_tableHdrText; // Table header text
color g_news_rowAlt; // Alternating row background
color g_news_rowText; // Row text color
color g_news_rowHover; // Row hover background
//--- Actual value direction colors
color g_news_actualUp; // Actual beat forecast (green)
color g_news_actualDown; // Actual missed forecast (red)
color g_news_revisedMark; // Revised previous indicator (gold)
color g_news_remainSoon; // Remain color when event is within 30 min
//--- Day separator row colors
color g_news_dayHeaderBg; // Day separator background
color g_news_dayHeaderText; // Day separator text
//--- Close button colors
color g_news_closeColor; // Close glyph idle color
color g_news_closeColorHover; // Close glyph hover color
color g_news_closeBgHover; // Close button hover background
//--- Toast colors
color g_news_toastBg; // Toast box background
color g_news_toastBorder; // Toast box border
color g_news_toastSuccess; // Toast success text
color g_news_toastError; // Toast error text
//--- Countdown banner colors
color g_news_countdownBg; // Active countdown background
color g_news_countdownReleaseBg; // Released countdown background
color g_news_countdownText; // Countdown text color
//--- Scrollbar thumb colors
color g_news_scrollSlider; // Idle thumb color
color g_news_scrollSliderHover; // Hover thumb color
color g_news_scrollSliderDrag; // Dragging thumb color
//--- Theme state
bool g_news_darkTheme = true; // Current theme flag
//+------------------------------------------------------------------+
//| Apply theme palette - dark or light |
//+------------------------------------------------------------------+
void News_ApplyTheme(bool dark)
{
//--- Store current theme flag
g_news_darkTheme = dark;
//--- Branch on theme mode
if(dark)
{
//--- Apply dark theme: charcoal with blue accent
g_news_bg = (color)C'24,26,32';
g_news_panelAlt = (color)C'30,33,40';
g_news_headerBg = (color)C'18,20,24';
g_news_border = (color)C'48,52,60';
g_news_borderAccent = (color)C'72,78,90';
g_news_titleText = (color)C'232,234,240';
g_news_subText = (color)C'148,152,162';
g_news_accent = (color)C'68,138,255';
g_news_chipOnBg = (color)C'68,138,255';
g_news_chipOnText = (color)C'255,255,255';
g_news_chipOffBg = (color)C'40,44,52';
g_news_chipOffText = (color)C'180,184,194';
g_news_chipHoverTint = (color)C'255,255,255';
g_news_currOnBg = (color)C'48,80,128';
g_news_currOnText = (color)C'255,255,255';
g_news_currOffBg = (color)C'36,40,48';
g_news_currOffText = (color)C'160,164,174';
g_news_impNone = (color)C'120,124,132';
g_news_impLow = (color)C'230,200,80';
g_news_impMed = (color)C'240,150,60';
g_news_impHigh = (color)C'225,80,80';
g_news_tableHdrBg = (color)C'36,40,48';
g_news_tableHdrText = (color)C'200,204,214';
g_news_rowAlt = (color)C'28,30,36';
g_news_rowText = (color)C'220,222,232';
g_news_rowHover = (color)C'70,76,92';
g_news_actualUp = (color)C'120,210,130';
g_news_actualDown = (color)C'235,100,100';
g_news_revisedMark = (color)C'250,200,60';
g_news_remainSoon = (color)C'255,90,90';
g_news_dayHeaderBg = (color)C'48,80,128';
g_news_dayHeaderText = (color)C'235,238,245';
g_news_closeColor = (color)C'180,184,194';
g_news_closeColorHover = (color)C'255,255,255';
g_news_closeBgHover = (color)C'180,60,60';
g_news_toastBg = (color)C'36,40,48';
g_news_toastBorder = (color)C'72,78,90';
g_news_toastSuccess = (color)C'120,200,120';
g_news_toastError = (color)C'225,100,100';
g_news_countdownBg = (color)C'48,80,128';
g_news_countdownReleaseBg = (color)C'140,60,60';
g_news_countdownText = (color)C'255,255,255';
g_news_scrollSlider = (color)C'90,100,120';
g_news_scrollSliderHover = (color)C'140,150,170';
g_news_scrollSliderDrag = (color)C'88,160,255';
}
else
{
//--- Apply light theme: white with blue accent
g_news_bg = (color)C'248,249,251';
g_news_panelAlt = (color)C'255,255,255';
g_news_headerBg = (color)C'238,240,244';
g_news_border = (color)C'218,222,228';
g_news_borderAccent = (color)C'180,186,196';
g_news_titleText = (color)C'24,28,36';
g_news_subText = (color)C'108,114,124';
g_news_accent = (color)C'40,110,220';
g_news_chipOnBg = (color)C'40,110,220';
g_news_chipOnText = (color)C'255,255,255';
g_news_chipOffBg = (color)C'232,236,242';
g_news_chipOffText = (color)C'70,76,86';
g_news_chipHoverTint = (color)C'0,0,0';
g_news_currOnBg = (color)C'200,220,250';
g_news_currOnText = (color)C'30,60,140';
g_news_currOffBg = (color)C'238,240,244';
g_news_currOffText = (color)C'90,96,106';
g_news_impNone = (color)C'140,144,152';
g_news_impLow = (color)C'220,180,40';
g_news_impMed = (color)C'230,130,40';
g_news_impHigh = (color)C'210,60,60';
g_news_tableHdrBg = (color)C'232,236,242';
g_news_tableHdrText = (color)C'40,46,56';
g_news_rowAlt = (color)C'248,250,253';
g_news_rowText = (color)C'30,34,42';
g_news_rowHover = (color)C'232,238,248';
g_news_actualUp = (color)C'30,140,60';
g_news_actualDown = (color)C'200,40,40';
g_news_revisedMark = (color)C'200,110,15';
g_news_remainSoon = (color)C'205,40,40';
g_news_dayHeaderBg = (color)C'215,228,245';
g_news_dayHeaderText = (color)C'30,60,140';
g_news_closeColor = (color)C'120,126,136';
g_news_closeColorHover = (color)C'255,255,255';
g_news_closeBgHover = (color)C'200,60,60';
g_news_toastBg = (color)C'255,255,255';
g_news_toastBorder = (color)C'180,186,196';
g_news_toastSuccess = (color)C'40,140,60';
g_news_toastError = (color)C'200,60,60';
g_news_countdownBg = (color)C'40,110,220';
g_news_countdownReleaseBg = (color)C'200,80,80';
g_news_countdownText = (color)C'255,255,255';
g_news_scrollSlider = (color)C'170,178,190';
g_news_scrollSliderHover = (color)C'120,128,142';
g_news_scrollSliderDrag = (color)C'40,110,220';
}
}
//+------------------------------------------------------------------+
//| Fast canvas subclass exposing direct pixel operations |
//+------------------------------------------------------------------+
class CNewsCanvasFast : public CCanvas
{
public:
int PixelWidth() { return m_width; } // Return canvas pixel width
int PixelHeight() { return m_height; } // Return canvas pixel height
//+------------------------------------------------------------------+
//| Copy rect from another canvas into this one at same coords |
//+------------------------------------------------------------------+
void CopyRectFromCanvas(CCanvas &src, int l, int t, int r, int b)
{
//--- Compute source and destination dimensions
const int sw = src.Width();
const int sh = src.Height();
const int dw = Width();
const int dh = Height();
//--- Clamp copy bounds to valid region
const int cl = MathMax(0, l);
const int ct = MathMax(0, t);
const int cr = MathMin(MathMin(r, sw), dw);
const int cb = MathMin(MathMin(b, sh), dh);
//--- Copy each pixel row by row
for(int yy = ct; yy < cb; yy++)
for(int xx = cl; xx < cr; xx++)
m_pixels[yy * dw + xx] = src.PixelGet(xx, yy);
}
//+------------------------------------------------------------------+
//| Copy rect from this canvas to a destination canvas at same coords|
//+------------------------------------------------------------------+
void CopyRectToCanvas(CCanvas &dst, int l, int t, int r, int b)
{
//--- Compute destination and source dimensions
const int dwOther = dst.Width();
const int dhOther = dst.Height();
const int sw = Width();
const int sh = Height();
//--- Clamp copy bounds to valid region
const int cl = MathMax(0, l);
const int ct = MathMax(0, t);
const int cr = MathMin(MathMin(r, sw), dwOther);
const int cb = MathMin(MathMin(b, sh), dhOther);
//--- Copy each pixel row by row
for(int yy = ct; yy < cb; yy++)
for(int xx = cl; xx < cr; xx++)
dst.PixelSet(xx, yy, m_pixels[yy * sw + xx]);
}
};
//+------------------------------------------------------------------+
//| Lighten a color by amount (0..1) |
//+------------------------------------------------------------------+
color News_LightenColor(color c, double amount)
{
//--- Decompose into RGB channels
const int r = (c & 0xFF);
const int g = (c >> 8) & 0xFF;
const int b = (c >> 16) & 0xFF;
//--- Blend each channel toward white
const int nr = (int)MathMin(255, r + (255 - r) * amount);
const int ng = (int)MathMin(255, g + (255 - g) * amount);
const int nb = (int)MathMin(255, b + (255 - b) * amount);
//--- Reassemble and return lightened color
return (color)((nb << 16) | (ng << 8) | nr);
}
//+------------------------------------------------------------------+
//| Darken a color by amount (0..1) |
//+------------------------------------------------------------------+
color News_DarkenColor(color c, double amount)
{
//--- Decompose into RGB channels
const int r = (c & 0xFF);
const int g = (c >> 8) & 0xFF;
const int b = (c >> 16) & 0xFF;
//--- Multiply each channel toward black
const int nr = (int)MathMax(0, r * amount);
const int ng = (int)MathMax(0, g * amount);
const int nb = (int)MathMax(0, b * amount);
//--- Reassemble and return darkened color
return (color)((nb << 16) | (ng << 8) | nr);
}
//+------------------------------------------------------------------+
//| Compute theme-aware border color for given fill |
//+------------------------------------------------------------------+
color News_BorderForBg(color bg)
{
//--- Lighten in dark theme, darken in light theme
if(g_news_darkTheme) return News_LightenColor(bg, 0.3);
return News_DarkenColor(bg, 0.78);
}
//+------------------------------------------------------------------+
//| Compute theme-aware hover color for a given base background |
//+------------------------------------------------------------------+
color News_HoverForBg(color bg)
{
//--- Lighten dark backgrounds, darken light backgrounds on hover
if(g_news_darkTheme) return News_LightenColor(bg, 0.20);
return News_DarkenColor(bg, 0.90);
}
//+------------------------------------------------------------------+
//| Compute current column widths from runtime dashboard width |
//+------------------------------------------------------------------+
void News_ComputeColumnWidths()
{
//--- Compute proportional scale factor relative to default width
const double scale = (double)g_news_dashW / (double)NEWS_DASHBOARD_W_DEFAULT;
//--- Scale each reference column width and enforce 20px floor
for(int i = 0; i < NEWS_COL_COUNT; i++)
{
int w = (int)MathRound(NEWS_COL_W_REF[i] * scale);
if(w < 20) w = 20;
g_news_colW[i] = w;
}
//--- Compute layout geometry to determine scrollbar reservation
const int sidePad = 10;
const int xStart = 6;
const int totalRows = ArraySize(g_news_rowPlan);
const int rowsTop = NEWS_HEADER_H + 36 + 32 + 32 + 6 + 28;
const int footerTop = g_news_dashH - 26 - 8;
const int viewportH = footerTop - rowsTop;
const int contentH = totalRows * 26;
const bool scrollNeeded = (contentH > viewportH);
const int scrollReserve = scrollNeeded ? 12 : 0;
const int availW = g_news_dashW - 2 * sidePad - xStart - scrollReserve;
//--- Sum all scaled column widths
int sumW = 0;
for(int i = 0; i < NEWS_COL_COUNT; i++) sumW += g_news_colW[i];
//--- Shrink the Event column (index 4) if total exceeds available width
if(sumW > availW)
{
const int over = sumW - availW;
int eventW = g_news_colW[4] - over;
if(eventW < 80) eventW = 80;
g_news_colW[4] = eventW;
}
}
//+------------------------------------------------------------------+
//| Blend a single ARGB pixel onto canvas at (x, y) |
//+------------------------------------------------------------------+
void News_BlendPixel(CCanvas &canv, int x, int y, uint argb)
{
//--- Skip pixels outside canvas bounds
if(x < 0 || y < 0 || x >= canv.Width() || y >= canv.Height()) return;
//--- Extract source alpha; skip fully transparent pixels
const uchar sa = (uchar)((argb >> 24) & 0xFF);
if(sa == 0) return;
//--- Extract source RGB channels
const uchar sr = (uchar)((argb >> 16) & 0xFF);
const uchar sg = (uchar)((argb >> 8) & 0xFF);
const uchar sb = (uchar)(argb & 0xFF);
//--- Read destination pixel and extract its channels
const uint dst = canv.PixelGet(x, y);
const uchar da = (uchar)((dst >> 24) & 0xFF);
const uchar dr = (uchar)((dst >> 16) & 0xFF);
const uchar dg = (uchar)((dst >> 8) & 0xFF);
const uchar db = (uchar)(dst & 0xFF);
//--- Perform source-over alpha composite
const int isa = 255 - sa;
const uchar or_ = (uchar)((sa * sr + isa * dr) / 255);
const uchar og = (uchar)((sa * sg + isa * dg) / 255);
const uchar ob = (uchar)((sa * sb + isa * db) / 255);
const uchar oa = (uchar)(sa + (isa * da) / 255);
canv.PixelSet(x, y, (oa << 24) | (or_ << 16) | (og << 8) | ob);
}
//+------------------------------------------------------------------+
//| Get text width in pixels |
//+------------------------------------------------------------------+
int News_TextWidth(string text, string font, int size)
{
//--- Allocate a shared single-pixel measurement canvas on first call
static CCanvas s_measureCanvas;
static bool s_ready = false;
if(!s_ready)
{
s_measureCanvas.CreateBitmap("NewsMeasureCanvas", 0, 0, 1, 1, COLOR_FORMAT_ARGB_NORMALIZE);
s_ready = true;
}
//--- Set font and measure text dimensions
s_measureCanvas.FontSet(font, -size * 10, FW_NORMAL);
int w = 0, h = 0;
s_measureCanvas.TextSize(text, w, h);
return w;
}
//+------------------------------------------------------------------+
//| Get text height in pixels |
//+------------------------------------------------------------------+
int News_TextHeight(string font, int size)
{
//--- Allocate a shared single-pixel measurement canvas on first call
static CCanvas s_measureCanvas;
static bool s_ready = false;
if(!s_ready)
{
s_measureCanvas.CreateBitmap("NewsMeasureCanvas2", 0, 0, 1, 1, COLOR_FORMAT_ARGB_NORMALIZE);
s_ready = true;
}
//--- Measure height using a capital-descender reference string
s_measureCanvas.FontSet(font, -size * 10, FW_NORMAL);
int w = 0, h = 0;
s_measureCanvas.TextSize("Mg", w, h);
return h;
}
//+------------------------------------------------------------------+
//| Stamp normal-weight text onto canvas |
//+------------------------------------------------------------------+
void News_StampText(CCanvas &canv, int x, int y, string text, string font, int size, color clr)
{
//--- Set normal font weight and draw text at position
canv.FontSet(font, -size * 10, FW_NORMAL);
canv.TextOut(x, y, text, ColorToARGB(clr, 255), TA_LEFT | TA_TOP);
}
//+------------------------------------------------------------------+
//| Stamp bold text onto canvas |
//+------------------------------------------------------------------+
void News_StampTextBold(CCanvas &canv, int x, int y, string text, string font, int size, color clr)
{
//--- Set bold font weight and draw text at position
canv.FontSet(font, -size * 10, FW_BOLD);
canv.TextOut(x, y, text, ColorToARGB(clr, 255), TA_LEFT | TA_TOP);
}
//+------------------------------------------------------------------+
//| Fit text into width with ellipsis fallback |
//+------------------------------------------------------------------+
string News_FitTextToWidth(string text, string font, int size, int maxW)
{
//--- Return text unchanged if it already fits
if(News_TextWidth(text, font, size) <= maxW) return text;
//--- Measure ellipsis width; return empty if even ellipsis cannot fit
const string ellipsis = "...";
const int ellW = News_TextWidth(ellipsis, font, size);
if(ellW >= maxW) return "";
//--- Binary search for the longest prefix that fits with ellipsis appended
const int textLen = StringLen(text);
int lo = 0, hi = textLen, fit = 0;
while(lo <= hi)
{
const int mid = (lo + hi) / 2;
const string pre = StringSubstr(text, 0, mid) + ellipsis;
if(News_TextWidth(pre, font, size) <= maxW) { fit = mid; lo = mid + 1; }
else { hi = mid - 1; }
}
return StringSubstr(text, 0, fit) + ellipsis;
}
//+------------------------------------------------------------------+
//| Drawing primitives helper - supersampled HR rendering |
//+------------------------------------------------------------------+
class CNewsPrimitives
{
public:
CNewsCanvasFast m_hrFill; // HR fill offscreen canvas
bool m_hrFillReady; // HR fill canvas ready flag
int m_hrFillW; // HR fill canvas width
int m_hrFillH; // HR fill canvas height
CNewsCanvasFast m_hrBorder; // HR border offscreen canvas
bool m_hrBorderReady; // HR border canvas ready flag
int m_hrBorderW; // HR border canvas width
int m_hrBorderH; // HR border canvas height
//--- Constructor: initialize all flags and sizes to zero/false
CNewsPrimitives()
{
m_hrFillReady = false; m_hrFillW = 0; m_hrFillH = 0;
m_hrBorderReady = false; m_hrBorderW = 0; m_hrBorderH = 0;
}
bool EnsureHrFill(int needW, int needH);
bool EnsureHrBorder(int needW, int needH);
void BlendPixelSet(CCanvas &canvas, int x, int y, uint src);
void FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY);
void FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb);
void DrawRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, int thickness, uint argb);
void FillRoundRectSharp(CCanvas &target, int x, int y, int w, int h, int radius, uint argb, int factor = 4);
void DrawRoundRectBorderSharp(CCanvas &target, int x, int y, int w, int h, int radius, int thickness, uint argb, int factor = 4);
void DrawRoundRectBorderObStyle(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb);
};
//+------------------------------------------------------------------+
//| Ensure HR fill canvas is allocated and large enough |
//+------------------------------------------------------------------+
bool CNewsPrimitives::EnsureHrFill(int needW, int needH)
{
//--- Return early if canvas already meets size requirements
if(m_hrFillReady && needW <= m_hrFillW && needH <= m_hrFillH) return true;
//--- Grow to the larger of current and requested dimensions
const int w = MathMax(needW, m_hrFillW);
const int h = MathMax(needH, m_hrFillH);
//--- Destroy stale canvas before recreating
if(m_hrFillReady) { m_hrFill.Destroy(); m_hrFillReady = false; }
if(!m_hrFill.CreateBitmap("NewsPrimHrFill", 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
//--- Record new dimensions and mark ready
m_hrFillW = w; m_hrFillH = h;
m_hrFillReady = true;
return true;
}
//+------------------------------------------------------------------+
//| Ensure HR border canvas is allocated and large enough |
//+------------------------------------------------------------------+
bool CNewsPrimitives::EnsureHrBorder(int needW, int needH)
{
//--- Return early if canvas already meets size requirements
if(m_hrBorderReady && needW <= m_hrBorderW && needH <= m_hrBorderH) return true;
//--- Grow to the larger of current and requested dimensions
const int w = MathMax(needW, m_hrBorderW);
const int h = MathMax(needH, m_hrBorderH);
//--- Destroy stale canvas before recreating
if(m_hrBorderReady) { m_hrBorder.Destroy(); m_hrBorderReady = false; }
if(!m_hrBorder.CreateBitmap("NewsPrimHrBorder", 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
//--- Record new dimensions and mark ready
m_hrBorderW = w; m_hrBorderH = h;
m_hrBorderReady = true;
return true;
}
//+------------------------------------------------------------------+
//| Blend a single pixel onto canvas using src-over alpha |
//+------------------------------------------------------------------+
void CNewsPrimitives::BlendPixelSet(CCanvas &canvas, int x, int y, uint src)
{
//--- Bounds check; skip out-of-range pixels
if(x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return;
//--- Decompose source and destination into float channels
uint dst = canvas.PixelGet(x, y);
double sA = ((src >> 24) & 0xFF) / 255.0, sR = ((src >> 16) & 0xFF) / 255.0;
double sG = ((src >> 8) & 0xFF) / 255.0, sB = ( src & 0xFF) / 255.0;
double dA = ((dst >> 24) & 0xFF) / 255.0, dR = ((dst >> 16) & 0xFF) / 255.0;
double dG = ((dst >> 8) & 0xFF) / 255.0, dB = ( dst & 0xFF) / 255.0;
//--- Compute output alpha via src-over formula
double oA = sA + dA * (1.0 - sA);
if(oA == 0.0) { canvas.PixelSet(x, y, 0); return; }
//--- Composite and write final pixel
canvas.PixelSet(x, y,
((uint)(uchar)(oA * 255 + 0.5) << 24) |
((uint)(uchar)((sR * sA + dR * dA * (1.0 - sA)) / oA * 255 + 0.5) << 16) |
((uint)(uchar)((sG * sA + dG * dA * (1.0 - sA)) / oA * 255 + 0.5) << 8) |
(uint)(uchar)((sB * sA + dB * dA * (1.0 - sA)) / oA * 255 + 0.5));
}
//+------------------------------------------------------------------+
//| Fill quarter-circle corner on high-resolution canvas |
//+------------------------------------------------------------------+
void CNewsPrimitives::FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY)
{
//--- Set up radius, alpha, RGB, and subpixel grid
double rd = (double)radius;
uchar bA = (uchar)((argb >> 24) & 0xFF);
uint rgb = argb & 0x00FFFFFF;
int sub = 4;
double step = 1.0 / sub;
int subSq = sub * sub;
//--- Iterate over bounding box pixels
for(int dy = -(radius + 1); dy <= (radius + 1); dy++)
for(int dx = -(radius + 1); dx <= (radius + 1); dx++)
{
//--- Skip pixels outside this quadrant
bool inQ = ((signX > 0) ? (dx >= 0) : (dx <= 0)) && ((signY > 0) ? (dy >= 0) : (dy <= 0));
if(!inQ) continue;
//--- Skip pixels clearly outside or inside the arc band
double dist = MathSqrt((double)(dx * dx + dy * dy));
if(dist > rd + 1.0) continue;
if(dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
//--- Supersample the arc edge for anti-aliasing
int inside = 0;
for(int sy = 0; sy < sub; sy++)
for(int sx = 0; sx < sub; sx++)
{
double sdx = (double)dx - 0.5 + (sx + 0.5) * step;
double sdy = (double)dy - 0.5 + (sy + 0.5) * step;
if(sdx * sdx + sdy * sdy <= rd * rd) inside++;
}
if(inside == 0) continue;
//--- Blend partial coverage pixel
BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
}
}
//+------------------------------------------------------------------+
//| Fill rounded rectangle on HR canvas |
//+------------------------------------------------------------------+
void CNewsPrimitives::FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb)
{
//--- Clamp radius to half the shortest dimension
radius = MathMin(radius, MathMin(w / 2, h / 2));
if(radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; }
//--- Fill the three non-corner rectangles
canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb);
canvas.FillRectangle(x, y + radius, x + radius - 1, y + h - radius - 1, argb);
canvas.FillRectangle(x + w - radius, y + radius, x + w - 1, y + h - radius - 1, argb);
//--- Fill each anti-aliased corner quadrant
FillCornerQuadrantHR(canvas, x + radius, y + radius, radius, argb, -1, -1);
FillCornerQuadrantHR(canvas, x + w - radius, y + radius, radius, argb, 1, -1);
FillCornerQuadrantHR(canvas, x + radius, y + h - radius, radius, argb, -1, 1);
FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb, 1, 1);
}
//+------------------------------------------------------------------+
//| Draw rounded border on HR canvas (filled outer minus inner) |
//+------------------------------------------------------------------+
void CNewsPrimitives::DrawRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, int thickness, uint argb)
{
//--- Fill the outer rounded rect
FillRoundRectHR(canvas, x, y, w, h, radius, argb);
//--- Compute inner rect dimensions
const int innerX = x + thickness;
const int innerY = y + thickness;
const int innerW = w - 2 * thickness;
const int innerH = h - 2 * thickness;
const int innerR = MathMax(0, radius - thickness);
//--- Punch out inner area with transparent fill
if(innerW > 0 && innerH > 0)
FillRoundRectHR(canvas, innerX, innerY, innerW, innerH, innerR, 0x00000000);
}
//+------------------------------------------------------------------+
//| Fill rounded rect with supersampled anti-aliasing |
//+------------------------------------------------------------------+
void CNewsPrimitives::FillRoundRectSharp(CCanvas &target, int x, int y, int w, int h, int radius, uint argb, int factor)
{
//--- Skip degenerate rectangles
if(w <= 0 || h <= 0) return;
//--- Allocate or grow HR offscreen canvas
const int hrW = w * factor, hrH = h * factor;
if(!EnsureHrFill(hrW, hrH)) return;
//--- Clear and fill HR canvas
m_hrFill.FillRectangle(0, 0, hrW - 1, hrH - 1, 0);
FillRoundRectHR(m_hrFill, 0, 0, hrW, hrH, radius * factor, argb);
//--- Downsample HR canvas into target
const int ss2 = factor * factor;
const int tW = target.Width(), tH = target.Height();
for(int py = 0; py < h; py++)
{
const int ty = y + py;
if(ty < 0 || ty >= tH) continue;
for(int px = 0; px < w; px++)
{
const int tx = x + px;
if(tx < 0 || tx >= tW) continue;
//--- Accumulate subpixel coverage
double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
for(int dy = 0; dy < factor; dy++)
for(int dx = 0; dx < factor; dx++)
{
const int sx = px * factor + dx, sy = py * factor + dy;
if(sx >= hrW || sy >= hrH) continue;
const uint p = m_hrFill.PixelGet(sx, sy);
const uchar a = (uchar)((p >> 24) & 0xFF);
sA += a;
if(a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
}
//--- Blend averaged sample into target
const uchar fa = (uchar)(sA / ss2);
if(fa == 0 || wc == 0) continue;
const uint sample = ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc);
BlendPixelSet(target, tx, ty, sample);
}
}
}
//+------------------------------------------------------------------+
//| Draw rounded border with supersampled anti-aliasing |
//+------------------------------------------------------------------+
void CNewsPrimitives::DrawRoundRectBorderSharp(CCanvas &target, int x, int y, int w, int h, int radius, int thickness, uint argb, int factor)
{
//--- Skip degenerate or zero-thickness requests
if(w <= 0 || h <= 0 || thickness <= 0) return;
//--- Allocate or grow HR border offscreen canvas
const int hrW = w * factor, hrH = h * factor;
if(!EnsureHrBorder(hrW, hrH)) return;
//--- Clear and render border onto HR canvas
m_hrBorder.FillRectangle(0, 0, hrW - 1, hrH - 1, 0);
DrawRoundRectBorderHR(m_hrBorder, 0, 0, hrW, hrH, radius * factor, thickness * factor, argb);
//--- Downsample HR border canvas into target
const int ss2 = factor * factor;
const int tW = target.Width(), tH = target.Height();
for(int py = 0; py < h; py++)
{
const int ty = y + py;
if(ty < 0 || ty >= tH) continue;
for(int px = 0; px < w; px++)
{
const int tx = x + px;
if(tx < 0 || tx >= tW) continue;
//--- Accumulate subpixel coverage
double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
for(int dy = 0; dy < factor; dy++)
for(int dx = 0; dx < factor; dx++)
{
const int sx = px * factor + dx, sy = py * factor + dy;
if(sx >= hrW || sy >= hrH) continue;
const uint p = m_hrBorder.PixelGet(sx, sy);
const uchar a = (uchar)((p >> 24) & 0xFF);
sA += a;
if(a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
}
//--- Blend averaged sample into target
const uchar fa = (uchar)(sA / ss2);
if(fa == 0 || wc == 0) continue;
const uint sample = ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc);
BlendPixelSet(target, tx, ty, sample);
}
}
}
//+------------------------------------------------------------------+
//| Global Primitives Instance |
//+------------------------------------------------------------------+
CNewsPrimitives g_news_prim; // Shared primitives helper instance
//+------------------------------------------------------------------+
//| Draw rounded rect border using arc-sampled 1px outline |
//+------------------------------------------------------------------+
void CNewsPrimitives::DrawRoundRectBorderObStyle(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb)
{
//--- Clamp radius and extract alpha and RGB
radius = MathMin(radius, MathMin(w / 2, h / 2));
if(radius < 0) radius = 0;
uchar bA = (uchar)((argb >> 24) & 0xFF);
uint rgb = argb & 0x00FFFFFF;
const int sub = 4;
const double step = 1.0 / sub;
const int subSq = sub * sub;
const double rd = (double)radius;
//--- Draw straight edges between corner arcs
canvas.Line(x + radius, y, x + w - radius - 1, y, argb);
canvas.Line(x + radius, y + h - 1, x + w - radius - 1, y + h - 1, argb);
canvas.Line(x, y + radius, x, y + h - radius - 1, argb);
canvas.Line(x + w - 1, y + radius, x + w - 1, y + h - radius - 1, argb);
if(radius == 0) return;
//--- Render anti-aliased arc for each of the four corners
for(int corner = 0; corner < 4; corner++)
{
int cx = (corner == 0 || corner == 2) ? (x + radius) : (x + w - 1 - radius);
int cy = (corner == 0 || corner == 1) ? (y + radius) : (y + h - 1 - radius);
int signX = (corner == 0 || corner == 2) ? -1 : 1;
int signY = (corner == 0 || corner == 1) ? -1 : 1;
for(int adyL = 0; adyL <= radius + 1; adyL++)
{
for(int adxL = 0; adxL <= radius + 1; adxL++)
{
//--- Skip pixels clearly outside the arc band
double dist = MathSqrt((double)(adxL * adxL + adyL * adyL));
if(dist > rd + 1.0 || dist < rd - 1.0) continue;
//--- Supersample the arc edge pixel
int inside = 0;
for(int sy2 = 0; sy2 < sub; sy2++)
for(int sx2 = 0; sx2 < sub; sx2++)
{
double sdx = (double)adxL - 0.5 + (sx2 + 0.5) * step;
double sdy = (double)adyL - 0.5 + (sy2 + 0.5) * step;
double sd = MathSqrt(sdx * sdx + sdy * sdy);
if(sd >= rd - 0.5 && sd <= rd + 0.5) inside++;
}
if(inside == 0) continue;
//--- Blend partial-coverage arc pixel
const uint sample = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb;
const int px = cx + adxL * signX;
const int py = cy + adyL * signY;
BlendPixelSet(canvas, px, py, sample);
}
}
}
}
//+------------------------------------------------------------------+
//| Fill rounded rect via supersampled HR rendering |
//+------------------------------------------------------------------+
void News_FillRoundRect(CCanvas &canv, int x, int y, int w, int h, int r, uint argb)
{
//--- Delegate to shared primitives instance
g_news_prim.FillRoundRectSharp(canv, x, y, w, h, r, argb, 4);
}
//+------------------------------------------------------------------+
//| Draw rounded rect border via supersampled HR rendering |
//+------------------------------------------------------------------+
void News_DrawRoundRectBorder(CCanvas &canv, int x, int y, int w, int h, int r, int thick, uint argb)
{
//--- Delegate to shared primitives instance
g_news_prim.DrawRoundRectBorderSharp(canv, x, y, w, h, r, thick, argb, 4);
}
//+------------------------------------------------------------------+
//| Test if point lies inside a rectangle |
//+------------------------------------------------------------------+
bool News_PointInRect(int px, int py, int rx, int ry, int rw, int rh)
{
//--- Return true when point is within bounds
return (px >= rx && px < rx + rw && py >= ry && py < ry + rh);
}
//+------------------------------------------------------------------+
//| Draw anti-aliased thick line |
//+------------------------------------------------------------------+
void News_ThickLineAA(CCanvas &canvas, int x0, int y0, int x1, int y1, int thickness, uint argb)
{
//--- Clamp thickness to supported range
if(thickness < 1) thickness = 1;
if(thickness > 4) thickness = 4;
//--- Horizontal fast path
if(y0 == y1)
{
const int xL = MathMin(x0, x1);
const int xR = MathMax(x0, x1);
const int yTop = y0 - thickness / 2;
const int yBot = yTop + thickness - 1;
for(int yy = yTop; yy <= yBot; yy++)
for(int xx = xL; xx <= xR; xx++)
News_BlendPixel(canvas, xx, yy, argb);
return;
}
//--- Vertical fast path
if(x0 == x1)
{
const int yT = MathMin(y0, y1);
const int yB = MathMax(y0, y1);
const int xL = x0 - thickness / 2;
const int xR = xL + thickness - 1;
for(int xx = xL; xx <= xR; xx++)
for(int yy = yT; yy <= yB; yy++)
News_BlendPixel(canvas, xx, yy, argb);
return;
}
//--- Diagonal: supersampled coverage along the line normal
const double halfT = (double)thickness / 2.0;
const double ax = (double)x0 + 0.5, ay = (double)y0 + 0.5;
const double bx = (double)x1 + 0.5, by = (double)y1 + 0.5;
const double dx = bx - ax, dy = by - ay;
const double lenSq = dx * dx + dy * dy;
if(lenSq < 1e-9) return;
//--- Compute tight bounding box around line with padding
const double pad = halfT + 1.0;
const int bbL = (int)MathFloor(MathMin(ax, bx) - pad);
const int bbT = (int)MathFloor(MathMin(ay, by) - pad);
const int bbR = (int)MathCeil(MathMax(ax, bx) + pad);
const int bbB = (int)MathCeil(MathMax(ay, by) + pad);
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
const int sub = 4;
const double step = 1.0 / sub;
const int subSq = sub * sub;
//--- Sample each pixel in the bounding box
for(int py = bbT; py <= bbB; py++)
{
for(int px = bbL; px <= bbR; px++)
{
//--- Project pixel center onto line segment
const double pcx = (double)px + 0.5;
const double pcy = (double)py + 0.5;
double t = ((pcx - ax) * dx + (pcy - ay) * dy) / lenSq;
if(t < 0.0) t = 0.0;
if(t > 1.0) t = 1.0;
const double projX = ax + t * dx;
const double projY = ay + t * dy;
const double pdx = pcx - projX;
const double pdy = pcy - projY;
const double centerDist = MathSqrt(pdx * pdx + pdy * pdy);
if(centerDist > halfT + 1.0) continue;
//--- Fill fully-covered pixels directly
if(centerDist <= halfT - 1.0) { News_BlendPixel(canvas, px, py, argb); continue; }
//--- Supersample partial-coverage pixels
int inside = 0;
for(int sy = 0; sy < sub; sy++)
{
for(int sx = 0; sx < sub; sx++)
{
const double sx_ = (double)px + (sx + 0.5) * step;
const double sy_ = (double)py + (sy + 0.5) * step;
double st = ((sx_ - ax) * dx + (sy_ - ay) * dy) / lenSq;
if(st < 0.0) st = 0.0;
if(st > 1.0) st = 1.0;
const double spx = ax + st * dx;
const double spy = ay + st * dy;
const double sdx = sx_ - spx;
const double sdy = sy_ - spy;
if(sdx * sdx + sdy * sdy <= halfT * halfT) inside++;
}
}
if(inside == 0) continue;
//--- Blend coverage-weighted pixel
const uint covArgb = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb;
News_BlendPixel(canvas, px, py, covArgb);
}
}
}
//+------------------------------------------------------------------+
//| Chevron Direction Codes |
//+------------------------------------------------------------------+
#define NEWS_CHEVRON_UP 0 // Up chevron direction
#define NEWS_CHEVRON_DOWN 1 // Down chevron direction
#define NEWS_CHEVRON_LEFT 2 // Left chevron direction
#define NEWS_CHEVRON_RIGHT 3 // Right chevron direction
//+------------------------------------------------------------------+
//| Draw chevron centered at (cx, cy) pointing in given direction |
//+------------------------------------------------------------------+
void News_DrawChevron(CCanvas &canvas, int cx, int cy, int direction, uint argb)
{
//--- Draw two strokes forming a V-shape for the given direction
if(direction == NEWS_CHEVRON_UP)
{
News_ThickLineAA(canvas, cx - 4, cy + 2, cx, cy - 2, 2, argb);
News_ThickLineAA(canvas, cx, cy - 2, cx + 4, cy + 2, 2, argb);
}
else if(direction == NEWS_CHEVRON_DOWN)
{
News_ThickLineAA(canvas, cx - 4, cy - 2, cx, cy + 2, 2, argb);
News_ThickLineAA(canvas, cx, cy + 2, cx + 4, cy - 2, 2, argb);
}
else if(direction == NEWS_CHEVRON_LEFT)
{
News_ThickLineAA(canvas, cx + 2, cy - 4, cx - 2, cy, 2, argb);
News_ThickLineAA(canvas, cx - 2, cy, cx + 2, cy + 4, 2, argb);
}
else
{
//--- Draw right-pointing chevron strokes
News_ThickLineAA(canvas, cx - 2, cy - 4, cx + 2, cy, 2, argb);
News_ThickLineAA(canvas, cx + 2, cy, cx - 2, cy + 4, 2, argb);
}
}
//+------------------------------------------------------------------+
//| Draw equilateral triangle centered at (cx, cy) |
//+------------------------------------------------------------------+
void News_DrawTriangle(CCanvas &canvas, int cx, int cy, int direction, uint argb)
{
//--- Define equilateral triangle geometry; side=7, height=side*sqrt(3)/2
const double side = 7.0;
const double height = side * 0.86602540378;
const double halfBase = side / 2.0;
const uchar bA = (uchar)((argb >> 24) & 0xFF);
const uint rgb = argb & 0x00FFFFFF;
const int sub = 8;
const double step = 1.0 / sub;
const int subSq = sub * sub;
//--- Set bounding box with padding for AA edge coverage
const int rad = 5;
const int bbL = cx - rad;
const int bbR = cx + rad;
const int bbT = cy - rad;
const int bbB = cy + rad;
//--- Compute centroid offset and apex/axis vectors for this direction
const double cz = height / 3.0;
double apexX, apexY, axisX, axisY, perpX, perpY;
if(direction == NEWS_CHEVRON_UP)
{
apexX = (double)cx; apexY = (double)cy - (height - cz);
axisX = 0.0; axisY = 1.0;
perpX = 1.0; perpY = 0.0;
}
else if(direction == NEWS_CHEVRON_DOWN)
{
apexX = (double)cx; apexY = (double)cy + (height - cz);
axisX = 0.0; axisY = -1.0;
perpX = 1.0; perpY = 0.0;
}
else if(direction == NEWS_CHEVRON_LEFT)
{
apexX = (double)cx - (height - cz); apexY = (double)cy;
axisX = 1.0; axisY = 0.0;
perpX = 0.0; perpY = 1.0;
}
else
{
//--- Right-facing apex
apexX = (double)cx + (height - cz); apexY = (double)cy;
axisX = -1.0; axisY = 0.0;
perpX = 0.0; perpY = 1.0;
}
//--- Supersample each pixel in the bounding box
for(int py = bbT; py <= bbB; py++)
{
for(int px = bbL; px <= bbR; px++)
{
int inside = 0;
for(int sy = 0; sy < sub; sy++)
{
for(int sx = 0; sx < sub; sx++)
{
const double sxp = (double)px + (sx + 0.5) * step;
const double syp = (double)py + (sy + 0.5) * step;
const double ddx = sxp - apexX;
const double ddy = syp - apexY;
const double along = ddx * axisX + ddy * axisY;
if(along < 0.0 || along > height) continue;
const double perp = MathAbs(ddx * perpX + ddy * perpY);
const double maxPerp = halfBase * (along / height);
if(perp <= maxPerp) inside++;
}
}
if(inside == 0) continue;
//--- Blend fully-covered or partial-coverage pixel
if(inside == subSq)
News_BlendPixel(canvas, px, py, argb);
else
{
const uint covArgb = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb;
News_BlendPixel(canvas, px, py, covArgb);
}
}
}
}
//+------------------------------------------------------------------+
//| Draw filled anti-aliased circle for impact dot |
//+------------------------------------------------------------------+
void News_DrawFilledCircle(CCanvas &canv, int cx, int cy, int radius, uint argb)
{
const double rd = (double)radius;
const double rd2 = rd * rd;
//--- Iterate over the bounding square
for(int dy = -radius; dy <= radius; dy++)
{
for(int dx = -radius; dx <= radius; dx++)
{
const double dist2 = (double)(dx * dx + dy * dy);
//--- Fill pixels fully inside the circle
if(dist2 <= rd2 - 2.0 * rd + 1.0)
{
canv.PixelSet(cx + dx, cy + dy, argb);
}
//--- Anti-alias pixels on the circle edge
else if(dist2 <= rd2 + 2.0 * rd + 1.0)
{
const double dist = MathSqrt(dist2);
const double cover = MathMax(0.0, MathMin(1.0, rd - dist + 0.5));
if(cover > 0.0)
{
const uchar baseA = (uchar)((argb >> 24) & 0xFF);
const uchar a = (uchar)(baseA * cover);
const uint blended = (a << 24) | (argb & 0x00FFFFFF);
News_BlendPixel(canv, cx + dx, cy + dy, blended);
}
}
}
}
}
//+------------------------------------------------------------------+
//| Draw small two-stroke checkmark icon |
//+------------------------------------------------------------------+
void News_DrawCheckIcon(CCanvas &canv, int x, int y, int size, uint argb)
{
//--- Compute stroke thickness and checkmark vertex positions
const int t = MathMax(1, size / 7);
const int p1x = x + size / 5;
const int p1y = y + size / 2;
const int p2x = x + size * 2 / 5;
const int p2y = y + size * 3 / 4;
const int p3x = x + size * 4 / 5;
const int p3y = y + size / 4;
//--- Draw two anti-aliased strokes with thickness offsets
for(int dx = -t / 2; dx <= t / 2; dx++)
{
for(int dy = -t / 2; dy <= t / 2; dy++)
{
canv.LineAA(p1x + dx, p1y + dy, p2x + dx, p2y + dy, argb);
canv.LineAA(p2x + dx, p2y + dy, p3x + dx, p3y + dy, argb);
}
}
}
//+------------------------------------------------------------------+
//| Draw small X-shaped cross icon |
//+------------------------------------------------------------------+
void News_DrawCrossIcon(CCanvas &canv, int x, int y, int size, uint argb)
{
//--- Compute stroke thickness and padded corner positions
const int t = MathMax(1, size / 7);
const int pad = size / 5;
//--- Draw two diagonal strokes with thickness offsets
for(int dx = -t / 2; dx <= t / 2; dx++)
{
for(int dy = -t / 2; dy <= t / 2; dy++)
{
canv.LineAA(x + pad + dx, y + pad + dy,
x + size - pad + dx, y + size - pad + dy, argb);
canv.LineAA(x + size - pad + dx, y + pad + dy,
x + pad + dx, y + size - pad + dy, argb);
}
}
}
//+------------------------------------------------------------------+
//| Currency Filter Constants |
//+------------------------------------------------------------------+
const string NEWS_CURRENCIES[] = {"AUD", "CAD", "CHF", "EUR", "GBP", "JPY", "NZD", "USD"};
#define NEWS_CURR_COUNT 8
//+------------------------------------------------------------------+
//| Impact Level Constants |
//+------------------------------------------------------------------+
const string NEWS_IMPACT_LABELS[] = {"None", "Low", "Medium", "High"};
const ENUM_CALENDAR_EVENT_IMPORTANCE NEWS_IMPACT_LEVELS[] =
{
CALENDAR_IMPORTANCE_NONE, CALENDAR_IMPORTANCE_LOW,
CALENDAR_IMPORTANCE_MODERATE, CALENDAR_IMPORTANCE_HIGH
};
#define NEWS_IMPACT_COUNT 4
//+------------------------------------------------------------------+
//| Economic event record |
//+------------------------------------------------------------------+
struct NewsEvent
{
string eventDate; // Date string YYYY.MM.DD
string eventTime; // Time string HH:MM
string currency; // Currency code
string event; // Event name
string importance; // None / Low / Medium / High
double actual; // Actual released value
double forecast; // Forecast value
double previous; // Previous value
double revisedPrevious; // Revised previous value
bool hasActual; // True when actual value is published
bool hasForecast; // True when forecast value is published
bool hasPrevious; // True when previous value is published
bool hasRevised; // True when revised previous is published
int unit; // ENUM_CALENDAR_EVENT_UNIT
int multiplier; // ENUM_CALENDAR_EVENT_MULTIPLIER
int digits; // Decimal places for display
datetime eventDateTime; // Combined event timestamp
long eventId; // Unique event ID for trade tracking
};
//+------------------------------------------------------------------+
//| Event Arrays |
//+------------------------------------------------------------------+
NewsEvent g_news_allEvents[]; // All loaded events from CSV or live feed
NewsEvent g_news_filteredEvents[]; // Events filtered by date range
NewsEvent g_news_displayableEvents[]; // Events after all active filters applied
//+------------------------------------------------------------------+
//| Row Plan Constants |
//+------------------------------------------------------------------+
#define NEWS_ROW_KIND_DAY 0 // Row represents a day-separator header
#define NEWS_ROW_KIND_EVENT 1 // Row represents an individual event entry
//+------------------------------------------------------------------+
//| Row plan entry structure |
//+------------------------------------------------------------------+
struct NewsRowEntry
{
int kind; // NEWS_ROW_KIND_DAY or NEWS_ROW_KIND_EVENT
int eventIdx; // Index into g_news_displayableEvents when kind==EVENT
string label; // Day label string when kind==DAY
string dateKey; // Date string used for collapse toggle
};
//+------------------------------------------------------------------+
//| Row Plan and Collapse State |
//+------------------------------------------------------------------+
NewsRowEntry g_news_rowPlan[]; // Render plan built from displayable events
string g_news_collapsedDays[]; // Date strings of currently collapsed day groups
//+------------------------------------------------------------------+
//| Filter Selection State |
//+------------------------------------------------------------------+
bool g_news_currSelected[NEWS_CURR_COUNT]; // Per-currency selection flag
bool g_news_impactSelected[NEWS_IMPACT_COUNT]; // Per-impact selection flag
//+------------------------------------------------------------------+
//| Filter Master Toggles |
//+------------------------------------------------------------------+
bool g_news_filterCurrencyOn = true; // Currency filter enabled flag
bool g_news_filterImpactOn = true; // Impact filter enabled flag
bool g_news_filterTimeOn = true; // Time range filter enabled flag
//+------------------------------------------------------------------+
//| Event Counters |
//+------------------------------------------------------------------+
int g_news_totalConsidered = 0; // Total events considered before filtering
int g_news_totalFiltered = 0; // Total events after range filter
int g_news_totalDisplayable = 0; // Total events after all filters
//+------------------------------------------------------------------+
//| Dashboard Position and Visibility State |
//+------------------------------------------------------------------+
int g_news_dashboardX = 50; // Dashboard X position on chart
int g_news_dashboardY = 50; // Dashboard Y position on chart
bool g_news_dashboardVisible = false; // Dashboard visibility flag
bool g_news_canvasExists = false; // Canvas created flag
bool g_news_dragging = false; // Drag-move in progress flag
int g_news_dragOffsetX = 0; // X offset from dashboard origin at drag start
int g_news_dragOffsetY = 0; // Y offset from dashboard origin at drag start
//+------------------------------------------------------------------+
//| Table Scratch Canvas |
//+------------------------------------------------------------------+
CNewsCanvasFast g_news_tableTmp; // Offscreen scratch canvas for row clipping
bool g_news_tableTmpReady = false; // Scratch canvas ready flag
int g_news_tableTmpW = 0; // Scratch canvas width
int g_news_tableTmpH = 0; // Scratch canvas height
//+------------------------------------------------------------------+
//| Mouse State |
//+------------------------------------------------------------------+
int g_news_mouseLx = -1; // Cached canvas-local mouse X
int g_news_mouseLy = -1; // Cached canvas-local mouse Y
//+------------------------------------------------------------------+
//| Hover Codes |
//+------------------------------------------------------------------+
#define NEWS_HOV_NONE 0 // No element hovered
#define NEWS_HOV_CLOSE 1 // Close button hovered
#define NEWS_HOV_THEME 2 // Theme toggle hovered
#define NEWS_HOV_DRAG 3 // Drag handle hovered
#define NEWS_HOV_RESIZE_R 4 // Right-edge resize zone hovered
#define NEWS_HOV_RESIZE_B 5 // Bottom-edge resize zone hovered
#define NEWS_HOV_FILTER_CURR 10 // Currency filter toggle hovered
#define NEWS_HOV_FILTER_IMP 11 // Impact filter toggle hovered
#define NEWS_HOV_FILTER_TIME 12 // Time filter toggle hovered
#define NEWS_HOV_CURR_BASE 100 // Currency chip base offset (+0..7)
#define NEWS_HOV_IMP_BASE 200 // Impact pill base offset (+0..3)
#define NEWS_HOV_ROW_BASE 300 // Event row base offset (+0..N-1)
//+------------------------------------------------------------------+
//| Interaction and Change Tracking State |
//+------------------------------------------------------------------+
int g_news_hover = NEWS_HOV_NONE; // Current hover code
int g_news_lastEventCount = -1; // Event count at last redraw
datetime g_news_lastEventStamp = 0; // Timestamp at last event load
bool g_news_filtersChanged = true; // Filters dirty flag
//+------------------------------------------------------------------+
//| Trade State |
//+------------------------------------------------------------------+
bool g_news_tradeExecuted = false; // Trade already fired flag
datetime g_news_tradedNewsTime = 0; // Timestamp of the traded news event
long g_news_triggeredIds[]; // IDs of events that already fired trades
//+------------------------------------------------------------------+
//| Toast State |
//+------------------------------------------------------------------+
string g_news_toastText = ""; // Current toast message text
bool g_news_toastIsError = false; // True when toast represents an error
ulong g_news_toastExpiryMs = 0; // Toast expiry tick count in milliseconds
//+------------------------------------------------------------------+
//| Timer State |
//+------------------------------------------------------------------+
ulong g_news_lastBlinkMs = 0; // Tick count of last blink cycle
//+------------------------------------------------------------------+
//| Set toast notification and start 5-second expiry |
//+------------------------------------------------------------------+
void News_ShowToast(string text, bool isError)
{
//--- Store message and error flag
g_news_toastText = text;
g_news_toastIsError = isError;
//--- Schedule expiry 5 seconds from now
g_news_toastExpiryMs = GetTickCount64() + 5000;
}
//+------------------------------------------------------------------+
//| Initialize default filter selections - all on |
//+------------------------------------------------------------------+
void News_InitDefaultFilters()
{
//--- Enable all currency filters
for(int i = 0; i < NEWS_CURR_COUNT; i++) g_news_currSelected[i] = true;
//--- Enable all impact filters
for(int i = 0; i < NEWS_IMPACT_COUNT; i++) g_news_impactSelected[i] = true;
//--- Enable all master filter toggles
g_news_filterCurrencyOn = true;
g_news_filterImpactOn = true;
g_news_filterTimeOn = true;
//--- Mark filters as dirty to trigger rebuild
g_news_filtersChanged = true;
}
//+------------------------------------------------------------------+
//| Scroll state structure |
//+------------------------------------------------------------------+
struct NewsScrollState
{
int scrollPx; // Current scroll offset in pixels
int totalH; // Total content height in pixels
int viewportH; // Visible viewport height
int trackL; // Track left X
int trackT; // Track top Y
int trackR; // Track right X
int trackB; // Track bottom Y
bool dragging; // Drag in progress flag
int dragStartY; // Mouse Y at drag start
int dragStartScroll; // scrollPx value at drag start
bool hover; // Cursor over track or thumb
bool hoveredArea; // Cursor over track area
bool hoveredThumb; // Cursor over thumb specifically
};
//+------------------------------------------------------------------+
//| Initialize scroll state to defaults |
//+------------------------------------------------------------------+
void News_ScrollInit(NewsScrollState &s)
{
//--- Zero all scroll fields
s.scrollPx = 0;
s.totalH = 0;
s.viewportH = 0;
s.trackL = 0;
s.trackT = 0;
s.trackR = 0;
s.trackB = 0;
s.dragging = false;
s.dragStartY = 0;
s.dragStartScroll = 0;
s.hover = false;
s.hoveredArea = false;
s.hoveredThumb = false;
}
//+------------------------------------------------------------------+
//| Compute maximum scroll position |
//+------------------------------------------------------------------+
int News_ScrollMax(NewsScrollState &s)
{
//--- Return positive overflow or zero
const int m = s.totalH - s.viewportH;
return (m > 0) ? m : 0;
}
//+------------------------------------------------------------------+
//| Clamp current scroll position to valid range |
//+------------------------------------------------------------------+
void News_ScrollClamp(NewsScrollState &s)
{
//--- Enforce lower and upper scroll bounds
const int m = News_ScrollMax(s);
if(s.scrollPx < 0) s.scrollPx = 0;
if(s.scrollPx > m) s.scrollPx = m;
}
//+------------------------------------------------------------------+
//| Test if scrollbar should be visible |
//+------------------------------------------------------------------+
bool News_ScrollVisible(NewsScrollState &s)
{
//--- Scrollbar appears only when content exceeds viewport
return s.totalH > s.viewportH;
}
//+------------------------------------------------------------------+
//| Compute thumb rectangle within track |
//+------------------------------------------------------------------+
void News_ScrollThumbRect(NewsScrollState &s, int &outT, int &outB)
{
//--- Handle degenerate track
const int trackH = s.trackB - s.trackT;
if(trackH <= 0 || s.totalH <= 0)
{
outT = s.trackT; outB = s.trackB;
return;
}
//--- Compute thumb height proportional to viewport/total ratio
const double ratio = (double)s.viewportH / (double)s.totalH;
int thumbH = (int)(trackH * ratio);
if(thumbH < 20) thumbH = 20;
if(thumbH > trackH) thumbH = trackH;
//--- Position thumb proportional to current scroll offset
const int m = News_ScrollMax(s);
const double scrollRatio = (m > 0) ? ((double)s.scrollPx / (double)m) : 0.0;
const int avail = trackH - thumbH;
outT = s.trackT + (int)(avail * scrollRatio);
outB = outT + thumbH;
}
//+------------------------------------------------------------------+
//| Hit-test scrollbar thumb |
//+------------------------------------------------------------------+
bool News_ScrollHitThumb(NewsScrollState &s, int x, int y)
{
//--- Reject points outside the track lane
if(x < s.trackL - 2 || x > s.trackR + 2) return false;
//--- Test Y against current thumb bounds
int tT, tB;
News_ScrollThumbRect(s, tT, tB);
return (y >= tT && y < tB);
}
//+------------------------------------------------------------------+
//| Begin scroll drag from mouse y position |
//+------------------------------------------------------------------+
void News_ScrollBeginDrag(NewsScrollState &s, int y)
{
//--- Record drag start state
s.dragging = true;
s.dragStartY = y;
s.dragStartScroll = s.scrollPx;
}
//+------------------------------------------------------------------+
//| Update drag with current mouse y |
//+------------------------------------------------------------------+
void News_ScrollUpdateDrag(NewsScrollState &s, int y)
{
//--- Abort if not in drag state
if(!s.dragging) return;
const int trackH = s.trackB - s.trackT;
if(trackH <= 0) return;
//--- Convert pixel delta to scroll units and apply
const int dy = y - s.dragStartY;
const double scale = (double)s.totalH / (double)trackH;
const int newScroll = s.dragStartScroll + (int)(dy * scale);
s.scrollPx = newScroll;
News_ScrollClamp(s);
}
//+------------------------------------------------------------------+
//| End scroll drag |
//+------------------------------------------------------------------+
void News_ScrollEndDrag(NewsScrollState &s)
{
//--- Clear drag flag
s.dragging = false;
}
//+------------------------------------------------------------------+
//| Apply mouse wheel delta to scroll position |
//+------------------------------------------------------------------+
void News_ScrollByWheel(NewsScrollState &s, int delta, int step)
{
//--- Scroll up when delta is positive, down when negative
if(delta > 0) s.scrollPx -= step;
else s.scrollPx += step;
News_ScrollClamp(s);
}
//+------------------------------------------------------------------+
//| Draw scrollbar with fully-rounded thumb onto canvas |
//+------------------------------------------------------------------+
void News_ScrollDraw(CCanvas &canv, NewsScrollState &s)
{
//--- Skip when scrollbar is not needed
if(!News_ScrollVisible(s)) return;
const int trackW = s.trackR - s.trackL;
if(trackW <= 0) return;
//--- Compute thumb position
int tT, tB;
News_ScrollThumbRect(s, tT, tB);
const int thumbH = tB - tT;
if(thumbH <= 0) return;
//--- Pick thumb color based on current interaction state
color thumbColor = g_news_scrollSlider;
if(s.dragging) thumbColor = g_news_scrollSliderDrag;
else if(s.hoveredThumb || s.hover) thumbColor = g_news_scrollSliderHover;
//--- Draw fully-rounded thumb with radius equal to half track width
const int radius = trackW / 2;
News_FillRoundRect(canv, s.trackL, tT, trackW, thumbH, radius, ColorToARGB(thumbColor, 255));
}
//+------------------------------------------------------------------+
//| Global Scroll State |
//+------------------------------------------------------------------+
NewsScrollState g_news_tableScroll; // Scroll state for the event table
//+------------------------------------------------------------------+
//| Format datetime as "HH:MM" string |
//+------------------------------------------------------------------+
string News_FormatTimeShort(datetime t)
{
//--- Delegate to built-in time formatter with minutes precision
return TimeToString(t, TIME_MINUTES);
}
//+------------------------------------------------------------------+
//| Get impact color for a label string |
//+------------------------------------------------------------------+
color News_GetImpactColor(string label)
{
//--- Map label to corresponding theme color
if(label == "High") return g_news_impHigh;
if(label == "Medium") return g_news_impMed;
if(label == "Low") return g_news_impLow;
return g_news_impNone;
}
//+------------------------------------------------------------------+
//| Get importance enum from label string |
//+------------------------------------------------------------------+
ENUM_CALENDAR_EVENT_IMPORTANCE News_GetImpactEnum(string label)
{
//--- Map label to corresponding enum value
if(label == "High") return CALENDAR_IMPORTANCE_HIGH;
if(label == "Medium") return CALENDAR_IMPORTANCE_MODERATE;
if(label == "Low") return CALENDAR_IMPORTANCE_LOW;
return CALENDAR_IMPORTANCE_NONE;
}
//+------------------------------------------------------------------+
//| Get importance label from enum value |
//+------------------------------------------------------------------+
string News_GetImpactLabel(ENUM_CALENDAR_EVENT_IMPORTANCE imp)
{
//--- Map enum to display label string
if(imp == CALENDAR_IMPORTANCE_HIGH) return "High";
if(imp == CALENDAR_IMPORTANCE_MODERATE) return "Medium";
if(imp == CALENDAR_IMPORTANCE_LOW) return "Low";
return "None";
}
//+------------------------------------------------------------------+
//| Format calendar value with unit, multiplier, and digits metadata |
//+------------------------------------------------------------------+
string News_FormatValue(bool hasValue, double v, int unit, int multiplier, int digits)
{
//--- Return dash when value is not published
if(!hasValue) return "-";
//--- Determine multiplier suffix
string suffix = "";
if(multiplier == CALENDAR_MULTIPLIER_THOUSANDS) suffix = "K";
else if(multiplier == CALENDAR_MULTIPLIER_MILLIONS) suffix = "M";
else if(multiplier == CALENDAR_MULTIPLIER_BILLIONS) suffix = "B";
else if(multiplier == CALENDAR_MULTIPLIER_TRILLIONS) suffix = "T";
//--- Clamp decimal places to valid range
int d = digits;
if(d < 0) d = 0;
if(d > 6) d = 6;
string num = DoubleToString(v, d);
//--- Apply currency prefix for USD unit
if(unit == CALENDAR_UNIT_USD) return "$" + num + suffix;
//--- Apply percent suffix for percent unit
if(unit == CALENDAR_UNIT_PERCENT) return num + "%" + suffix;
//--- Map remaining units to their textual suffix
string post = "";
if(unit == CALENDAR_UNIT_HOUR) post = " h";
else if(unit == CALENDAR_UNIT_JOB) post = " jobs";
else if(unit == CALENDAR_UNIT_RIG) post = " rigs";
else if(unit == CALENDAR_UNIT_PEOPLE) post = " ppl";
else if(unit == CALENDAR_UNIT_MORTGAGE) post = " mtg";
else if(unit == CALENDAR_UNIT_VOTE) post = " votes";
else if(unit == CALENDAR_UNIT_BARREL) post = " bbl";
else if(unit == CALENDAR_UNIT_CUBICFEET) post = " cf";
else if(unit == CALENDAR_UNIT_POSITION) post = " pos";
else if(unit == CALENDAR_UNIT_BUILDING) post = " bldg";
else if(unit == CALENDAR_UNIT_CURRENCY) post = "";
//--- Return number with suffix and unit postfix
return num + suffix + post;
}
//+------------------------------------------------------------------+
//| Format time-remaining string for an event row |
//+------------------------------------------------------------------+
string News_FormatRemain(datetime evTime, datetime now)
{
//--- Compute signed difference in seconds
const long diffSec = (long)evTime - (long)now;
//--- Show static label for past events
if(diffSec < -60) return "Released";
//--- Show "now" within 60 seconds of release
const long absSec = (diffSec < 0) ? -diffSec : diffSec;
if(absSec <= 60) return "now";
//--- Decompose absolute seconds into days, hours, minutes, seconds
const long mins = absSec / 60;
const long hours = mins / 60;
const long days = hours / 24;
string body;
if(days > 0)
{
const long remH = hours - days * 24;
body = IntegerToString(days) + "d " + IntegerToString(remH) + "h";
}
else if(hours > 0)
{
const long remM = mins - hours * 60;
body = IntegerToString(hours) + "h " + IntegerToString(remM) + "m";
}
else if(mins > 0)
{
const long remS = absSec - mins * 60;
body = IntegerToString(mins) + "m " + IntegerToString(remS) + "s";
}
else
{
body = IntegerToString(absSec) + "s";
}
//--- Prefix with "in " for future events
return "in " + body;
}
//+------------------------------------------------------------------+
//| Test if event is upcoming and within 30 minutes |
//+------------------------------------------------------------------+
bool News_RemainIsImminent(datetime evTime, datetime now)
{
//--- Return true only when event is in the future and within 30 min
const long diffSec = (long)evTime - (long)now;
return (diffSec > 0 && diffSec <= 30 * 60);
}
#endif // NEWS_CORE_MQH