299 lines
11 KiB
MQL5
299 lines
11 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| 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)
| |||
{
| |||
//--- Skip drawing when not hovered and not dragging
| |||
if(!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;
| |||
}
| |||
| |||
#endif // AI_CANVAS_SCROLLBAR_MQH
| |||
//+------------------------------------------------------------------+
|