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