﻿//+------------------------------------------------------------------+
//|                                 ToolsPalette_Engine_Interact.mqh |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#ifndef TOOLS_PALETTE_ENGINE_INTERACT_MQH
#define TOOLS_PALETTE_ENGINE_INTERACT_MQH

//--- Include CDrawingEngine class declaration; guard prevents double-inclusion via the main chain
#include "ToolsPalette_Tools.mqh"

//+------------------------------------------------------------------+
//| Test whether point (px, py) lies inside a convex rotated quad   |
//+------------------------------------------------------------------+
bool CDrawingEngine::PointInRotatedRect(int px, int py, const int &cx[], const int &cy[])
  {
   //--- Track whether any cross products are positive or negative
   bool anyPos = false, anyNeg = false;
   //--- Iterate over all 4 edges of the convex quad
   for(int i = 0; i < 4; i++)
     {
      //--- Wrap edge index to form a closed polygon
      int j = (i + 1) % 4;
      //--- Compute edge vector from corner i to corner j
      int ex = cx[j] - cx[i];
      int ey = cy[j] - cy[i];
      //--- Compute vector from corner i to the test point
      int fx = px - cx[i];
      int fy = py - cy[i];
      //--- Compute the 2D cross product z-component
      long crossZ = (long)ex * fy - (long)ey * fx;
      //--- Record the sign of this cross product
      if(crossZ > 0) anyPos = true;
      if(crossZ < 0) anyNeg = true;
      //--- Both signs found — point is outside the quad
      if(anyPos && anyNeg) return false;
     }
   //--- All cross products had the same sign — point is inside
   return true;
  }

//+------------------------------------------------------------------+
//| Hit test: rectangle body including interior fill                 |
//+------------------------------------------------------------------+
bool CDrawingEngine::HitTestRectangle(int mx, int my, int x1, int y1, int x2, int y2)
  {
   //--- Normalize corner coordinates so min/max are unambiguous
   int lx = MathMin(x1, x2), rx = MathMax(x1, x2);
   int ty = MathMin(y1, y2), by = MathMax(y1, y2);
   //--- Accept any click inside the interior or threshold border band (translucent fill makes all interior valid)
   return (mx >= lx - m_hitThreshold && mx <= rx + m_hitThreshold &&
           my >= ty - m_hitThreshold && my <= by + m_hitThreshold);
  }

//+------------------------------------------------------------------+
//| Hit test: ellipse border circumference only                      |
//+------------------------------------------------------------------+
bool CDrawingEngine::HitTestEllipse(int mx, int my, int cx, int cy, int rx, int ry)
  {
   //--- Reject degenerate ellipses with zero radius
   if(rx < 1 || ry < 1) return false;
   //--- Compute normalized distance from center (1.0 = on the ellipse border)
   double dx = (double)(mx - cx) / rx;
   double dy = (double)(my - cy) / ry;
   double dist = MathSqrt(dx * dx + dy * dy);
   //--- Convert the pixel hit threshold to normalized ellipse space
   double normThresh = (double)m_hitThreshold / MathMin(rx, ry);
   //--- Accept if the normalized distance is within threshold of 1.0
   return MathAbs(dist - 1.0) <= normThresh;
  }

//+------------------------------------------------------------------+
//| Hit test a specific object's handles — return handle index       |
//+------------------------------------------------------------------+
int CDrawingEngine::HitTestHandles(int mx, int my, int objIdx)
  {
   //--- Reject invalid object index
   if(objIdx < 0 || objIdx >= ArraySize(m_drawnObjects)) return -1;
   //--- Resolve P1 screen coordinates as the baseline anchor position
   int hx1 = 0, hy1 = 0, hx2 = 0, hy2 = 0, hx3 = 0, hy3 = 0;
   ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[objIdx].time1, m_drawnObjects[objIdx].price1, hx1, hy1);

   //--- Override handle X for TOOL_HLINE: drawn at right edge minus 150 px, not at chart origin
   if(m_drawnObjects[objIdx].toolType == TOOL_HLINE)
     {
      int cW = m_canvasDrawings.Width();
      hx1 = cW - 150;
      //--- Clamp to a minimum 20 px from the right edge for very narrow charts
      if(hx1 < 20) hx1 = cW - 20;
     }
   //--- Override handle Y for TOOL_VLINE: drawn near the bottom edge
   else if(m_drawnObjects[objIdx].toolType == TOOL_VLINE)
     {
      int cH = m_canvasDrawings.Height();
      hy1 = cH - 150;
      //--- Clamp to a minimum 20 px from the bottom edge
      if(hy1 < 20) hy1 = cH - 20;
     }
   //--- TOOL_CROSS_LINE handle sits at the intersection (hx1, hy1); no override needed

   //--- Handle hit radius is 10 px — larger than m_hitThreshold (8 px) so handle always fires before body-only zone

   //--- TOOL_CIRCLE: idx 0 = center (visibility mirrors render hide rule), idx 1 = border point (always hittable)
   if(m_drawnObjects[objIdx].toolType == TOOL_CIRCLE)
     {
      //--- Resolve the border handle screen position
      ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[objIdx].time2, m_drawnObjects[objIdx].price2, hx2, hy2);
      //--- Test border handle first (idx 1) — always hittable
      if(MathAbs(mx - hx2) <= 10 && MathAbs(my - hy2) <= 10) return 1;
      //--- Replicate the renderer's center-handle hide logic exactly
      bool hasText  = StringLen(m_drawnObjects[objIdx].labelText) > 0;
      bool isSel    = (m_drawnObjects[objIdx].id == m_selectedObjectId);
      bool isHov    = (m_drawnObjects[objIdx].id == m_hoveredObjectId);
      bool cursorOnHandleForThisObj = (m_hoveredHandleHostId == m_drawnObjects[objIdx].id &&
                                       m_hoveredHandleIdx >= 0);
      bool anyDragging_ = (m_isDraggingHandle || m_isDraggingObject);
      //--- Determine whether the "+ Add text" prompt is actively shown for this object
      bool showingPrompt = isSel && isHov && !hasText && !m_isEditingLabel &&
                           !cursorOnHandleForThisObj && !anyDragging_;
      bool activelyEditing = m_isEditingLabel && isSel;
      //--- Center handle hidden if object has text, prompt is showing, or label is being edited
      bool centerHidden = hasText || showingPrompt || activelyEditing;
      //--- Test center handle (idx 0) only when it is visible
      if(!centerHidden && MathAbs(mx - hx1) <= 10 && MathAbs(my - hy1) <= 10) return 0;
      return -1;
     }

   //--- TOOL_ARC: idx 0 = P1 chord start, idx 1 = P2 chord end (generic test below), idx 2 = clamped apex
   if(m_drawnObjects[objIdx].toolType == TOOL_ARC &&
      m_drawnObjects[objIdx].time2 != 0 &&
      m_drawnObjects[objIdx].time3 != 0)
     {
      //--- Resolve chord endpoints in screen space
      ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[objIdx].time2, m_drawnObjects[objIdx].price2, hx2, hy2);
      ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[objIdx].time3, m_drawnObjects[objIdx].price3, hx3, hy3);
      //--- Compute chord vector and length
      double chx  = (double)(hx2 - hx1);
      double chy  = (double)(hy2 - hy1);
      double cLen = MathSqrt(chx * chx + chy * chy);
      if(cLen >= 2.0)
        {
         //--- Compute unit tangent and perpendicular bisector direction
         double ux = chx / cLen, uy = chy / cLen;
         double nx = -uy,        ny =  ux;
         //--- Find chord midpoint
         double midX = 0.5 * ((double)hx1 + (double)hx2);
         double midY = 0.5 * ((double)hy1 + (double)hy2);
         //--- Project P3 onto the perpendicular bisector to get bulge magnitude
         double perp = ((double)hx3 - midX) * nx + ((double)hy3 - midY) * ny;
         //--- Compute the clamped apex handle screen position
         int apxX = (int)MathRound(midX + perp * nx);
         int apxY = (int)MathRound(midY + perp * ny);
         //--- Test apex handle (idx 2)
         if(MathAbs(mx - apxX) <= 10 && MathAbs(my - apxY) <= 10) return 2;
        }
     }

   //--- TOOL_PATH: N handles, one per polyline vertex
   if(m_drawnObjects[objIdx].toolType == TOOL_PATH)
     {
      int N = ArraySize(m_drawnObjects[objIdx].pathTimes);
      //--- Iterate every path vertex and test each against the cursor
      for(int pi = 0; pi < N; pi++)
        {
         int pxx = 0, pyy = 0;
         ChartTimePriceToXY(m_chartId, 0,
                             m_drawnObjects[objIdx].pathTimes[pi],
                             m_drawnObjects[objIdx].pathPrices[pi],
                             pxx, pyy);
         //--- Return the matching vertex index on hit
         if(MathAbs(mx - pxx) <= 10 && MathAbs(my - pyy) <= 10) return pi;
        }
      return -1;
     }

   //--- TOOL_ELLIPSE rotated: idx 0/1 = major axis ends (generic test below), idx 2/3 = minor axis endpoints
   if(m_drawnObjects[objIdx].toolType == TOOL_ELLIPSE &&
      m_drawnObjects[objIdx].time2 != 0 &&
      m_drawnObjects[objIdx].time3 != 0)
     {
      //--- Resolve major axis endpoints and P3 in screen space
      ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[objIdx].time2, m_drawnObjects[objIdx].price2, hx2, hy2);
      ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[objIdx].time3, m_drawnObjects[objIdx].price3, hx3, hy3);
      //--- Compute ellipse center as midpoint of the major axis
      double ecx = (hx1 + hx2) * 0.5;
      double ecy = (hy1 + hy2) * 0.5;
      //--- Compute major axis direction vector and its length
      double dxM  = (double)(hx2 - hx1);
      double dyM  = (double)(hy2 - hy1);
      double lenM = MathSqrt(dxM * dxM + dyM * dyM);
      if(lenM >= 2.0)
        {
         //--- Normalize major axis to get cosine/sine of rotation angle
         double cosT = dxM / lenM;
         double sinT = dyM / lenM;
         //--- Project P3 onto the perpendicular direction to get the semi-minor radius
         double dx3p     = (double)hx3 - ecx;
         double dy3p     = (double)hy3 - ecy;
         double perpSide = -dx3p * sinT + dy3p * cosT;
         double b        = MathAbs(perpSide);
         if(b < 1.0) b  = 1.0;
         //--- Preserve the signed direction P3 lies relative to the major axis
         double signP3 = (perpSide >= 0.0) ? 1.0 : -1.0;
         double perpX  = -sinT * signP3;
         double perpY  =  cosT * signP3;
         //--- Compute the two minor axis endpoint screen positions
         int m2x = (int)MathRound(ecx + perpX * b);
         int m2y = (int)MathRound(ecy + perpY * b);
         int m3x = (int)MathRound(ecx - perpX * b);
         int m3y = (int)MathRound(ecy - perpY * b);
         //--- Test positive minor axis handle (idx 2)
         if(MathAbs(mx - m2x) <= 10 && MathAbs(my - m2y) <= 10) return 2;
         //--- Test negative minor axis handle (idx 3)
         if(MathAbs(mx - m3x) <= 10 && MathAbs(my - m3y) <= 10) return 3;
        }
      //--- idx 0 and 1 (P1, P2) fall through to the generic anchor test below
     }

   //--- Generic anchor test: P1 (idx 0), P2 (idx 1), P3 (idx 2)
   if(MathAbs(mx - hx1) <= 10 && MathAbs(my - hy1) <= 10) return 0;
   if(m_drawnObjects[objIdx].time2 != 0)
     {
      ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[objIdx].time2, m_drawnObjects[objIdx].price2, hx2, hy2);
      if(MathAbs(mx - hx2) <= 10 && MathAbs(my - hy2) <= 10) return 1;
     }
   if(m_drawnObjects[objIdx].time3 != 0)
     {
      ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[objIdx].time3, m_drawnObjects[objIdx].price3, hx3, hy3);
      if(MathAbs(mx - hx3) <= 10 && MathAbs(my - hy3) <= 10) return 2;
     }

   //--- TOOL_PARALLEL_CHANNEL: idx 3 = derived D, idx 4 = mid-AB top handle, idx 5 = mid-CD bottom handle
   if(m_drawnObjects[objIdx].toolType == TOOL_PARALLEL_CHANNEL &&
      m_drawnObjects[objIdx].time3 != 0)
     {
      //--- Compute derived corner D in screen space
      int hx4 = hx2 + (hx3 - hx1);
      int hy4 = hy2 + (hy3 - hy1);
      //--- Test D handle (idx 3)
      if(MathAbs(mx - hx4) <= 10 && MathAbs(my - hy4) <= 10) return 3;
      //--- Compute and test top-line midpoint handle (idx 4)
      int mABx = (hx1 + hx2) / 2, mABy = (hy1 + hy2) / 2;
      if(MathAbs(mx - mABx) <= 10 && MathAbs(my - mABy) <= 10) return 4;
      //--- Compute and test bottom-line midpoint handle (idx 5)
      int mCDx = (hx3 + hx4) / 2, mCDy = (hy3 + hy4) / 2;
      if(MathAbs(mx - mCDx) <= 10 && MathAbs(my - mCDy) <= 10) return 5;
     }

   //--- TOOL_REGRESSION_CHANNEL / STDDEV_CHANNEL: idx 0 = left regression endpoint, idx 1 = right endpoint
   if((m_drawnObjects[objIdx].toolType == TOOL_REGRESSION_CHANNEL ||
       m_drawnObjects[objIdx].toolType == TOOL_STDDEV_CHANNEL) &&
      m_drawnObjects[objIdx].time1 != 0 &&
      m_drawnObjects[objIdx].time2 != 0)
     {
      //--- Identify the earlier and later time anchor
      datetime t1 = m_drawnObjects[objIdx].time1;
      datetime t2 = m_drawnObjects[objIdx].time2;
      datetime tL = (t1 < t2) ? t1 : t2;
      datetime tR = (t1 < t2) ? t2 : t1;
      //--- Convert times to bar shift indices
      int barR = iBarShift(_Symbol, _Period, tR, false);
      int barL = iBarShift(_Symbol, _Period, tL, false);
      //--- Ensure barL >= barR (older bars have higher shift indices)
      if(barL < barR) { int tmp = barL; barL = barR; barR = tmp; }
      int nBars = barL - barR + 1;
      if(nBars >= 2)
        {
         //--- Accumulate least-squares sums for linear regression
         double Sx = 0, Sy = 0, Sxx = 0, Sxy = 0;
         for(int i = 0; i < nBars; i++)
           {
            int    shift = barL - i;
            double cls   = iClose(_Symbol, _Period, shift);
            //--- Skip bars with no close data
            if(cls == 0.0) continue;
            double x = (double)i;
            Sx += x; Sy += cls; Sxx += x * x; Sxy += x * cls;
           }
         double dn    = (double)nBars;
         double denom = dn * Sxx - Sx * Sx;
         if(MathAbs(denom) >= 1e-12)
           {
            //--- Compute regression slope and intercept
            double slope     = (dn * Sxy - Sx * Sy) / denom;
            double intercept = (Sy - slope * Sx) / dn;
            //--- Compute center line prices at the left and right bar times
            datetime leftTime  = (datetime)iTime(_Symbol, _Period, barL);
            datetime rightTime = (datetime)iTime(_Symbol, _Period, barR);
            double   leftCP    = intercept;
            double   rightCP   = slope * (double)(nBars - 1) + intercept;
            //--- Convert regression endpoints to screen pixel coordinates
            int xL_p = 0, yLc = 0, xR_p = 0, yRc = 0;
            ChartTimePriceToXY(m_chartId, 0, leftTime,  leftCP,  xL_p, yLc);
            ChartTimePriceToXY(m_chartId, 0, rightTime, rightCP, xR_p, yRc);
            //--- Test left center line endpoint (idx 0)
            if(MathAbs(mx - xL_p) <= 10 && MathAbs(my - yLc) <= 10) return 0;
            //--- Test right center line endpoint (idx 1)
            if(MathAbs(mx - xR_p) <= 10 && MathAbs(my - yRc) <= 10) return 1;
           }
        }
     }

   //--- TOOL_GANN_BOX: idx 2 = (P1.x, P2.y) cross-corner, idx 3 = (P2.x, P1.y) cross-corner
   if(m_drawnObjects[objIdx].toolType == TOOL_GANN_BOX &&
      m_drawnObjects[objIdx].time1 != 0 &&
      m_drawnObjects[objIdx].time2 != 0)
     {
      //--- Test cross-corner at (P1.x, P2.y) — idx 2
      int hxCross2 = hx1, hyCross2 = hy2;
      if(MathAbs(mx - hxCross2) <= 10 && MathAbs(my - hyCross2) <= 10) return 2;
      //--- Test cross-corner at (P2.x, P1.y) — idx 3
      int hxCross3 = hx2, hyCross3 = hy1;
      if(MathAbs(mx - hxCross3) <= 10 && MathAbs(my - hyCross3) <= 10) return 3;
     }

   //--- TOOL_RECTANGLE: idx 0/1 = P1/P2 corners, idx 2/3 = mixed corners, idx 4-7 = edge midpoints (T/R/B/L)
   if(m_drawnObjects[objIdx].toolType == TOOL_RECTANGLE &&
      m_drawnObjects[objIdx].time1 != 0 &&
      m_drawnObjects[objIdx].time2 != 0)
     {
      //--- Test mixed corner handles (idx 2, idx 3)
      int hxCross2r = hx1, hyCross2r = hy2;
      if(MathAbs(mx - hxCross2r) <= 10 && MathAbs(my - hyCross2r) <= 10) return 2;
      int hxCross3r = hx2, hyCross3r = hy1;
      if(MathAbs(mx - hxCross3r) <= 10 && MathAbs(my - hyCross3r) <= 10) return 3;
      //--- Compute normalized bounding box extents (independent of draw direction)
      int xLr   = (hx1 < hx2) ? hx1 : hx2;
      int xRr   = (hx1 < hx2) ? hx2 : hx1;
      int yTr   = (hy1 < hy2) ? hy1 : hy2;
      int yBr   = (hy1 < hy2) ? hy2 : hy1;
      int midXr = (hx1 + hx2) / 2;
      int midYr = (hy1 + hy2) / 2;
      //--- Test edge midpoint handles (idx 4 = top, 5 = right, 6 = bottom, 7 = left)
      if(MathAbs(mx - midXr) <= 10 && MathAbs(my - yTr)   <= 10) return 4; // TOP
      if(MathAbs(mx - xRr)   <= 10 && MathAbs(my - midYr) <= 10) return 5; // RIGHT
      if(MathAbs(mx - midXr) <= 10 && MathAbs(my - yBr)   <= 10) return 6; // BOTTOM
      if(MathAbs(mx - xLr)   <= 10 && MathAbs(my - midYr) <= 10) return 7; // LEFT
     }
   return -1;
  }

//+------------------------------------------------------------------+
//| Main hit test loop — return object ID under cursor or -1         |
//+------------------------------------------------------------------+
int CDrawingEngine::HitTestAllObjects(int mouseX, int mouseY)
  {
   int n = ArraySize(m_drawnObjects);
   //--- Iterate in reverse draw order so the topmost rendered object wins
   for(int i = n - 1; i >= 0; i--)
     {
      //--- Skip invisible objects
      if(!m_drawnObjects[i].visible) continue;
      //--- Resolve all three anchor points to screen pixel coordinates
      int x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0;
      ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time1, m_drawnObjects[i].price1, x1, y1);
      if(m_drawnObjects[i].time2 != 0)
         ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time2, m_drawnObjects[i].price2, x2, y2);
      if(m_drawnObjects[i].time3 != 0)
         ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time3, m_drawnObjects[i].price3, x3, y3);
      //--- Dispatch to the appropriate hit test based on the tool type
      bool hit = false;
      switch(m_drawnObjects[i].toolType)
        {
         //--- Line-type tools: test cursor proximity to the line segment
         case TOOL_TRENDLINE:
         case TOOL_RAY:
         case TOOL_EXTENDED_LINE:
         case TOOL_TREND_ANGLE:
            hit = HitTestTrendLine(mouseX, mouseY, x1, y1, x2, y2, m_hitThreshold);
            break;
         case TOOL_INFO_LINE:
            //--- Info line: hit on the line body OR inside the floating info panel
            hit = HitTestTrendLine(mouseX, mouseY, x1, y1, x2, y2, m_hitThreshold) ||
                  HitTestInfoLinePanel(mouseX, mouseY);
            break;
         case TOOL_HLINE:
            hit = HitTestHorizontalLine(mouseX, mouseY, y1, m_hitThreshold);
            break;
         case TOOL_VLINE:
            hit = HitTestVerticalLine(mouseX, mouseY, x1, m_hitThreshold);
            break;
         case TOOL_CROSS_LINE:
            //--- Cross line is an H-line plus a V-line sharing a center
            hit = HitTestHorizontalLine(mouseX, mouseY, y1, m_hitThreshold) ||
                  HitTestVerticalLine(mouseX, mouseY, x1, m_hitThreshold);
            break;
         case TOOL_RECTANGLE:
            hit = HitTestRectangle(mouseX, mouseY, x1, y1, x2, y2);
            break;
         default: break;
        }
      //--- Return the first (topmost) matching object's ID
      if(hit) return m_drawnObjects[i].id;
     }
   return -1;
  }

//+------------------------------------------------------------------+
//| Handle pointer tool mouse move — update hover state only         |
//+------------------------------------------------------------------+
void CDrawingEngine::HandlePointerMouseMove(int mouseX, int mouseY)
  {
   //--- Run body hit test to find which object (if any) the cursor is over
   int hitId = HitTestAllObjects(mouseX, mouseY);

   //--- Test selected-object handles first (highest priority) to suppress prompt as cursor nears a handle
   int newHoveredHandle = -1;
   int handleHostId     = -1;
   if(m_selectedObjectId >= 0)
     {
      int selIdx = FindObjectIndexById(m_selectedObjectId);
      if(selIdx >= 0)
        {
         int hh = HitTestHandles(mouseX, mouseY, selIdx);
         //--- Record the handle index and its owning object
         if(hh >= 0) { newHoveredHandle = hh; handleHostId = m_selectedObjectId; }
        }
     }
   //--- If no selected-object handle was hit, try handles on the hovered object
   if(newHoveredHandle < 0 && hitId >= 0 && hitId != m_selectedObjectId)
     {
      int hIdx = FindObjectIndexById(hitId);
      if(hIdx >= 0)
        {
         int hh = HitTestHandles(mouseX, mouseY, hIdx);
         //--- Record the handle index and its owning hovered object
         if(hh >= 0) { newHoveredHandle = hh; handleHostId = hitId; }
        }
     }

   //--- Grace region: keep hover set if cursor is inside the "+ Add text" prompt rect to prevent mid-motion hide
   if(hitId < 0 &&
      m_addTextPromptObjId >= 0 &&
      m_addTextPromptObjId == m_selectedObjectId &&
      ArraySize(m_addTextPromptCornerX) >= 4 &&
      PointInRotatedRect(mouseX, mouseY,
                         m_addTextPromptCornerX,
                         m_addTextPromptCornerY))
     {
      hitId = m_addTextPromptObjId;
     }

   //--- Grace region for the committed label of the selected object
   if(hitId < 0 &&
      m_labelHitObjIdForSelected >= 0 &&
      m_labelHitObjIdForSelected == m_selectedObjectId &&
      ArraySize(m_labelHitCornerX) >= 4 &&
      PointInRotatedRect(mouseX, mouseY,
                         m_labelHitCornerX,
                         m_labelHitCornerY))
     {
      hitId = m_labelHitObjIdForSelected;
     }

   //--- Only trigger a redraw if any hover state actually changed
   if(hitId != m_hoveredObjectId ||
      newHoveredHandle != m_hoveredHandleIdx ||
      handleHostId != m_hoveredHandleHostId)
     {
      m_hoveredObjectId     = hitId;
      m_hoveredHandleIdx    = newHoveredHandle;
      m_hoveredHandleHostId = handleHostId;
      RedrawAllObjects();
     }
  }

//+------------------------------------------------------------------+
//| Handle pointer tool click — select object or begin drag          |
//+------------------------------------------------------------------+
void CDrawingEngine::HandlePointerClick(int mouseX, int mouseY)
  {
   //--- If a label edit is in progress, handle the click relative to the editing context
   if(m_isEditingLabel)
     {
      //--- Determine whether the click landed inside the currently editing label rect
      bool clickInsideEditedLabel = false;
      if(m_labelHitObjIdForSelected >= 0 &&
         m_labelHitObjIdForSelected == m_selectedObjectId &&
         ArraySize(m_labelHitCornerX) == 4 &&
         PointInRotatedRect(mouseX, mouseY, m_labelHitCornerX, m_labelHitCornerY))
        {
         clickInsideEditedLabel = true;
        }
      if(clickInsideEditedLabel)
        {
         //--- Reposition the caret to the nearest character boundary; do not commit on inside-click
         int editIdx = FindObjectIndexById(m_selectedObjectId);
         int padX = 0, padY = 0, fontPt = 12, wrapW = 0;
         string fontN = "Arial";
         //--- Use the shared layout helper to match the renderer's font/wrap params
         if(GetHostTextLayout(editIdx, padX, padY, fontN, fontPt, wrapW))
            SetCaretFromMouseClick(mouseX, mouseY, padX, padY, fontN, fontPt, wrapW);
         return;
        }
      //--- Outside the editing label: finalize edit (empty text-annotation discards; host-shape label commits)
      FinalizeOpenLabelEdit();
     }

   //--- Click on the committed label of the selected object re-enters edit mode (uses rotated-rect test, not AABB)
   if(m_labelHitObjIdForSelected >= 0 &&
      m_labelHitObjIdForSelected == m_selectedObjectId &&
      PointInRotatedRect(mouseX, mouseY, m_labelHitCornerX, m_labelHitCornerY))
     {
      int  labIdx     = FindObjectIndexById(m_labelHitObjIdForSelected);
      bool isTextAnnot = (labIdx >= 0 &&
                          (m_drawnObjects[labIdx].toolType == TOOL_TEXT ||
                           m_drawnObjects[labIdx].toolType == TOOL_NOTE ||
                           m_drawnObjects[labIdx].toolType == TOOL_CALLOUT ||
                           m_drawnObjects[labIdx].toolType == TOOL_COMMENT));
      if(isTextAnnot)
        {
         //--- Text annotation: arm drag and set pending-edit; release resolves edit vs drag on movement threshold
         m_isDraggingObject = true;
         m_dragLastMouseX           = mouseX;
         m_dragLastMouseY           = mouseY;
         m_pendingTextEditArmed     = true;
         m_pendingTextEditObjId     = m_labelHitObjIdForSelected;
         m_pendingTextEditStartX    = mouseX;
         m_pendingTextEditStartY    = mouseY;
         //--- Disable chart scroll while the drag/edit is being resolved
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         return;
        }
      //--- Non-text tool label: click always enters edit mode (drag is done via the host shape, not the label)
      StartLabelEdit();
      RedrawAllObjects();
      return;
     }

   //--- Click inside the "+ Add text" prompt for the selected object enters edit mode
   if(m_addTextPromptObjId >= 0 &&
      m_addTextPromptObjId == m_selectedObjectId &&
      PointInRotatedRect(mouseX, mouseY, m_addTextPromptCornerX, m_addTextPromptCornerY))
     {
      StartLabelEdit();
      RedrawAllObjects();
      return;
     }

   //--- Check for a handle hit on the selected object; handle-drag starts on single click (handles are unambiguous)
   if(m_selectedObjectId >= 0)
     {
      int selIdx    = FindObjectIndexById(m_selectedObjectId);
      int handleIdx = HitTestHandles(mouseX, mouseY, selIdx);
      if(handleIdx >= 0)
        {
         //--- TREND_ANGLE: handle 0 (angle origin) drags whole object; handle 1 (far anchor) resizes
         if(selIdx >= 0 &&
            m_drawnObjects[selIdx].toolType == TOOL_TREND_ANGLE &&
            handleIdx == 0)
           {
            //--- Arm a whole-object drag for the angle-origin handle
            m_isDraggingObject = true;
            m_dragLastMouseX   = mouseX;
            m_dragLastMouseY   = mouseY;
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            return;
           }
         //--- Arm a single-handle drag for all other handle hits
         m_isDraggingHandle = true;
         m_draggedHandleIdx = handleIdx;
         return;
        }
     }

   //--- Body hit test: find any object under the cursor (not handles)
   int hitId = HitTestAllObjects(mouseX, mouseY);

   //--- Press-and-drag on any hit body: arm whole-object drag immediately; release without movement = click-to-select
   if(hitId >= 0)
     {
      //--- Update selection (no-op if the same object is already selected)
      if(m_selectedObjectId != hitId)
         SelectObjectById(hitId);
      //--- Arm whole-object drag for subsequent mouse moves while pressed
      m_isDraggingObject = true;
      m_dragLastMouseX   = mouseX;
      m_dragLastMouseY   = mouseY;
      //--- Prevent chart scroll from interfering while dragging
      ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
      RedrawAllObjects();
      return;
     }

   //--- No object under the cursor — deselect whatever is currently selected
   if(m_selectedObjectId >= 0)
     {
      SelectObjectById(-1);
      RedrawAllObjects();
     }
  }

//+------------------------------------------------------------------+
//| Handle pointer double click — select and begin whole-object drag |
//+------------------------------------------------------------------+
void CDrawingEngine::HandlePointerDoubleClick(int mouseX, int mouseY)
  {
   //--- Find the object under the cursor
   int hitId = HitTestAllObjects(mouseX, mouseY);
   //--- Nothing under the cursor — nothing to do
   if(hitId < 0) return;
   //--- Select the hit object if it is not already the active selection
   if(m_selectedObjectId != hitId)
      SelectObjectById(hitId);
   //--- Arm a whole-object drag for immediate movement on the next mouse move
   m_isDraggingObject = true;
   m_dragLastMouseX   = mouseX;
   m_dragLastMouseY   = mouseY;
   //--- Lock chart scroll for the duration of the drag
   ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
  }

//+------------------------------------------------------------------+
//| Handle pointer drag move — translate or reshape dragged object   |
//+------------------------------------------------------------------+
void CDrawingEngine::HandlePointerDragMove(int mouseX, int mouseY)
  {
   //--- Whole-object drag: translate all anchor points by the same delta
   if(m_isDraggingObject && m_selectedObjectId >= 0)
     {
      int idx = FindObjectIndexById(m_selectedObjectId);
      if(idx < 0) return;
      //--- Convert the last and current mouse positions to time/price
      datetime t1, t2; double p1, p2; int sub;
      ChartXYToTimePrice(m_chartId, m_dragLastMouseX, m_dragLastMouseY, sub, t1, p1);
      ChartXYToTimePrice(m_chartId, mouseX,           mouseY,           sub, t2, p2);
      //--- Compute the time and price deltas between the two positions
      long   dtDelta    = (long)t2 - (long)t1;
      double priceDelta = p2 - p1;
      //--- Apply the delta to all stored anchor points
      m_drawnObjects[idx].time1  = (datetime)((long)m_drawnObjects[idx].time1  + dtDelta);
      m_drawnObjects[idx].price1 =             m_drawnObjects[idx].price1 + priceDelta;
      if(m_drawnObjects[idx].time2 != 0)
        {
         m_drawnObjects[idx].time2  = (datetime)((long)m_drawnObjects[idx].time2  + dtDelta);
         m_drawnObjects[idx].price2 =             m_drawnObjects[idx].price2 + priceDelta;
        }
      if(m_drawnObjects[idx].time3 != 0)
        {
         m_drawnObjects[idx].time3  = (datetime)((long)m_drawnObjects[idx].time3  + dtDelta);
         m_drawnObjects[idx].price3 =             m_drawnObjects[idx].price3 + priceDelta;
        }
      //--- PATH: shift every vertex too; without this only the first two anchors move and the polyline deforms
      if(m_drawnObjects[idx].toolType == TOOL_PATH)
        {
         int N = ArraySize(m_drawnObjects[idx].pathTimes);
         for(int pi = 0; pi < N; pi++)
           {
            //--- Translate this vertex time by the drag delta
            m_drawnObjects[idx].pathTimes[pi]  =
               (datetime)((long)m_drawnObjects[idx].pathTimes[pi] + dtDelta);
            //--- Translate this vertex price by the drag delta
            m_drawnObjects[idx].pathPrices[pi] =
               m_drawnObjects[idx].pathPrices[pi] + priceDelta;
           }
        }
      //--- Update the last mouse position for incremental delta computation
      m_dragLastMouseX = mouseX;
      m_dragLastMouseY = mouseY;
      //--- Redraw at the committed color (WYSIWYG — no accent color switch during drag)
      RedrawAllObjects();
      return;
     }

   //--- Single handle drag: reshape the object by moving one anchor
   if(m_isDraggingHandle && m_selectedObjectId >= 0)
     {
      int idx = FindObjectIndexById(m_selectedObjectId);
      if(idx < 0) return;
      //--- Convert the current mouse position to time/price coordinates
      datetime newTime; double newPrice; int sub;
      if(!ChartXYToTimePrice(m_chartId, mouseX, mouseY, sub, newTime, newPrice)) return;

      //--- TOOL_PARALLEL_CHANNEL: corner handles (0..3) adjust length; mid-line handles (4, 5) adjust height
      if(m_drawnObjects[idx].toolType == TOOL_PARALLEL_CHANNEL)
        {
         //--- Cache current anchor values before any modification
         datetime tA = m_drawnObjects[idx].time1;  double pA = m_drawnObjects[idx].price1;
         datetime tB = m_drawnObjects[idx].time2;  double pB = m_drawnObjects[idx].price2;
         datetime tC = m_drawnObjects[idx].time3;  double pC = m_drawnObjects[idx].price3;
         //--- Compute derived corner D: time = tB, price = pB + (pC - pA)
         double   pD = pB + (pC - pA);
         datetime tD = tB;
         switch(m_draggedHandleIdx)
           {
            case 0:
              {
               //--- A moves to cursor; C shifts by the same delta to preserve channel height
               long   dt = (long)newTime - (long)tA;
               double dp = newPrice - pA;
               m_drawnObjects[idx].time1  = newTime;
               m_drawnObjects[idx].price1 = newPrice;
               m_drawnObjects[idx].time3  = (datetime)((long)tC + dt);
               m_drawnObjects[idx].price3 = pC + dp;
               break;
              }
            case 1:
              {
               //--- B moves to cursor; D shifts implicitly since D.price = pB + (pC - pA)
               long   dt = (long)newTime - (long)tB;
               double dp = newPrice - pB;
               m_drawnObjects[idx].time2  = newTime;
               m_drawnObjects[idx].price2 = newPrice;
               //--- A and C remain unchanged
               break;
              }
            case 2:
              {
               //--- C moves to cursor; A shifts by the same delta to preserve channel height
               long   dt = (long)newTime - (long)tC;
               double dp = newPrice - pC;
               m_drawnObjects[idx].time3  = newTime;
               m_drawnObjects[idx].price3 = newPrice;
               m_drawnObjects[idx].time1  = (datetime)((long)tA + dt);
               m_drawnObjects[idx].price1 = pA + dp;
               break;
              }
            case 3:
              {
               //--- D moves to cursor; compute the required price delta and apply it to B
               long   dt = (long)newTime - (long)tD;
               double dp = newPrice - pD;
               m_drawnObjects[idx].time2  = (datetime)((long)tB + dt);
               m_drawnObjects[idx].price2 = pB + dp;
               //--- A and C remain unchanged
               break;
              }
            case 4:
              {
               //--- Mid-AB drag: shift A and B prices by the same delta (channel height adjust)
               double midABP = (pA + pB) / 2.0;
               double dp     = newPrice - midABP;
               m_drawnObjects[idx].price1 = pA + dp;
               m_drawnObjects[idx].price2 = pB + dp;
               //--- Times and C/D unchanged
               break;
              }
            case 5:
              {
               //--- Mid-CD drag: shift C price by the required delta (channel height adjust)
               double midCDP = (pC + pD) / 2.0;
               double dp     = newPrice - midCDP;
               m_drawnObjects[idx].price3 = pC + dp;
               //--- Times and A/B unchanged
               break;
              }
           }
        }
      else if(m_drawnObjects[idx].toolType == TOOL_RECTANGLE)
        {
         //--- Cache current anchor values before modification
         datetime tA = m_drawnObjects[idx].time1;
         datetime tB = m_drawnObjects[idx].time2;
         double   pA = m_drawnObjects[idx].price1;
         double   pB = m_drawnObjects[idx].price2;
         //--- Determine which anchor is on the top (larger price) and left (smaller time) sides
         bool aIsTop  = (pA >= pB);
         bool aIsLeft = (tA <= tB);
         switch(m_draggedHandleIdx)
           {
            case 0:
               //--- P1 corner: move freely to the cursor
               m_drawnObjects[idx].time1  = newTime;
               m_drawnObjects[idx].price1 = newPrice;
               break;
            case 1:
               //--- P2 corner: move freely to the cursor
               m_drawnObjects[idx].time2  = newTime;
               m_drawnObjects[idx].price2 = newPrice;
               break;
            case 2:
               //--- (P1.x, P2.y) mixed corner: update P1 time and P2 price
               m_drawnObjects[idx].time1  = newTime;
               m_drawnObjects[idx].price2 = newPrice;
               break;
            case 3:
               //--- (P2.x, P1.y) mixed corner: update P2 time and P1 price
               m_drawnObjects[idx].time2  = newTime;
               m_drawnObjects[idx].price1 = newPrice;
               break;
            case 4:
               //--- TOP edge midpoint: update the price of whichever anchor is on top
               if(aIsTop) m_drawnObjects[idx].price1 = newPrice;
               else       m_drawnObjects[idx].price2 = newPrice;
               break;
            case 5:
               //--- RIGHT edge midpoint: update the time of whichever anchor is on the right
               if(aIsLeft) m_drawnObjects[idx].time2 = newTime;
               else        m_drawnObjects[idx].time1 = newTime;
               break;
            case 6:
               //--- BOTTOM edge midpoint: update the price of whichever anchor is on the bottom
               if(aIsTop) m_drawnObjects[idx].price2 = newPrice;
               else       m_drawnObjects[idx].price1 = newPrice;
               break;
            case 7:
               //--- LEFT edge midpoint: update the time of whichever anchor is on the left
               if(aIsLeft) m_drawnObjects[idx].time1 = newTime;
               else        m_drawnObjects[idx].time2 = newTime;
               break;
           }
        }
      else
        {
         //--- Default handle drag for all other tool types: move one anchor to the cursor
         switch(m_draggedHandleIdx)
           {
            case 0:
               m_drawnObjects[idx].time1  = newTime;
               m_drawnObjects[idx].price1 = newPrice;
               break;
            case 1:
               m_drawnObjects[idx].time2  = newTime;
               m_drawnObjects[idx].price2 = newPrice;
               break;
            case 2:
               m_drawnObjects[idx].time3  = newTime;
               m_drawnObjects[idx].price3 = newPrice;
               break;
           }
        }
      //--- Redraw to reflect the updated anchor positions
      RedrawAllObjects();
     }
  }

//+------------------------------------------------------------------+
//| Handle pointer drag release — finalize drag and restore state    |
//+------------------------------------------------------------------+
void CDrawingEngine::HandlePointerDragRelease()
  {
   //--- Resolve a pending text-annotation click vs drag decision
   if(m_pendingTextEditArmed)
     {
      //--- Compute the total displacement during the press
      int  dx              = m_dragLastMouseX - m_pendingTextEditStartX;
      int  dy              = m_dragLastMouseY - m_pendingTextEditStartY;
      bool movedMeaningfully = (dx * dx + dy * dy) > (4 * 4);
      if(!movedMeaningfully)
        {
         //--- Displacement under 4 px: treat as a plain click and enter edit mode
         m_isDraggingObject     = false;
         m_draggedHandleIdx     = -1;
         m_isDraggingHandle     = false;
         m_pendingTextEditArmed = false;
         StartLabelEdit();
         RedrawAllObjects();
         return;
        }
      //--- Displacement over 4 px: it was a real drag — clear the pending flag and fall through
      m_pendingTextEditArmed = false;
     }
   //--- Record whether any drag was in progress before clearing flags
   bool wasDragging = (m_isDraggingHandle || m_isDraggingObject);
   //--- Clear all drag state flags
   m_isDraggingHandle = false;
   m_isDraggingObject = false;
   m_draggedHandleIdx = -1;
   //--- Redraw so the hidden drag handle reappears and the "+ Add text" prompt can show again
   if(wasDragging) RedrawAllObjects();
   //--- Do not restore chart scroll here; pointer mode keeps it locked until ToggleTool switches away
  }

//+------------------------------------------------------------------+
//| Delete the currently selected drawn object                       |
//+------------------------------------------------------------------+
void CDrawingEngine::DeleteSelectedObject()
  {
   //--- Nothing to do if no object is selected
   if(m_selectedObjectId < 0) return;
   //--- Remove the object from the drawn objects array and the chart canvas
   RemoveDrawnObject(m_selectedObjectId);
   //--- Clear the selection and hover IDs so the UI returns to idle state
   m_selectedObjectId = -1;
   m_hoveredObjectId  = -1;
  }

#endif // TOOLS_PALETTE_ENGINE_INTERACT_MQH
//+------------------------------------------------------------------+