| //+------------------------------------------------------------------+
|
| //| AI Canvas Scrollbar.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 AI_CANVAS_SCROLLBAR_MQH
|
| #define AI_CANVAS_SCROLLBAR_MQH
|
|
|
| //--- Include required modules
|
| #include "AI Canvas Theme.mqh"
|
| #include "AI Canvas Primitives.mqh"
|
|
|
| //+------------------------------------------------------------------+
|
| //| Scrollbar State Structure |
|
| //+------------------------------------------------------------------+
|
| struct AiScrollState
|
| {
|
| int trackL; // Track left edge
|
| int trackT; // Track top edge
|
| int trackR; // Track right edge
|
| int trackB; // Track bottom edge
|
| int scrollPx; // Current scroll offset in pixels
|
| int viewportH; // Visible viewport height
|
| int totalH; // Total scrollable content height
|
| bool hoveredArea; // Cursor is inside the scrollbar area
|
| bool hoveredThumb; // Cursor is over the scrollbar thumb
|
| bool dragging; // Thumb is being dragged
|
| bool hover; // Generic hover flag
|
| int dragOriginPx; // Scroll position at drag start
|
| int dragOriginY; // Mouse Y at drag start
|
| };
|
|
|
| //+------------------------------------------------------------------+
|
| //| Reset scroll state to defaults |
|
| //+------------------------------------------------------------------+
|
| void AiScrollInit(AiScrollState &s)
|
| {
|
| //--- Zero track and scroll metrics
|
| s.trackL = 0;
|
| s.trackT = 0;
|
| s.trackR = 0;
|
| s.trackB = 0;
|
| s.scrollPx = 0;
|
| s.viewportH = 0;
|
| s.totalH = 0;
|
| //--- Clear hover and drag flags
|
| s.hoveredArea = false;
|
| s.hoveredThumb = false;
|
| s.hover = false;
|
| s.dragging = false;
|
| s.dragOriginPx = 0;
|
| s.dragOriginY = 0;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Compute maximum allowed scroll offset |
|
| //+------------------------------------------------------------------+
|
| int AiScrollMax(const AiScrollState &s)
|
| {
|
| //--- Cap minimum at zero when content fits in viewport
|
| int m = s.totalH - s.viewportH;
|
| return (m < 0) ? 0 : m;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Test if scrollbar should be visible |
|
| //+------------------------------------------------------------------+
|
| bool AiScrollVisible(const AiScrollState &s)
|
| {
|
| //--- Only visible when content overflows viewport
|
| return AiScrollMax(s) > 0;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Clamp scroll offset within valid range |
|
| //+------------------------------------------------------------------+
|
| void AiScrollClamp(AiScrollState &s)
|
| {
|
| //--- Clamp scroll position between 0 and max
|
| int m = AiScrollMax(s);
|
| if(s.scrollPx < 0)
|
| s.scrollPx = 0;
|
| if(s.scrollPx > m)
|
| s.scrollPx = m;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Compute thumb top and bottom Y coordinates |
|
| //+------------------------------------------------------------------+
|
| bool AiScrollThumbRect(const AiScrollState &s, int &thT, int &thB)
|
| {
|
| //--- Bail when scrollbar is not visible
|
| if(!AiScrollVisible(s))
|
| {
|
| thT = 0;
|
| thB = 0;
|
| return false;
|
| }
|
| //--- Bail when track has no height
|
| int trackH = s.trackB - s.trackT;
|
| if(trackH <= 0)
|
| {
|
| thT = 0;
|
| thB = 0;
|
| return false;
|
| }
|
| //--- Size thumb proportional to viewport-to-content ratio
|
| int thumbH = (int)((double)trackH * (double)s.viewportH / (double)s.totalH);
|
| if(thumbH < 20)
|
| thumbH = 20;
|
| if(thumbH > trackH)
|
| thumbH = trackH;
|
| //--- Position thumb based on scroll ratio
|
| int maxScroll = AiScrollMax(s);
|
| int travel = trackH - thumbH;
|
| double ratio = (maxScroll == 0) ? 0.0 : (double)s.scrollPx / (double)maxScroll;
|
| thT = s.trackT + (int)((double)travel * ratio);
|
| thB = thT + thumbH;
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Test if point is over the scrollbar thumb |
|
| //+------------------------------------------------------------------+
|
| bool AiScrollHitTestThumb(const AiScrollState &s, int x, int y)
|
| {
|
| //--- Get thumb rect and test point
|
| int thT, thB;
|
| if(!AiScrollThumbRect(s, thT, thB))
|
| return false;
|
| //--- Expand the hit area beyond the thin drawn thumb so it is easy to grab
|
| const int padX = 9; // horizontal grab tolerance each side
|
| const int padY = 5; // vertical grab tolerance each side
|
| return AiPointInRect(x, y,
|
| s.trackL - padX, thT - padY,
|
| (s.trackR - s.trackL) + 2 * padX,
|
| (thB - thT) + 2 * padY);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Test if point is over the scrollbar area (track + padding) |
|
| //+------------------------------------------------------------------+
|
| bool AiScrollHitTestArea(const AiScrollState &s, int x, int y)
|
| {
|
| //--- Bail when scrollbar is not visible
|
| if(!AiScrollVisible(s))
|
| return false;
|
| //--- Test against padded track for easier hover targeting
|
| const int pad = 6;
|
| return AiPointInRect(x, y, s.trackL - pad, s.trackT,
|
| (s.trackR - s.trackL) + 2 * pad, s.trackB - s.trackT);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw scrollbar thumb pill onto canvas |
|
| //+------------------------------------------------------------------+
|
| void AiScrollDraw(CCanvas &canvas, AiScrollState &s, CAiCanvasPrimitives &prim, bool forceShow = false)
|
| {
|
| //--- Skip drawing when not hovered and not dragging (unless forced visible)
|
| if(!forceShow && !s.hoveredArea && !s.dragging)
|
| return;
|
| //--- Get thumb rect and bail if invalid
|
| int thT, thB;
|
| if(!AiScrollThumbRect(s, thT, thB))
|
| return;
|
| const int sbW = s.trackR - s.trackL;
|
| const int thumbH = thB - thT;
|
| if(sbW <= 0 || thumbH <= 0)
|
| return;
|
| //--- Pick pill color and alpha by interaction state
|
| color pillCol;
|
| uchar pillA;
|
| if(s.dragging)
|
| {
|
| pillCol = g_ai_scrollSliderDrag;
|
| pillA = 255;
|
| }
|
| else
|
| if(s.hoveredThumb)
|
| {
|
| pillCol = g_ai_scrollSliderHover;
|
| pillA = 255;
|
| }
|
| else
|
| {
|
| pillCol = g_ai_scrollSlider;
|
| pillA = 180;
|
| }
|
| const uint pillArgb = ColorToARGB(pillCol, pillA);
|
| //--- Setup high-res super-sampling factor
|
| const int factor = 4;
|
| const int pws = sbW * factor;
|
| const int phs = thumbH * factor;
|
| //--- Reuse cached HR fill canvas from primitives helper
|
| if(!prim.EnsureHrFill(pws, phs))
|
| return;
|
| prim.m_hrFill.FillRectangle(0, 0, pws - 1, phs - 1, 0);
|
| prim.FillRoundRectHR(prim.m_hrFill, 0, 0, pws, phs, MathMax(1, pws / 2), pillArgb);
|
| //--- Downsample HR pill back onto canvas with alpha blending
|
| const int ss2 = factor * factor;
|
| for(int py = 0; py < thumbH; py++)
|
| for(int px = 0; px < sbW; px++)
|
| {
|
| //--- Sum HR sub-pixels for this output pixel
|
| double sumA = 0, sumR = 0, sumG = 0, sumB = 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 >= pws || sy >= phs)
|
| continue;
|
| const uint p = prim.m_hrFill.PixelGet(sx, sy);
|
| const uchar pa = (uchar)((p >> 24) & 0xFF);
|
| sumA += pa;
|
| if(pa > 0)
|
| {
|
| sumR += (p >> 16) & 0xFF;
|
| sumG += (p >> 8) & 0xFF;
|
| sumB += p & 0xFF;
|
| wc += 1.0;
|
| }
|
| }
|
| //--- Compute averaged final pixel and blend onto canvas
|
| const uchar fa = (uchar)(sumA / ss2);
|
| if(fa > 0 && wc > 0)
|
| prim.BlendPixelSet(canvas, s.trackL + px, thT + py,
|
| ((uint)fa << 24) | ((uint)(uchar)(sumR / wc) << 16) |
|
| ((uint)(uchar)(sumG / wc) << 8) | (uint)(uchar)(sumB / wc));
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Apply mouse-wheel scroll delta |
|
| //+------------------------------------------------------------------+
|
| void AiScrollByWheel(AiScrollState &s, int delta, int step = 30)
|
| {
|
| //--- Negative delta scrolls up, positive scrolls down
|
| if(delta > 0)
|
| s.scrollPx -= step;
|
| else
|
| s.scrollPx += step;
|
| AiScrollClamp(s);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Begin scrollbar thumb drag |
|
| //+------------------------------------------------------------------+
|
| void AiScrollBeginDrag(AiScrollState &s, int mouseY)
|
| {
|
| //--- Snapshot scroll position and mouse Y at drag start
|
| s.dragging = true;
|
| s.dragOriginPx = s.scrollPx;
|
| s.dragOriginY = mouseY;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Update scroll position during thumb drag |
|
| //+------------------------------------------------------------------+
|
| void AiScrollUpdateDrag(AiScrollState &s, int mouseY)
|
| {
|
| //--- Bail when not actively dragging
|
| if(!s.dragging)
|
| return;
|
| //--- Get thumb rect and bail when invalid
|
| int trackH = s.trackB - s.trackT;
|
| int thT, thB;
|
| if(!AiScrollThumbRect(s, thT, thB))
|
| {
|
| s.dragging = false;
|
| return;
|
| }
|
| //--- Bail when no travel room
|
| int thumbH = thB - thT;
|
| int travel = trackH - thumbH;
|
| if(travel <= 0)
|
| return;
|
| //--- Compute new scroll position from mouse delta ratio
|
| int dy = mouseY - s.dragOriginY;
|
| double ratio = (double)dy / (double)travel;
|
| int maxScroll = AiScrollMax(s);
|
| s.scrollPx = s.dragOriginPx + (int)(ratio * maxScroll);
|
| AiScrollClamp(s);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| End scrollbar thumb drag |
|
| //+------------------------------------------------------------------+
|
| void AiScrollEndDrag(AiScrollState &s)
|
| {
|
| //--- Clear drag flag
|
| s.dragging = false;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Compute horizontal scrollbar thumb rectangle |
|
| //+------------------------------------------------------------------+
|
| bool AiScrollThumbRectH(const AiScrollState &s, int &thL, int &thR)
|
| {
|
| if(!AiScrollVisible(s))
|
| {
|
| thL = 0;
|
| thR = 0;
|
| return false;
|
| }
|
| int trackW = s.trackR - s.trackL;
|
| if(trackW <= 0)
|
| {
|
| thL = 0;
|
| thR = 0;
|
| return false;
|
| }
|
| int thumbW = (int)((double)trackW * (double)s.viewportH / (double)s.totalH);
|
| if(thumbW < 20)
|
| thumbW = 20;
|
| if(thumbW > trackW)
|
| thumbW = trackW;
|
| int maxScroll = AiScrollMax(s);
|
| int travel = trackW - thumbW;
|
| double ratio = (maxScroll == 0) ? 0.0 : (double)s.scrollPx / (double)maxScroll;
|
| thL = s.trackL + (int)((double)travel * ratio);
|
| thR = thL + thumbW;
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Test if point is over the horizontal thumb |
|
| //+------------------------------------------------------------------+
|
| bool AiScrollHitTestThumbH(const AiScrollState &s, int x, int y)
|
| {
|
| int thL, thR;
|
| if(!AiScrollThumbRectH(s, thL, thR))
|
| return false;
|
| const int padX = 5, padY = 9;
|
| return AiPointInRect(x, y, thL - padX, s.trackT - padY,
|
| (thR - thL) + 2 * padX, (s.trackB - s.trackT) + 2 * padY);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Test if point is over the horizontal scrollbar area |
|
| //+------------------------------------------------------------------+
|
| bool AiScrollHitTestAreaH(const AiScrollState &s, int x, int y)
|
| {
|
| if(!AiScrollVisible(s))
|
| return false;
|
| const int pad = 6;
|
| return AiPointInRect(x, y, s.trackL, s.trackT - pad,
|
| s.trackR - s.trackL, (s.trackB - s.trackT) + 2 * pad);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw the horizontal scrollbar pill |
|
| //+------------------------------------------------------------------+
|
| void AiScrollDrawH(CCanvas &canvas, AiScrollState &s, CAiCanvasPrimitives &prim, bool forceShow = false)
|
| {
|
| if(!forceShow && !s.hoveredArea && !s.dragging)
|
| return;
|
| int thL, thR;
|
| if(!AiScrollThumbRectH(s, thL, thR))
|
| return;
|
| const int sbH = s.trackB - s.trackT;
|
| const int thumbW = thR - thL;
|
| if(sbH <= 0 || thumbW <= 0)
|
| return;
|
| color pillCol;
|
| uchar pillA;
|
| if(s.dragging)
|
| {
|
| pillCol = g_ai_scrollSliderDrag;
|
| pillA = 255;
|
| }
|
| else
|
| if(s.hoveredThumb)
|
| {
|
| pillCol = g_ai_scrollSliderHover;
|
| pillA = 255;
|
| }
|
| else
|
| {
|
| pillCol = g_ai_scrollSlider;
|
| pillA = 180;
|
| }
|
| const uint pillArgb = ColorToARGB(pillCol, pillA);
|
| const int factor = 4;
|
| const int pws = thumbW * factor;
|
| const int phs = sbH * factor;
|
| if(!prim.EnsureHrFill(pws, phs))
|
| return;
|
| prim.m_hrFill.FillRectangle(0, 0, pws - 1, phs - 1, 0);
|
| prim.FillRoundRectHR(prim.m_hrFill, 0, 0, pws, phs, MathMax(1, phs / 2), pillArgb);
|
| const int ss2 = factor * factor;
|
| for(int py = 0; py < sbH; py++)
|
| for(int px = 0; px < thumbW; px++)
|
| {
|
| double sumA = 0, sumR = 0, sumG = 0, sumB = 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 >= pws || sy >= phs)
|
| continue;
|
| const uint p = prim.m_hrFill.PixelGet(sx, sy);
|
| const uchar pa = (uchar)((p >> 24) & 0xFF);
|
| sumA += pa;
|
| if(pa > 0)
|
| {
|
| sumR += (p >> 16) & 0xFF;
|
| sumG += (p >> 8) & 0xFF;
|
| sumB += p & 0xFF;
|
| wc += 1.0;
|
| }
|
| }
|
| const uchar fa = (uchar)(sumA / ss2);
|
| if(fa > 0 && wc > 0)
|
| prim.BlendPixelSet(canvas, thL + px, s.trackT + py,
|
| ((uint)fa << 24) | ((uint)(uchar)(sumR / wc) << 16) |
|
| ((uint)(uchar)(sumG / wc) << 8) | (uint)(uchar)(sumB / wc));
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Begin dragging the horizontal thumb |
|
| //+------------------------------------------------------------------+
|
| void AiScrollBeginDragH(AiScrollState &s, int mouseX)
|
| {
|
| s.dragging = true;
|
| s.dragOriginPx = s.scrollPx;
|
| s.dragOriginY = mouseX; // reuse field to store the X anchor
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Update horizontal scroll offset while dragging |
|
| //+------------------------------------------------------------------+
|
| void AiScrollUpdateDragH(AiScrollState &s, int mouseX)
|
| {
|
| if(!s.dragging)
|
| return;
|
| int thL, thR;
|
| if(!AiScrollThumbRectH(s, thL, thR))
|
| {
|
| s.dragging = false;
|
| return;
|
| }
|
| int trackW = s.trackR - s.trackL;
|
| int thumbW = thR - thL;
|
| int travel = trackW - thumbW;
|
| if(travel <= 0)
|
| return;
|
| int dx = mouseX - s.dragOriginY;
|
| int maxScroll = AiScrollMax(s);
|
| s.scrollPx = s.dragOriginPx + (int)((double)dx / (double)travel * (double)maxScroll);
|
| AiScrollClamp(s);
|
| }
|
|
|
| #endif // AI_CANVAS_SCROLLBAR_MQH
|
| //+------------------------------------------------------------------+
|