//+------------------------------------------------------------------+
//|                                                 News Interact.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_INTERACT_MQH
#define NEWS_INTERACT_MQH

//--- Include core data definitions
#include "News Core.mqh"
//--- Include trade logic handlers
#include "News Logic.mqh"
//--- Include canvas rendering routines
#include "News Render.mqh"

//+------------------------------------------------------------------+
//| Interaction State                                                |
//+------------------------------------------------------------------+
bool g_news_scrollDragging = false; // Scrollbar thumb drag in progress flag

//+------------------------------------------------------------------+
//| Convert chart coordinates to canvas-local coordinates            |
//+------------------------------------------------------------------+
void News_ChartToCanvas(int chartX, int chartY, int &localX, int &localY)
  {
//--- Subtract dashboard origin to get canvas-local coordinates
   localX = chartX - g_news_dashboardX;
   localY = chartY - g_news_dashboardY;
  }

//+------------------------------------------------------------------+
//| Hit-test canvas-local coordinates and return hover code          |
//+------------------------------------------------------------------+
int News_HitTest(int lx, int ly)
  {
//--- Test right-edge resize hot zone along the full edge minus corner radii
   {
      const int cornerR    = 8;
      const int edgeMargin = 4;
      const int hotTop     = cornerR + edgeMargin;
      const int hotBot     = NEWS_DASHBOARD_H - cornerR - edgeMargin;
      if(lx >= g_news_dashW - NEWS_RESIZE_HOT_W && lx <= g_news_dashW
         && ly >= hotTop && ly <= hotBot)
         return NEWS_HOV_RESIZE_R;
   }
//--- Test bottom-edge resize hot zone along the full edge minus corner radii
   {
      const int cornerR    = 8;
      const int edgeMargin = 4;
      const int hotLeft    = cornerR + edgeMargin;
      const int hotRight   = NEWS_DASHBOARD_W - cornerR - edgeMargin;
      if(ly >= g_news_dashH - NEWS_RESIZE_HOT_H && ly <= g_news_dashH
         && lx >= hotLeft && lx <= hotRight)
         return NEWS_HOV_RESIZE_B;
   }
//--- Test theme toggle button
   if(News_PointInRect(lx, ly, g_news_themeL, g_news_themeT,
                       g_news_themeR - g_news_themeL, g_news_themeB - g_news_themeT))
      return NEWS_HOV_THEME;
//--- Test close button
   if(News_PointInRect(lx, ly, g_news_closeL, g_news_closeT,
                       g_news_closeR - g_news_closeL, g_news_closeB - g_news_closeT))
      return NEWS_HOV_CLOSE;
//--- Test currency filter master toggle
   if(News_PointInRect(lx, ly, g_news_currTglL, g_news_currTglT,
                       g_news_currTglR - g_news_currTglL, g_news_currTglB - g_news_currTglT))
      return NEWS_HOV_FILTER_CURR;
//--- Test impact filter master toggle
   if(News_PointInRect(lx, ly, g_news_impTglL, g_news_impTglT,
                       g_news_impTglR - g_news_impTglL, g_news_impTglB - g_news_impTglT))
      return NEWS_HOV_FILTER_IMP;
//--- Test time filter master toggle
   if(News_PointInRect(lx, ly, g_news_timeTglL, g_news_timeTglT,
                       g_news_timeTglR - g_news_timeTglL, g_news_timeTglB - g_news_timeTglT))
      return NEWS_HOV_FILTER_TIME;
//--- Test each currency chip
   for(int i = 0; i < NEWS_CURR_COUNT; i++)
     {
      if(News_PointInRect(lx, ly, g_news_currL[i], g_news_currT[i],
                          g_news_currR[i] - g_news_currL[i], g_news_currB[i] - g_news_currT[i]))
         return NEWS_HOV_CURR_BASE + i;
     }
//--- Test each impact pill
   for(int i = 0; i < NEWS_IMPACT_COUNT; i++)
     {
      if(News_PointInRect(lx, ly, g_news_impL[i], g_news_impT[i],
                          g_news_impR[i] - g_news_impL[i], g_news_impB[i] - g_news_impT[i]))
         return NEWS_HOV_IMP_BASE + i;
     }
//--- Test each visible event row
   for(int r = 0; r < g_news_visibleRowCount; r++)
     {
      if(News_PointInRect(lx, ly, g_news_rowL[r], g_news_rowT[r],
                          g_news_rowR[r] - g_news_rowL[r], g_news_rowB[r] - g_news_rowT[r]))
         return NEWS_HOV_ROW_BASE + r;
     }
//--- Treat the header strip as the drag region when no button matched
   if(ly >= 0 && ly < NEWS_HEADER_H) return NEWS_HOV_DRAG;
   return NEWS_HOV_NONE;
  }

//+------------------------------------------------------------------+
//| Test if point falls inside the events table viewport area        |
//+------------------------------------------------------------------+
bool News_PointInTableArea(int lx, int ly)
  {
//--- Compute table vertical bounds and test point
   const int rowsTop = News_TableRowsTop();
   const int rowsBot = News_TableRowsBottom();
   return (lx >= NEWS_SIDE_PAD && lx < NEWS_DASHBOARD_W - NEWS_SIDE_PAD
           && ly >= rowsTop && ly < rowsBot);
  }

//+------------------------------------------------------------------+
//| Handle action triggered by hover code                            |
//+------------------------------------------------------------------+
void News_HandleAction(int hov, bool doubleClick = false)
  {
//--- Close dashboard on close button click
   if(hov == NEWS_HOV_CLOSE)
     {
      g_news_dashboardVisible = false;
      News_DestroyCanvas();
      return;
     }
//--- Toggle dark/light theme on theme button click
   if(hov == NEWS_HOV_THEME)
     {
      News_ApplyTheme(!g_news_darkTheme);
      News_RenderAll();
      ChartRedraw();
      return;
     }
//--- Toggle currency filter master switch
   if(hov == NEWS_HOV_FILTER_CURR)
     {
      g_news_filterCurrencyOn = !g_news_filterCurrencyOn;
      g_news_filtersChanged   = true;
      News_RefreshEvents();
      News_RenderAll();
      ChartRedraw();
      return;
     }
//--- Toggle impact filter master switch
   if(hov == NEWS_HOV_FILTER_IMP)
     {
      g_news_filterImpactOn = !g_news_filterImpactOn;
      g_news_filtersChanged = true;
      News_RefreshEvents();
      News_RenderAll();
      ChartRedraw();
      return;
     }
//--- Toggle time range filter master switch
   if(hov == NEWS_HOV_FILTER_TIME)
     {
      g_news_filterTimeOn   = !g_news_filterTimeOn;
      g_news_filtersChanged = true;
      News_RefreshEvents();
      News_RenderAll();
      ChartRedraw();
      return;
     }
//--- Toggle individual currency chip selection
   if(hov >= NEWS_HOV_CURR_BASE && hov < NEWS_HOV_CURR_BASE + NEWS_CURR_COUNT)
     {
      const int idx = hov - NEWS_HOV_CURR_BASE;
      g_news_currSelected[idx] = !g_news_currSelected[idx];
      g_news_filtersChanged    = true;
      News_RefreshEvents();
      News_RenderAll();
      ChartRedraw();
      return;
     }
//--- Toggle individual impact pill selection
   if(hov >= NEWS_HOV_IMP_BASE && hov < NEWS_HOV_IMP_BASE + NEWS_IMPACT_COUNT)
     {
      const int idx = hov - NEWS_HOV_IMP_BASE;
      g_news_impactSelected[idx] = !g_news_impactSelected[idx];
      g_news_filtersChanged      = true;
      News_RefreshEvents();
      News_RenderAll();
      ChartRedraw();
      return;
     }
//--- Dispatch row click by row kind
   if(hov >= NEWS_HOV_ROW_BASE && hov < NEWS_HOV_ROW_BASE + NEWS_MAX_VISIBLE_ROWS)
     {
      const int rIdx = hov - NEWS_HOV_ROW_BASE;
      if(rIdx < g_news_visibleRowCount)
        {
         const int planIdx = g_news_rowEventIdx[rIdx];
         if(planIdx >= 0 && planIdx < ArraySize(g_news_rowPlan))
           {
            const int kind = g_news_rowPlan[planIdx].kind;
            //--- Toggle day collapse on double-click of a day separator row
            if(kind == NEWS_ROW_KIND_DAY)
              {
               if(doubleClick)
                 {
                  const string dateKey = g_news_rowPlan[planIdx].dateKey;
                  News_ToggleDayCollapsed(dateKey);
                  News_BuildRowPlan();
                  News_ScrollClamp(g_news_tableScroll);
                 }
              }
            //--- Show event details in toast on single event row click
            else if(kind == NEWS_ROW_KIND_EVENT)
              {
               const int evIdx = g_news_rowPlan[planIdx].eventIdx;
               if(evIdx >= 0 && evIdx < ArraySize(g_news_displayableEvents))
                 {
                  const NewsEvent ev  = g_news_displayableEvents[evIdx];
                  string msg          = ev.eventDate + " " + ev.eventTime + "  "
                                        + ev.currency + "  " + ev.event;
                  News_ShowToast(msg, false);
                 }
              }
           }
        }
      News_RenderAll();
      ChartRedraw();
      return;
     }
  }

//+------------------------------------------------------------------+
//| Create main and separator canvas bitmap labels                   |
//+------------------------------------------------------------------+
bool News_CreateCanvas()
  {
//--- Return early if canvas already exists
   if(g_news_canvasExists) return true;
//--- Create main canvas at maximum dimensions for crop-based resizing
   if(!g_news_canv.CreateBitmapLabel(NEWS_CANVAS_NAME,
                                      g_news_dashboardX, g_news_dashboardY,
                                      NEWS_DASHBOARD_W_MAX, NEWS_DASHBOARD_H_MAX,
                                      COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("News_CreateCanvas: failed - ", GetLastError());
      return false;
     }
//--- Configure main canvas object properties
   ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_BACK,       false);
   ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_HIDDEN,     true);
   ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_ZORDER,     1000);
//--- Suppress default tooltip; interact layer sets it dynamically for revised-value hover
   ObjectSetString(0,  NEWS_CANVAS_NAME, OBJPROP_TOOLTIP, "\n");
//--- Crop visible area to current dashboard size
   ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_XSIZE, g_news_dashW);
   ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_YSIZE, g_news_dashH);
   g_news_canvasExists = true;
//--- Create separators overlay canvas at same maximum dimensions
   if(g_news_canvSep.CreateBitmapLabel(NEWS_CANVAS_NAME_SEP,
                                        g_news_dashboardX, g_news_dashboardY,
                                        NEWS_DASHBOARD_W_MAX, NEWS_DASHBOARD_H_MAX,
                                        COLOR_FORMAT_ARGB_NORMALIZE))
     {
      //--- Configure separator canvas object properties
      ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_BACK,       false);
      ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_HIDDEN,     true);
      ObjectSetString(0,  NEWS_CANVAS_NAME_SEP, OBJPROP_TOOLTIP,    "\n");
      ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_ZORDER,     500);
      //--- Crop separator canvas to current dashboard size
      ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_XSIZE, g_news_dashW);
      ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_YSIZE, g_news_dashH);
      g_news_canvSepExists = true;
     }
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy main canvas and separators overlay                       |
//+------------------------------------------------------------------+
void News_DestroyCanvas()
  {
//--- Destroy separator canvas first if it exists
   if(g_news_canvSepExists)
     {
      g_news_canvSep.Destroy();
      ObjectDelete(0, NEWS_CANVAS_NAME_SEP);
      g_news_canvSepExists = false;
     }
//--- Abort if main canvas was never created
   if(!g_news_canvasExists) return;
//--- Destroy main canvas and remove chart object
   g_news_canv.Destroy();
   ObjectDelete(0, NEWS_CANVAS_NAME);
   g_news_canvasExists = false;
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Apply visible crop to both canvases without reallocating bitmaps |
//+------------------------------------------------------------------+
void News_ResizeCanvases(int newW, int newH)
  {
//--- Update visible crop on main canvas
   if(g_news_canvasExists)
     {
      ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_XSIZE, newW);
      ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_YSIZE, newH);
     }
//--- Update visible crop on separator canvas
   if(g_news_canvSepExists)
     {
      ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_XSIZE, newW);
      ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_YSIZE, newH);
     }
  }

//+------------------------------------------------------------------+
//| Show dashboard - create canvas, load data, and render            |
//+------------------------------------------------------------------+
void News_ShowDashboard()
  {
//--- Abort if canvas creation fails
   if(!News_CreateCanvas()) return;
//--- Mark dashboard visible and load events
   g_news_dashboardVisible = true;
   News_RefreshEvents();
//--- Enable mouse move and wheel chart events
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
//--- Render and push to chart
   News_RenderAll();
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Hide dashboard and destroy canvas                                |
//+------------------------------------------------------------------+
void News_HideDashboard()
  {
//--- Destroy all canvas objects and clear visibility flag
   News_DestroyCanvas();
   g_news_dashboardVisible = false;
  }

//+------------------------------------------------------------------+
//| Initialize program                                               |
//+------------------------------------------------------------------+
bool News_Init()
  {
//--- Apply default light theme
   News_ApplyTheme(false);
//--- Initialize currency/impact filters and scroll state
   News_InitDefaultFilters();
   News_ScrollInit(g_news_tableScroll);
//--- Set magic number on the trade helper object
   g_news_trade.SetExpertMagicNumber(20260507);
//--- Open the SQLite database; abort if it fails
   if(!News_DbOpen())
     {
      Print(">>> News DB: Failed to open database - cannot continue");
      return false;
     }
//--- Branch on tester vs live mode for data loading strategy
   if(MQLInfoInteger(MQL_TESTER))
     {
      //--- Load events from database for the configured backtest window
      Print(">>> News DB: Running in Strategy Tester, loading from: ", NEWS_DB_PATH, "...");
      const int loaded = News_DbLoadEventsForWindow(
                           (datetime)StartDate, (datetime)EndDate);
      if(loaded == 0)
        {
         //--- Instruct user to download data on a live chart first
         Print("================================================================");
         Print(">>> News DB: NO DATA FOUND FOR BACKTEST PERIOD");
         Print(">>> Go to a live chart and enable 'Download NEWS data for");
         Print(">>> offline testing?' with the same date range, then rerun.");
         Print("================================================================");
         return false;
        }
      //--- Log file size; use shared-read flags so file can be read while DB is open
      ResetLastError();
      int fh = FileOpen(NEWS_DB_PATH,
                        FILE_READ | FILE_BIN | FILE_COMMON | FILE_SHARE_READ | FILE_SHARE_WRITE);
      if(fh == INVALID_HANDLE)
        {
         Print(">>> News DB: Loaded ", loaded, " events from ", NEWS_DB_PATH,
               " (could not read file size, error ", GetLastError(), ")...");
        }
      else
        {
         //--- Format byte count into KB or MB for readability
         const ulong dbBytes = FileSize(fh);
         FileClose(fh);
         string sizeStr;
         if(dbBytes >= 1048576)
            sizeStr = DoubleToString((double)dbBytes / 1048576.0, 2) + " MB";
         else if(dbBytes >= 1024)
            sizeStr = DoubleToString((double)dbBytes / 1024.0, 1) + " KB";
         else
            sizeStr = IntegerToString((long)dbBytes) + " bytes";
         Print(">>> News DB: Loaded ", loaded, " events from ", NEWS_DB_PATH,
               " (size: ", dbBytes, " bytes / ", sizeStr, ")...");
        }
      News_ApplyFilters();
     }
   else
     {
      //--- Prune stale events and restore triggered IDs from database
      News_DbPruneOldEvents(7);
      News_DbLoadTriggered();
      //--- Download calendar data when user has enabled offline download
      if(inp_DownloadDataInLive)
        {
         Print(">>> News DB: inp_DownloadDataInLive=true - downloading ",
               TimeToString((datetime)inp_DownloadStartDate), " to ",
               TimeToString((datetime)inp_DownloadEndDate));
         News_DbDownloadAndSave((datetime)inp_DownloadStartDate,
                                 (datetime)inp_DownloadEndDate);
        }
      //--- Load current live window from MT5 calendar API
      const datetime now = TimeTradeServer();
      News_LoadEventsFromLive(now - PeriodSeconds(start_time),
                               now + PeriodSeconds(end_time));
      News_ApplyFilters();
     }
//--- Show dashboard after successful initialization
   News_ShowDashboard();
   return true;
  }

//+------------------------------------------------------------------+
//| Deinitialize program                                             |
//+------------------------------------------------------------------+
void News_Deinit()
  {
//--- Hide and destroy the dashboard canvas
   News_HideDashboard();
//--- Close the SQLite database before EA unloads
   News_DbClose();
//--- Destroy persistent table scratch canvas
   if(g_news_tableTmpReady)
     {
      g_news_tableTmp.Destroy();
      g_news_tableTmpReady = false;
     }
//--- Destroy primitives high-resolution fill canvas
   if(g_news_prim.m_hrFillReady)
     {
      g_news_prim.m_hrFill.Destroy();
      g_news_prim.m_hrFillReady = false;
     }
//--- Destroy primitives high-resolution border canvas
   if(g_news_prim.m_hrBorderReady)
     {
      g_news_prim.m_hrBorder.Destroy();
      g_news_prim.m_hrBorderReady = false;
     }
  }

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void News_OnTick()
  {
//--- Skip processing when dashboard is hidden
   if(!g_news_dashboardVisible) return;
//--- Refresh live events and DB every 30 seconds
   static datetime lastRefresh = 0;
   const datetime now = TimeCurrent();
   if(now - lastRefresh >= 30)
     {
      //--- Snapshot displayable state before refresh to detect changes
      const int      preCount = ArraySize(g_news_displayableEvents);
      const datetime preStamp = g_news_lastEventStamp;
      News_RefreshEvents();
      //--- Redraw only if event list actually changed
      const int      postCount = ArraySize(g_news_displayableEvents);
      const datetime postStamp = g_news_lastEventStamp;
      if(postCount != preCount || postStamp != preStamp)
        {
         News_RenderAll();
         ChartRedraw();
        }
      lastRefresh = now;
     }
//--- Check whether a news trade should be placed
   News_CheckForNewsTrade();
  }

//+------------------------------------------------------------------+
//| Expert timer function                                            |
//+------------------------------------------------------------------+
void News_OnTimer()
  {
//--- Skip processing when dashboard is hidden
   if(!g_news_dashboardVisible) return;
//--- Manage toast lifecycle; force render while toast is alive
   bool needFullRender = false;
   bool toastAlive     = false;
   if(StringLen(g_news_toastText) > 0)
     {
      //--- Clear expired toast and flag a final render
      if(GetTickCount64() > g_news_toastExpiryMs)
        {
         g_news_toastText     = "";
         g_news_toastExpiryMs = 0;
         needFullRender       = true;
        }
      else
        {
         //--- Toast is still live; animate progress bar this tick
         toastAlive = true;
        }
     }
//--- Determine if a full render is due this tick
   static ulong s_lastFullMs = 0;
   const ulong nowMs = GetTickCount64();
//--- Render when toast is alive, just expired, or 5-second interval elapsed
   if(toastAlive || needFullRender || (nowMs - s_lastFullMs) >= 5000)
     {
      News_RenderAll();
      ChartRedraw();
      s_lastFullMs = nowMs;
      return;
     }
//--- Fast path: update only changed Remain cells to reduce CPU cost
   if(News_TickRemainCells()) ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Expert chart event function                                      |
//+------------------------------------------------------------------+
void News_OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
//--- Skip all events when dashboard is not visible
   if(!g_news_dashboardVisible) return;

//--- Handle mouse move events (left-button state arrives in sparam)
   if(id == CHARTEVENT_MOUSE_MOVE)
     {
      const int mx     = (int)lparam;
      const int my     = (int)dparam;
      const int mstate = (int)StringToInteger(sparam);
      int lx, ly;
      News_ChartToCanvas(mx, my, lx, ly);
//--- Update cached canvas-local mouse position
      g_news_mouseLx = lx;
      g_news_mouseLy = ly;

//--- Process dashboard drag movement
      if(g_news_dragging)
        {
         //--- Release drag when left button is no longer held
         if((mstate & 1) == 0)
           {
            g_news_dragging = false;
           }
         else
           {
            //--- Clamp new dashboard position to chart bounds
            const int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
            const int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
            int newX = MathMax(0, MathMin(chartW - NEWS_DASHBOARD_W, mx - g_news_dragOffsetX));
            int newY = MathMax(0, MathMin(chartH - NEWS_DASHBOARD_H, my - g_news_dragOffsetY));
            g_news_dashboardX = newX;
            g_news_dashboardY = newY;
            //--- Move main canvas object to new position
            ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_XDISTANCE, newX);
            ObjectSetInteger(0, NEWS_CANVAS_NAME, OBJPROP_YDISTANCE, newY);
            //--- Move separator canvas in sync with main canvas
            if(g_news_canvSepExists)
              {
               ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_XDISTANCE, newX);
               ObjectSetInteger(0, NEWS_CANVAS_NAME_SEP, OBJPROP_YDISTANCE, newY);
              }
            ChartRedraw();
            return;
           }
        }

//--- Process right-edge horizontal resize drag
      if(g_news_resizing)
        {
         //--- Release resize when left button is no longer held
         if((mstate & 1) == 0)
           {
            g_news_resizing = false;
           }
         else
           {
            //--- Clamp new width to min/max bounds and chart width limit
            const int chartW       = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
            const int upperByChart = chartW - g_news_dashboardX;
            const int upperBound   = MathMin(NEWS_DASHBOARD_W_MAX, upperByChart);
            const int delta        = mx - g_news_resizeStartMouseX;
            int newW               = g_news_resizeStartW + delta;
            if(newW < NEWS_DASHBOARD_W_MIN) newW = NEWS_DASHBOARD_W_MIN;
            if(newW > upperBound)           newW = upperBound;
            //--- Skip render if width did not change
            if(newW != g_news_dashW)
              {
               const int oldW = g_news_dashW;
               g_news_dashW   = newW;
               //--- Expand: resize crop first then render to avoid blank edges
               if(newW > oldW)
                 {
                  News_ResizeCanvases(newW, NEWS_DASHBOARD_H);
                  News_RenderAll();
                 }
               //--- Contract: render first then crop to avoid stale content
               else
                 {
                  News_RenderAll();
                  News_ResizeCanvases(newW, NEWS_DASHBOARD_H);
                 }
               ChartRedraw();
              }
            return;
           }
        }

//--- Process bottom-edge vertical resize drag
      if(g_news_resizingV)
        {
         //--- Release resize when left button is no longer held
         if((mstate & 1) == 0)
           {
            g_news_resizingV = false;
           }
         else
           {
            //--- Clamp new height to min/max bounds and chart height limit
            const int chartH       = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
            const int upperByChart = chartH - g_news_dashboardY;
            const int upperBound   = MathMin(NEWS_DASHBOARD_H_MAX, upperByChart);
            const int delta        = my - g_news_resizeStartMouseY;
            int newH               = g_news_resizeStartH + delta;
            if(newH < NEWS_DASHBOARD_H_MIN) newH = NEWS_DASHBOARD_H_MIN;
            if(newH > upperBound)           newH = upperBound;
            //--- Skip render if height did not change
            if(newH != g_news_dashH)
              {
               const int oldH = g_news_dashH;
               g_news_dashH   = newH;
               //--- Expand: resize crop first then render
               if(newH > oldH)
                 {
                  News_ResizeCanvases(NEWS_DASHBOARD_W, newH);
                  News_RenderAll();
                 }
               //--- Contract: render first then crop
               else
                 {
                  News_RenderAll();
                  News_ResizeCanvases(NEWS_DASHBOARD_W, newH);
                 }
               ChartRedraw();
              }
            return;
           }
        }

//--- Process scrollbar thumb drag movement
      if(g_news_scrollDragging)
        {
         News_ScrollUpdateDrag(g_news_tableScroll, ly);
         //--- End drag when left button is released
         if((mstate & 1) == 0)
           {
            News_ScrollEndDrag(g_news_tableScroll);
            g_news_scrollDragging = false;
           }
         News_RenderAll();
         ChartRedraw();
         return;
        }

//--- Compute new hover code and flag redraw when it changes
      const int newHov    = News_HitTest(lx, ly);
      bool      needRedraw = (newHov != g_news_hover);
      g_news_hover         = newHov;

//--- Quantize cursor position to 4px steps to throttle handle redraws on resize edges
      const bool onResizeEdge = (newHov == NEWS_HOV_RESIZE_R || newHov == NEWS_HOV_RESIZE_B);
      const int  qx = (lx / 4) * 4;
      const int  qy = (ly / 4) * 4;
      if(onResizeEdge && (qx != g_news_cursorX || qy != g_news_cursorY)) needRedraw = true;
      g_news_cursorX = qx;
      g_news_cursorY = qy;

//--- Hit-test each row's revised-value triangle (10x10 hot zone around center)
      int newRevHov = -1;
      for(int rr = 0; rr < g_news_visibleRowCount; rr++)
        {
         const int triCx = g_news_revTriCx[rr];
         if(triCx < 0) continue;
         const int triCy = g_news_revTriCy[rr];
         if(lx >= triCx - 5 && lx <= triCx + 5 && ly >= triCy - 5 && ly <= triCy + 5)
           {
            newRevHov = rr;
            break;
           }
        }
//--- Update revised-value tooltip when the hovered row changes
      if(newRevHov != g_news_revisedHoverRow)
        {
         g_news_revisedHoverRow = newRevHov;
         string tip = "\n";
         if(newRevHov >= 0)
           {
            //--- Translate visible row index to plan index then to event
            const int planIdx = g_news_rowEventIdx[newRevHov];
            if(planIdx >= 0 && planIdx < ArraySize(g_news_rowPlan))
              {
               const NewsRowEntry entry = g_news_rowPlan[planIdx];
               if(entry.kind == NEWS_ROW_KIND_EVENT
                  && entry.eventIdx >= 0
                  && entry.eventIdx < ArraySize(g_news_displayableEvents))
                 {
                  const NewsEvent ev = g_news_displayableEvents[entry.eventIdx];
                  //--- Build "Revised from X" tooltip string
                  if(ev.hasRevised)
                    {
                     const string fromStr = News_FormatValue(ev.hasPrevious, ev.previous,
                                                              ev.unit, ev.multiplier, ev.digits);
                     tip = "Revised from " + fromStr;
                    }
                 }
              }
           }
         //--- Push tooltip string to chart object
         ObjectSetString(0, NEWS_CANVAS_NAME, OBJPROP_TOOLTIP, tip);
        }

//--- Update scrollbar hover flags; thumb highlight requires precise hit
      const bool scrollVis = News_ScrollVisible(g_news_tableScroll);
      bool sbThumb = false, sbArea = false;
      if(scrollVis)
        {
         sbThumb = News_ScrollHitThumb(g_news_tableScroll, lx, ly);
         sbArea  = News_PointInTableArea(lx, ly);
        }
      if(sbThumb != g_news_tableScroll.hoveredThumb) needRedraw = true;
      if(sbArea  != g_news_tableScroll.hoveredArea)  needRedraw = true;
      g_news_tableScroll.hoveredThumb = sbThumb;
      g_news_tableScroll.hoveredArea  = sbArea;
//--- Hover flag tracks only the thumb itself, not the broader track area
      const bool prevHover         = g_news_tableScroll.hover;
      g_news_tableScroll.hover     = sbThumb;
      if(prevHover != g_news_tableScroll.hover) needRedraw = true;

//--- Disable chart mouse scroll when cursor is over the dashboard
      const bool overDash = (lx >= 0 && lx < NEWS_DASHBOARD_W && ly >= 0 && ly < NEWS_DASHBOARD_H);
      ChartSetInteger(0, CHART_MOUSE_SCROLL, !overDash);

//--- Detect fresh press, release, and double-click
      static int   prevMouseState = 0;
      static ulong lastClickMs    = 0;
      static int   lastClickX     = -9999;
      static int   lastClickY     = -9999;
      const bool freshPress   = (prevMouseState == 0 && mstate == 1);
      const bool freshRelease = (prevMouseState == 1 && mstate == 0);
      prevMouseState = mstate;

//--- Identify double-click by proximity and timing against previous press
      bool wasDoubleClick = false;
      if(freshPress)
        {
         const ulong nowClick = GetTickCount64();
         const int   ddx      = lx - lastClickX;
         const int   ddy      = ly - lastClickY;
         if(nowClick - lastClickMs < 500 && (ddx * ddx + ddy * ddy) < 25)
            wasDoubleClick = true;
         lastClickMs = nowClick;
         lastClickX  = lx;
         lastClickY  = ly;
        }

//--- Dispatch press to the appropriate interaction handler
      if(freshPress)
        {
         //--- Begin scrollbar thumb drag
         if(scrollVis && News_ScrollHitThumb(g_news_tableScroll, lx, ly))
           {
            News_ScrollBeginDrag(g_news_tableScroll, ly);
            g_news_scrollDragging = true;
            needRedraw            = true;
           }
         //--- Begin header drag-move
         else if(newHov == NEWS_HOV_DRAG)
           {
            g_news_dragging    = true;
            g_news_dragOffsetX = lx;
            g_news_dragOffsetY = ly;
           }
         //--- Begin right-edge resize drag
         else if(newHov == NEWS_HOV_RESIZE_R)
           {
            g_news_resizing          = true;
            g_news_resizeStartMouseX = mx;
            g_news_resizeStartW      = g_news_dashW;
           }
         //--- Begin bottom-edge resize drag
         else if(newHov == NEWS_HOV_RESIZE_B)
           {
            g_news_resizingV         = true;
            g_news_resizeStartMouseY = my;
            g_news_resizeStartH      = g_news_dashH;
           }
         //--- Dispatch generic button or row action
         else if(newHov != NEWS_HOV_NONE)
           {
            News_HandleAction(newHov, wasDoubleClick);
            return;
           }
        }

//--- Clear all drag states on left-button release
      if(freshRelease)
        {
         g_news_dragging  = false;
         g_news_resizing  = false;
         g_news_resizingV = false;
         if(g_news_scrollDragging)
           {
            News_ScrollEndDrag(g_news_tableScroll);
            g_news_scrollDragging = false;
            needRedraw            = true;
           }
        }

//--- Redraw if any hover or state change occurred
      if(needRedraw)
        {
         News_RenderAll();
         ChartRedraw();
        }
      return;
     }

//--- Handle mouse wheel: scroll events table when cursor is over table area
   if(id == CHARTEVENT_MOUSE_WHEEL)
     {
      const int mx    = (int)(short)lparam;
      const int my    = (int)(short)(lparam >> 16);
      const int delta = (int)dparam;
      int lx, ly;
      News_ChartToCanvas(mx, my, lx, ly);
      if(News_PointInTableArea(lx, ly) && News_ScrollVisible(g_news_tableScroll))
        {
         //--- Suppress chart scroll and apply wheel to dashboard table
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         News_ScrollByWheel(g_news_tableScroll, delta, NEWS_ROW_H);
         News_RenderAll();
         ChartRedraw();
         return;
        }
      //--- Pass wheel through to chart when cursor is not over table
      ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
      return;
     }

//--- Handle chart resize: re-render to adapt to new chart dimensions
   if(id == CHARTEVENT_CHART_CHANGE)
     {
      News_RenderAll();
      ChartRedraw();
      return;
     }
  }

#endif // NEWS_INTERACT_MQH