//+------------------------------------------------------------------+ //| 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 //+------------------------------------------------------------------+ //| 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