255 lines
10 KiB
MQL5
255 lines
10 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;
|
||
|
|
return AiPointInRect(x, y, s.trackL, thT, s.trackR - s.trackL, thB - thT);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| 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
|