//+------------------------------------------------------------------+ //| Tools Palette Part 2.mq5 | //| 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" #property version "1.00" #property strict //--- Include canvas drawing library #include //+------------------------------------------------------------------+ //| Icon definitions — category sidebar buttons only | //+------------------------------------------------------------------+ struct SIconDefinition { string fontName; uchar charCode; }; // Store icon font and character code //--- Define icon for each tool category using font/char pairs SIconDefinition ICON_CATEGORY_CURSORS = { "Wingdings", (uchar)'v' }; // Cursors category icon SIconDefinition ICON_CATEGORY_LINES = { "Wingdings 3", (uchar)'&' }; // Lines category icon SIconDefinition ICON_CATEGORY_CHANNELS = { "Wingdings 3", (uchar)'2' }; // Channels category icon SIconDefinition ICON_CATEGORY_PITCHFORK = { "Wingdings 3", (uchar)'H' }; // Pitchfork category icon SIconDefinition ICON_CATEGORY_GANN = { "Wingdings", (uchar)'T' }; // Gann category icon SIconDefinition ICON_CATEGORY_FIBONACCI = { "Wingdings", (uchar)'z' }; // Fibonacci category icon SIconDefinition ICON_CATEGORY_SHAPES = { "Wingdings", (uchar)'o' }; // Shapes category icon SIconDefinition ICON_CATEGORY_ANNOTATIONS = { "Webdings", (uchar)'>' }; // Annotations category icon //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum ENUM_SNAP_STATE { SNAP_LEFT, // Snap sidebar to left edge SNAP_RIGHT, // Snap sidebar to right edge SNAP_FLOAT // Allow sidebar to float freely }; enum ENUM_CATEGORY { CAT_CURSORS = 0, // Cursors category index CAT_LINES, // Lines category index CAT_CHANNELS, // Channels category index CAT_PITCHFORK, // Pitchfork category index CAT_GANN, // Gann category index CAT_FIBONACCI, // Fibonacci category index CAT_SHAPES, // Shapes category index CAT_ANNOTATIONS, // Annotations category index CAT_COUNT // Total number of categories }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input int CanvasY = 50; // Canvas Y Position input double BackgroundOpacity = 0.92; // Background Opacity (0.0 - 1.0) input bool StartDark = true; // Start In Dark Theme input int BorderWidth = 1; // Border Width (px) input int CategoryIconSize = 26; // Category Icon Size (pt) input int SnapThreshold = 40; // Edge Snap Threshold (px) //+------------------------------------------------------------------+ //| Category definition structure | //+------------------------------------------------------------------+ struct CategoryDefinition { string categoryLabel; // Display label for the category string iconFontName; // Font name used to render the icon uchar iconCharCode; // Character code of the icon glyph bool hasMultipleTools; // Flag indicating category has sub-tools }; //+------------------------------------------------------------------+ //| Theme color set structure | //+------------------------------------------------------------------+ struct ThemeColorSet { color sidebarBackground; // Background fill color of the sidebar panel color sidebarBorder; // Outline border color of the sidebar panel color buttonIconColor; // Color used to render category icons color gripDotsColor; // Color of the drag-grip dot indicators color separatorColor; // Color of the horizontal separator lines }; //+------------------------------------------------------------------+ //| CLASS 1 — Blend and draw low-level canvas primitives | //+------------------------------------------------------------------+ class CCanvasPrimitives { protected: //--- Blend a single pixel onto the canvas using alpha compositing void BlendPixelSet(CCanvas &canvas, int x, int y, uint sourceARGB); //--- Downsample a high-res canvas into a lower-res destination void DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor); //--- Fill a single corner quadrant of a rounded rectangle at high res void FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY); //--- Fill a full rounded rectangle at high resolution void FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb); //--- Fill a rounded rectangle with per-corner rounding control at high res void FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, bool rTL, bool rTR, bool rBL, bool rBR); //--- Fill a quadrilateral shape using scanline rasterization void FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb); //--- Draw a single thick border edge between two points void DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb); //--- Check whether an angle falls within a given arc range bool IsAngleBetween(double angle, double startAngle, double endAngle); //--- Draw a corner arc segment with specified thickness and angle range void DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle); //--- Draw a rounded rectangle border with per-corner rounding control at high res void DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness, bool rTL, bool rTR, bool rBL, bool rBR); }; //+------------------------------------------------------------------+ //| Blend source pixel onto canvas using alpha compositing | //+------------------------------------------------------------------+ void CCanvasPrimitives::BlendPixelSet(CCanvas &canvas, int x, int y, uint src) { //--- Skip pixels outside canvas bounds if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return; //--- Read existing destination pixel uint dst = canvas.PixelGet(x, y); //--- Unpack source ARGB channels to normalised floats double sA = ((src >> 24) & 0xFF) / 255.0, sR = ((src >> 16) & 0xFF) / 255.0; double sG = ((src >> 8) & 0xFF) / 255.0, sB = ( src & 0xFF) / 255.0; //--- Unpack destination ARGB channels to normalised floats double dA = ((dst >> 24) & 0xFF) / 255.0, dR = ((dst >> 16) & 0xFF) / 255.0; double dG = ((dst >> 8) & 0xFF) / 255.0, dB = ( dst & 0xFF) / 255.0; //--- Compute output alpha using standard over-compositing formula double oA = sA + dA * (1.0 - sA); //--- Write fully transparent pixel and exit if output alpha is zero if (oA == 0.0) { canvas.PixelSet(x, y, 0); return; } //--- Write blended ARGB pixel to canvas canvas.PixelSet(x, y, ((uint)(uchar)(oA * 255 + 0.5) << 24) | ((uint)(uchar)((sR * sA + dR * dA * (1.0 - sA)) / oA * 255 + 0.5) << 16) | ((uint)(uchar)((sG * sA + dG * dA * (1.0 - sA)) / oA * 255 + 0.5) << 8) | (uint)(uchar)((sB * sA + dB * dA * (1.0 - sA)) / oA * 255 + 0.5)); } //+------------------------------------------------------------------+ //| Downsample high-res canvas into destination by averaging pixels | //+------------------------------------------------------------------+ void CCanvasPrimitives::DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor) { //--- Cache destination dimensions and squared sample count int dW = dst.Width(), dH = dst.Height(), ss2 = factor * factor; //--- Iterate over every destination pixel for (int py = 0; py < dH; py++) for (int px = 0; px < dW; px++) { //--- Accumulate channel sums across the source sample block double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0; for (int dy = 0; dy < factor; dy++) for (int dx = 0; dx < factor; dx++) { //--- Compute source sample coordinates int sx = px * factor + dx, sy = py * factor + dy; //--- Skip samples outside source bounds if (sx >= src.Width() || sy >= src.Height()) continue; //--- Read source pixel and extract alpha uint p = src.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF); //--- Accumulate alpha unconditionally sA += a; //--- Accumulate colour channels only for non-transparent samples if (a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; } } //--- Compute averaged output alpha uchar fa = (uchar)(sA / ss2); //--- Write transparent pixel and skip if result is fully transparent if (fa == 0 || wc == 0) { dst.PixelSet(px, py, 0); continue; } //--- Write averaged ARGB pixel to destination canvas dst.PixelSet(px, py, ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) | ((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc)); } } //+------------------------------------------------------------------+ //| Fill one corner quadrant of a rounded rect at high resolution | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY) { //--- Cache radius as double and extract alpha and RGB components double rd = (double)radius; uchar bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF; //--- Set sub-pixel sample count and derived values int sub = 4; double step = 1.0 / sub; int subSq = sub * sub; //--- Iterate over pixel neighbourhood around corner centre for (int dy = -(radius + 1); dy <= (radius + 1); dy++) for (int dx = -(radius + 1); dx <= (radius + 1); dx++) { //--- Check pixel belongs to the target quadrant bool inQ = ((signX > 0) ? (dx >= 0) : (dx <= 0)) && ((signY > 0) ? (dy >= 0) : (dy <= 0)); if (!inQ) continue; //--- Compute distance from corner centre double dist = MathSqrt((double)(dx * dx + dy * dy)); //--- Skip pixels too far outside radius if (dist > rd + 1.0) continue; //--- Fill pixels fully inside radius without anti-aliasing if (dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; } //--- Count sub-pixel samples falling inside the circle int inside = 0; for (int sy = 0; sy < sub; sy++) for (int sx = 0; sx < sub; sx++) { //--- Compute sub-pixel offsets from pixel centre double sdx = (double)dx - 0.5 + (sx + 0.5) * step; double sdy = (double)dy - 0.5 + (sy + 0.5) * step; if (sdx * sdx + sdy * sdy <= rd * rd) inside++; } //--- Skip pixel if no sub-samples are inside if (inside == 0) continue; //--- Blend anti-aliased pixel using coverage fraction BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb); } } //+------------------------------------------------------------------+ //| Fill a fully rounded rectangle at high resolution | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb) { //--- Clamp radius to half the smallest dimension radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Fall back to plain rectangle when radius is zero or negative if (radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; } //--- Fill horizontal centre strip canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb); //--- Fill left vertical strip canvas.FillRectangle(x, y + radius, x + radius - 1, y + h - radius - 1, argb); //--- Fill right vertical strip canvas.FillRectangle(x + w - radius, y + radius, x + w - 1, y + h - radius - 1, argb); //--- Fill top-left corner quadrant FillCornerQuadrantHR(canvas, x + radius, y + radius, radius, argb, -1, -1); //--- Fill top-right corner quadrant FillCornerQuadrantHR(canvas, x + w - radius, y + radius, radius, argb, 1, -1); //--- Fill bottom-left corner quadrant FillCornerQuadrantHR(canvas, x + radius, y + h - radius, radius, argb, -1, 1); //--- Fill bottom-right corner quadrant FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb, 1, 1); } //+------------------------------------------------------------------+ //| Fill rounded rectangle with per-corner rounding at high res | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, bool rTL, bool rTR, bool rBL, bool rBR) { //--- Clamp radius to half the smallest dimension radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Fall back to plain rectangle when radius is zero or negative if (radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; } //--- Fill horizontal centre strip spanning full width canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb); //--- Fill left strip, respecting top-left and bottom-left rounding canvas.FillRectangle(x, y + (rTL ? radius : 0), x + radius - 1, y + h - 1 - (rBL ? radius : 0), argb); //--- Fill right strip, respecting top-right and bottom-right rounding canvas.FillRectangle(x + w - radius, y + (rTR ? radius : 0), x + w - 1, y + h - 1 - (rBR ? radius : 0), argb); //--- Fill or square top-left corner based on rounding flag if (rTL) FillCornerQuadrantHR(canvas, x + radius, y + radius, radius, argb, -1, -1); else canvas.FillRectangle(x, y, x + radius - 1, y + radius - 1, argb); //--- Fill or square top-right corner based on rounding flag if (rTR) FillCornerQuadrantHR(canvas, x + w - radius, y + radius, radius, argb, 1, -1); else canvas.FillRectangle(x + w - radius, y, x + w - 1, y + radius - 1, argb); //--- Fill or square bottom-left corner based on rounding flag if (rBL) FillCornerQuadrantHR(canvas, x + radius, y + h - radius, radius, argb, -1, 1); else canvas.FillRectangle(x, y + h - radius, x + radius - 1, y + h - 1, argb); //--- Fill or square bottom-right corner based on rounding flag if (rBR) FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb, 1, 1); else canvas.FillRectangle(x + w - radius, y + h - radius, x + w - 1, y + h - 1, argb); } //+------------------------------------------------------------------+ //| Fill a quadrilateral using scanline rasterization | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb) { //--- Find vertical bounding extent of the quad double minY = vy[0], maxY = vy[0]; for (int i = 1; i < 4; i++) { if (vy[i] < minY) minY = vy[i]; if (vy[i] > maxY) maxY = vy[i]; } //--- Iterate over each horizontal scanline within the bounding box for (int scanY = (int)MathCeil(minY); scanY <= (int)MathCeil(maxY) - 1; scanY++) { //--- Compute scanline centre Y and prepare intersection buffer double cy = (double)scanY + 0.5; double xi[8]; int nc = 0; //--- Compute X intersections with each edge of the quad for (int i = 0; i < 4; i++) { int ni = (i + 1) % 4; //--- Determine edge vertical extents double eMin = (vy[i] < vy[ni]) ? vy[i] : vy[ni], eMax = (vy[i] > vy[ni]) ? vy[i] : vy[ni]; //--- Skip edges that do not cross the current scanline if (cy < eMin || cy > eMax || MathAbs(vy[ni] - vy[i]) < 1e-12) continue; //--- Compute intersection parameter along the edge double t = (cy - vy[i]) / (vy[ni] - vy[i]); if (t < 0.0 || t > 1.0) continue; //--- Record intersection X coordinate xi[nc++] = vx[i] + t * (vx[ni] - vx[i]); } //--- Sort intersections left to right for (int a = 0; a < nc - 1; a++) for (int b = a + 1; b < nc; b++) if (xi[a] > xi[b]) { double tmp = xi[a]; xi[a] = xi[b]; xi[b] = tmp; } //--- Fill pixels between paired intersection spans for (int p = 0; p + 1 < nc; p += 2) for (int fx = (int)MathCeil(xi[p]); fx <= (int)MathCeil(xi[p + 1]) - 1; fx++) canvas.PixelSet(fx, scanY, argb); } } //+------------------------------------------------------------------+ //| Draw a thick border edge between two endpoints | //+------------------------------------------------------------------+ void CCanvasPrimitives::DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb) { //--- Compute edge direction vector and length double dx = x1 - x0, dy = y1 - y0, len = MathSqrt(dx * dx + dy * dy); //--- Skip degenerate edges with near-zero length if (len < 1e-6) return; //--- Compute perpendicular and unit direction vectors double px = -dy / len, py = dx / len, ex = dx / len, ey = dy / len; //--- Compute half-thickness and end cap extension double ht = thickness / 2.0, ext = 0.23 * thickness; //--- Extend start and end points slightly for mitre cap effect double sx = x0 - ex * ext, sy = y0 - ey * ext, ex2 = x1 + ex * ext, ey2 = y1 + ey * ext; //--- Build quad vertices offset perpendicular to edge direction double tvx[4] = { sx - px*ht, sx + px*ht, ex2 + px*ht, ex2 - px*ht }; double tvy[4] = { sy - py*ht, sy + py*ht, ey2 + py*ht, ey2 - py*ht }; //--- Fill the resulting quad as the border edge shape FillQuadrilateralBorder(canvas, tvx, tvy, argb); } //+------------------------------------------------------------------+ //| Check whether an angle falls within a start-to-end arc range | //+------------------------------------------------------------------+ bool CCanvasPrimitives::IsAngleBetween(double angle, double start, double end) { //--- Normalise all angles to the [0, 2π) range double tp = 2.0 * M_PI; angle = MathMod(angle + tp, tp); start = MathMod(start + tp, tp); end = MathMod(end + tp, tp); //--- Return true if angle lies within the arc from start to end return MathMod(angle - start + tp, tp) <= MathMod(end - start + tp, tp); } //+------------------------------------------------------------------+ //| Draw an anti-aliased corner arc with specified thickness | //+------------------------------------------------------------------+ void CCanvasPrimitives::DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle) { //--- Compute outer and inner radii for the arc ring double oR = (double)radius, iR = MathMax(0.0, (double)radius - thickness); //--- Extract alpha and RGB components from packed colour uchar bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF; //--- Set sub-pixel sample count and pixel scan radius int sub = 4; double step = 1.0 / sub; int subSq = sub * sub, pr = (int)(oR + 2.0); //--- Iterate over pixels in the bounding box of the arc for (int dy = -pr; dy <= pr; dy++) for (int dx = -pr; dx <= pr; dx++) { //--- Compute distance from arc centre double dist = MathSqrt((double)(dx * dx + dy * dy)); //--- Skip pixels clearly outside the ring or wrong angle if (dist > oR + 1.0 || dist < iR - 1.0) continue; if (!IsAngleBetween(MathArctan2((double)dy, (double)dx), startAngle, endAngle)) continue; //--- Fill pixels fully inside the ring without anti-aliasing if (dist <= oR - 1.0 && dist >= iR + 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; } //--- Count sub-pixel samples inside the arc ring and angle range int inside = 0; for (int sy = 0; sy < sub; sy++) for (int sx = 0; sx < sub; sx++) { double sdx = (double)dx - 0.5 + (sx + 0.5) * step, sdy = (double)dy - 0.5 + (sy + 0.5) * step; double sd = MathSqrt(sdx * sdx + sdy * sdy); if (sd >= iR && sd <= oR && IsAngleBetween(MathArctan2(sdy, sdx), startAngle, endAngle)) inside++; } //--- Skip pixel if no sub-samples qualify if (inside == 0) continue; //--- Write fully opaque pixel if all sub-samples qualify if (inside >= subSq) canvas.PixelSet(cx + dx, cy + dy, argb); //--- Blend anti-aliased pixel using coverage fraction else BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb); } } //+------------------------------------------------------------------+ //| Draw rounded rect border with per-corner rounding at high res | //+------------------------------------------------------------------+ void CCanvasPrimitives::DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness, bool rTL, bool rTR, bool rBL, bool rBR) { //--- Skip drawing when border width is disabled if (BorderWidth <= 0) return; //--- Clamp radius to half the smallest dimension radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Compute per-corner radii based on rounding flags and half-thickness offset int tlR = rTL ? radius : 0, trR = rTR ? radius : 0, blR = rBL ? radius : 0, brR = rBR ? radius : 0, h2 = thickness / 2; //--- Draw top edge DrawBorderEdge(canvas, x + tlR, y + h2, x + w - trR, y + h2, thickness, argb); //--- Draw right edge if any right corner is rounded if (rTR || rBR) DrawBorderEdge(canvas, x + w - h2, y + trR, x + w - h2, y + h - brR, thickness, argb); //--- Draw bottom edge DrawBorderEdge(canvas, x + w - brR, y + h - h2, x + blR, y + h - h2, thickness, argb); //--- Draw left edge if any left corner is rounded if (rTL || rBL) DrawBorderEdge(canvas, x + h2, y + h - blR, x + h2, y + tlR, thickness, argb); //--- Draw top-left corner arc if rounded if (rTL) DrawCornerArc(canvas, x + radius, y + radius, radius, thickness, argb, M_PI, M_PI * 1.5); //--- Draw top-right corner arc if rounded if (rTR) DrawCornerArc(canvas, x + w - radius, y + radius, radius, thickness, argb, M_PI * 1.5, M_PI * 2.0); //--- Draw bottom-left corner arc if rounded if (rBL) DrawCornerArc(canvas, x + radius, y + h - radius, radius, thickness, argb, M_PI * 0.5, M_PI); //--- Draw bottom-right corner arc if rounded if (rBR) DrawCornerArc(canvas, x + w - radius, y + h - radius, radius, thickness, argb, 0.0, M_PI * 0.5); } //+------------------------------------------------------------------+ //| CLASS 2 — Manage and apply light and dark theme colour sets | //+------------------------------------------------------------------+ class CThemeManager : public CCanvasPrimitives { protected: bool m_isDarkTheme; // Active theme flag: true = dark, false = light ThemeColorSet m_themeColors; // Active colour set for the current theme protected: //--- Apply colour values matching the current theme state void ApplyTheme(); }; //+------------------------------------------------------------------+ //| Apply colour values matching the current theme state | //+------------------------------------------------------------------+ void CThemeManager::ApplyTheme() { //--- Apply dark theme colour assignments if (m_isDarkTheme) { m_themeColors.sidebarBackground = C'30,34,45'; // Dark navy background m_themeColors.sidebarBorder = C'200,210,225'; // Light blue-grey border m_themeColors.buttonIconColor = C'220,225,235'; // Near-white icon colour m_themeColors.gripDotsColor = C'90,100,120'; // Muted slate grip dots m_themeColors.separatorColor = C'44,50,64'; // Dark separator line } else { //--- Apply light theme colour assignments m_themeColors.sidebarBackground = clrWhite; // White background m_themeColors.sidebarBorder = C'30,35,45'; // Dark border m_themeColors.buttonIconColor = C'40,45,58'; // Dark icon colour m_themeColors.gripDotsColor = C'160,170,185'; // Light grey grip dots m_themeColors.separatorColor = C'210,215,225'; // Light separator line } } //+------------------------------------------------------------------+ //| CLASS 3 — Register and initialise all tool category definitions | //+------------------------------------------------------------------+ class CCategoryRegistry : public CThemeManager { protected: CategoryDefinition m_categories[CAT_COUNT]; // Array of all category definitions protected: //--- Populate all category definitions with labels, icons, and tool flags void InitAllCategories(); }; //+------------------------------------------------------------------+ //| Populate all category definitions with labels, icons, flags | //+------------------------------------------------------------------+ void CCategoryRegistry::InitAllCategories() { //--- Assign Cursors category definition m_categories[CAT_CURSORS].categoryLabel = "Cursors"; m_categories[CAT_CURSORS].iconFontName = ICON_CATEGORY_CURSORS.fontName; m_categories[CAT_CURSORS].iconCharCode = ICON_CATEGORY_CURSORS.charCode; m_categories[CAT_CURSORS].hasMultipleTools = false; //--- Assign Lines category definition m_categories[CAT_LINES].categoryLabel = "Lines"; m_categories[CAT_LINES].iconFontName = ICON_CATEGORY_LINES.fontName; m_categories[CAT_LINES].iconCharCode = ICON_CATEGORY_LINES.charCode; m_categories[CAT_LINES].hasMultipleTools = true; //--- Assign Channels category definition m_categories[CAT_CHANNELS].categoryLabel = "Channels"; m_categories[CAT_CHANNELS].iconFontName = ICON_CATEGORY_CHANNELS.fontName; m_categories[CAT_CHANNELS].iconCharCode = ICON_CATEGORY_CHANNELS.charCode; m_categories[CAT_CHANNELS].hasMultipleTools = true; //--- Assign Pitchfork category definition m_categories[CAT_PITCHFORK].categoryLabel = "Pitchfork"; m_categories[CAT_PITCHFORK].iconFontName = ICON_CATEGORY_PITCHFORK.fontName; m_categories[CAT_PITCHFORK].iconCharCode = ICON_CATEGORY_PITCHFORK.charCode; m_categories[CAT_PITCHFORK].hasMultipleTools = true; //--- Assign Gann category definition m_categories[CAT_GANN].categoryLabel = "Gann"; m_categories[CAT_GANN].iconFontName = ICON_CATEGORY_GANN.fontName; m_categories[CAT_GANN].iconCharCode = ICON_CATEGORY_GANN.charCode; m_categories[CAT_GANN].hasMultipleTools = true; //--- Assign Fibonacci category definition m_categories[CAT_FIBONACCI].categoryLabel = "Fibonacci"; m_categories[CAT_FIBONACCI].iconFontName = ICON_CATEGORY_FIBONACCI.fontName; m_categories[CAT_FIBONACCI].iconCharCode = ICON_CATEGORY_FIBONACCI.charCode; m_categories[CAT_FIBONACCI].hasMultipleTools = true; //--- Assign Shapes category definition m_categories[CAT_SHAPES].categoryLabel = "Shapes"; m_categories[CAT_SHAPES].iconFontName = ICON_CATEGORY_SHAPES.fontName; m_categories[CAT_SHAPES].iconCharCode = ICON_CATEGORY_SHAPES.charCode; m_categories[CAT_SHAPES].hasMultipleTools = true; //--- Assign Annotations category definition m_categories[CAT_ANNOTATIONS].categoryLabel = "Annotate"; m_categories[CAT_ANNOTATIONS].iconFontName = ICON_CATEGORY_ANNOTATIONS.fontName; m_categories[CAT_ANNOTATIONS].iconCharCode = ICON_CATEGORY_ANNOTATIONS.charCode; m_categories[CAT_ANNOTATIONS].hasMultipleTools = true; } //+------------------------------------------------------------------+ //| CLASS 4 — Create, destroy, and resize all canvas layers | //+------------------------------------------------------------------+ class CCanvasLayer : public CCategoryRegistry { protected: int m_supersampleFactor; // Supersampling multiplier for high-res rendering long m_chartId; // Chart identifier this layer belongs to CCanvas m_canvasSidebar; // Final display-resolution sidebar canvas CCanvas m_canvasSidebarHighRes; // High-resolution sidebar canvas for supersampling string m_nameSidebar; // Object name of the sidebar bitmap label protected: //--- Create all canvas objects at the given dimensions bool CreateAllCanvases(int w, int h); //--- Destroy all canvas objects and remove chart objects void DestroyAllCanvases(); //--- Resize both sidebar canvases to the given dimensions void ResizeSidebarCanvases(int w, int h); }; //+------------------------------------------------------------------+ //| Create all canvas objects at the given dimensions | //+------------------------------------------------------------------+ bool CCanvasLayer::CreateAllCanvases(int w, int h) { //--- Create the display-resolution sidebar bitmap label canvas if (!m_canvasSidebar.CreateBitmapLabel(0, 0, m_nameSidebar, 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("CCanvasLayer: Failed to create sidebar canvas"); return false; } //--- Create the high-resolution sidebar canvas for supersampled drawing if (!m_canvasSidebarHighRes.Create("ToolsPalette_SidebarHR", w * m_supersampleFactor, h * m_supersampleFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("CCanvasLayer: Failed to create sidebar HR canvas"); return false; } return true; } //+------------------------------------------------------------------+ //| Destroy all canvas objects and remove chart objects | //+------------------------------------------------------------------+ void CCanvasLayer::DestroyAllCanvases() { //--- Destroy display canvas and remove its chart object m_canvasSidebar.Destroy(); ObjectDelete(0, m_nameSidebar); //--- Destroy the high-resolution working canvas m_canvasSidebarHighRes.Destroy(); } //+------------------------------------------------------------------+ //| Resize both sidebar canvases to the given dimensions | //+------------------------------------------------------------------+ void CCanvasLayer::ResizeSidebarCanvases(int w, int h) { //--- Resize the display-resolution sidebar canvas m_canvasSidebar.Resize(w, h); //--- Update the chart object dimensions to match ObjectSetInteger(0, m_nameSidebar, OBJPROP_XSIZE, w); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YSIZE, h); //--- Resize the high-resolution canvas scaled by the supersample factor m_canvasSidebarHighRes.Resize(w * m_supersampleFactor, h * m_supersampleFactor); } //+------------------------------------------------------------------+ //| CLASS 5 — Compute and maintain sidebar layout and geometry | //+------------------------------------------------------------------+ class CSidebarLayout : public CCanvasLayer { protected: int m_panelX; // Horizontal position of the sidebar panel int m_panelY; // Vertical position of the sidebar panel int m_sidebarWidth; // Width of the sidebar panel in pixels int m_sidebarHeight; // Height of the sidebar panel in pixels int m_categoryButtonSize; // Size of each category button in pixels int m_categoryButtonPadding; // Vertical gap between category buttons int m_panelCornerRadius; // Corner rounding radius of the panel int m_headerGripHeight; // Height of the top header and grip area ENUM_SNAP_STATE m_snapState; // Current snap alignment state int m_sidebarMaxVisibleCats; // Maximum number of visible category buttons protected: //--- Compute and set the sidebar panel height based on available chart space void CalcSidebarHeight(); //--- Compute the Y pixel position of a category button by index int CalcCategoryButtonY(int idx); //--- Compute the top clipping boundary for the category button area int CalcClipTop(); //--- Compute the bottom clipping boundary for the category button area int CalcClipBottom(); }; //+------------------------------------------------------------------+ //| Compute and set sidebar height based on available chart space | //+------------------------------------------------------------------+ void CSidebarLayout::CalcSidebarHeight() { //--- Get current chart height in pixels int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Define vertical padding constants int topPad = 8, botPad = 10; //--- Set button gap spacing m_categoryButtonPadding = 6; //--- Pin panel to fixed Y offset below chart top m_panelY = 30; ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Compute maximum available height below panel top offset int availH = chartH - m_panelY - 8; //--- Compute ideal natural height to fit all category buttons int naturalH = m_headerGripHeight + topPad + CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; //--- Compute minimum height to show at least three category buttons int minH = m_headerGripHeight + topPad + 3 * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; //--- Clamp sidebar height between minimum and available space m_sidebarHeight = MathMax(minH, MathMin(naturalH, availH)); //--- Compute usable height for the button area int btnAreaH = m_sidebarHeight - m_headerGripHeight - topPad - botPad; //--- Compute total height needed for all buttons at natural spacing int fullBtnH = CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding; //--- Compute how many buttons fit within the available button area m_sidebarMaxVisibleCats = (fullBtnH <= btnAreaH) ? CAT_COUNT : MathMax(3, MathMin(CAT_COUNT, btnAreaH / (m_categoryButtonSize + m_categoryButtonPadding))); } //+------------------------------------------------------------------+ //| Compute Y pixel position of a category button by index | //+------------------------------------------------------------------+ int CSidebarLayout::CalcCategoryButtonY(int idx) { //--- Return Y offset below the header grip area return m_headerGripHeight + 8 + idx * (m_categoryButtonSize + m_categoryButtonPadding); } //+------------------------------------------------------------------+ //| Compute top clip boundary for the category button area | //+------------------------------------------------------------------+ int CSidebarLayout::CalcClipTop() { //--- Return Y position just below the header grip bottom edge return m_headerGripHeight + 8; } //+------------------------------------------------------------------+ //| Compute bottom clip boundary for the category button area | //+------------------------------------------------------------------+ int CSidebarLayout::CalcClipBottom() { //--- Return Y position leaving bottom padding inside the panel return m_sidebarHeight - 10; } //+------------------------------------------------------------------+ //| CLASS 6 — Draw and composite all sidebar visual elements | //+------------------------------------------------------------------+ class CSidebarRenderer : public CSidebarLayout { protected: //--- Draw and composite the full sidebar onto its canvas void DrawSidebar(); //--- Draw the header grip strip at high resolution void DrawHeaderStripHR(int canvasW, int canvasH); //--- Draw a single category button indicator at high resolution void DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool hasDot); //--- Draw icon glyphs and separator lines onto the display canvas void DrawSidebarIconLabels(); }; //+------------------------------------------------------------------+ //| Draw and composite the full sidebar onto its canvas | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebar() { //--- Compute high-resolution canvas dimensions int ws = m_sidebarWidth * m_supersampleFactor, hs = m_sidebarHeight * m_supersampleFactor; //--- Resize high-res canvas if dimensions have changed if (m_canvasSidebarHighRes.Width() != ws || m_canvasSidebarHighRes.Height() != hs) m_canvasSidebarHighRes.Resize(ws, hs); //--- Clear the high-res canvas to fully transparent m_canvasSidebarHighRes.Erase(0x00000000); //--- Compute background alpha from opacity input uchar bgA = (uchar)(255 * BackgroundOpacity); //--- Determine which corners are rounded based on snap state bool rTL = (m_snapState != SNAP_LEFT), rBL = rTL; bool rTR = (m_snapState != SNAP_RIGHT), rBR = rTR; //--- Fill sidebar background with selective rounded corners FillSelectiveRoundRectHR(m_canvasSidebarHighRes, 0, 0, ws, hs, m_panelCornerRadius * m_supersampleFactor, ColorToARGB(m_themeColors.sidebarBackground, bgA), rTL, rTR, rBL, rBR); //--- Draw left snap flush edge using foreground chart colour if (m_snapState == SNAP_LEFT) m_canvasSidebarHighRes.FillRectangle(0, 0, m_supersampleFactor - 1, hs - 1, ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255)); //--- Draw right snap flush edge using foreground chart colour else if (m_snapState == SNAP_RIGHT) m_canvasSidebarHighRes.FillRectangle(ws - m_supersampleFactor, 0, ws - 1, hs - 1, ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255)); //--- Draw the header grip strip onto the high-res canvas DrawHeaderStripHR(ws, hs); //--- Draw each category button indicator at high resolution for (int c = 0; c < CAT_COUNT; c++) DrawCategoryButtonHR(m_canvasSidebarHighRes, (m_sidebarWidth - m_categoryButtonSize) / 2 * m_supersampleFactor, CalcCategoryButtonY(c) * m_supersampleFactor, m_categoryButtonSize * m_supersampleFactor, m_categories[c].hasMultipleTools); //--- Draw the panel border at high resolution if enabled if (BorderWidth > 0) DrawSelectiveRoundRectBorderHR(m_canvasSidebarHighRes, 0, 0, ws, hs, m_panelCornerRadius * m_supersampleFactor, ColorToARGB(m_themeColors.sidebarBorder, 255), BorderWidth * m_supersampleFactor, rTL, rTR, rBL, rBR); //--- Downsample high-res canvas into the display-resolution canvas DownsampleCanvas(m_canvasSidebar, m_canvasSidebarHighRes, m_supersampleFactor); //--- Overlay icon glyphs and separator lines onto the display canvas DrawSidebarIconLabels(); //--- Flush the display canvas to the chart m_canvasSidebar.Update(); } //+------------------------------------------------------------------+ //| Draw the header grip strip at high resolution | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawHeaderStripHR(int canvasW, int canvasH) { //--- Compute scaled header height and border inset int headerH = m_headerGripHeight * m_supersampleFactor; int brd = BorderWidth * m_supersampleFactor; //--- Compute horizontal insets based on snap state int inL = (m_snapState == SNAP_LEFT) ? 0 : brd, inR = (m_snapState == SNAP_RIGHT) ? 0 : brd; //--- Compute header fill rectangle position and size int hx = inL, hy = brd, hw = canvasW - inL - inR, hh = headerH - brd; //--- Compute inner corner radius inset from border int innerR = MathMax(0, m_panelCornerRadius * m_supersampleFactor - brd); //--- Determine top corner rounding based on snap state bool rTL = (m_snapState != SNAP_LEFT), rTR = (m_snapState != SNAP_RIGHT); //--- Select header fill colour based on active theme color hdrFill = m_isDarkTheme ? C'25,29,40' : C'245,247,252'; //--- Fill the upper portion of the header strip with rounded top corners FillSelectiveRoundRectHR(m_canvasSidebarHighRes, hx, hy, hw, hh, innerR, ColorToARGB(hdrFill, 255), rTL, rTR, false, false); //--- Fill the lower half of the header to square off the bottom m_canvasSidebarHighRes.FillRectangle(hx, hy + hh / 2, hx + hw - 1, headerH - 1, ColorToARGB(hdrFill, 255)); //--- Compute grip dot row position and height int row2Y = m_categoryButtonSize * m_supersampleFactor, row2H = 20 * m_supersampleFactor; //--- Compute dot spacing and radius at high resolution int gapX = 6 * m_supersampleFactor, dotR = 2 * m_supersampleFactor; uint dotColor = ColorToARGB(m_themeColors.gripDotsColor, 255); //--- Draw three horizontally centred grip dots for (int col = 0; col < 3; col++) m_canvasSidebarHighRes.FillCircle(canvasW / 2 + (col - 1) * gapX, row2Y + row2H / 2, dotR, dotColor); } //+------------------------------------------------------------------+ //| Draw a single category button dot indicator at high resolution | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool hasDot) { //--- Draw the multi-tool indicator dot in the bottom-right corner if applicable if (hasDot) target.FillCircle(xHR + sizeHR - 6 * m_supersampleFactor, yHR + sizeHR - 6 * m_supersampleFactor, 2 * m_supersampleFactor, ColorToARGB(m_themeColors.gripDotsColor, 180)); } //+------------------------------------------------------------------+ //| Draw icon glyphs and separator lines onto the display canvas | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebarIconLabels() { //--- Iterate over all category slots to draw icons for (int c = 0; c < CAT_COUNT; c++) { //--- Compute button top-left position in display-resolution coordinates int btnY = CalcCategoryButtonY(c), btnX = (m_sidebarWidth - m_categoryButtonSize) / 2; //--- Set font to the category icon font at configured size m_canvasSidebar.FontSet(m_categories[c].iconFontName, CategoryIconSize); //--- Build the icon glyph string from its character code string sym = CharToString(m_categories[c].iconCharCode); //--- Measure icon glyph dimensions for centring int iw = m_canvasSidebar.TextWidth(sym), ih = m_canvasSidebar.TextHeight(sym); //--- Draw the icon centred within the button area m_canvasSidebar.TextOut(btnX + (m_categoryButtonSize - iw) / 2, btnY + (m_categoryButtonSize - ih) / 2, sym, ColorToARGB(m_themeColors.buttonIconColor, 255)); } //--- Compute separator line left and right extents based on snap and border int brd = BorderWidth; int sepL = (m_snapState == SNAP_LEFT) ? 0 : brd, sepR = m_sidebarWidth - 1 - ((m_snapState == SNAP_RIGHT) ? 0 : brd); //--- Pack separator line colours for primary and secondary lines uint sepCol = ColorToARGB(m_themeColors.separatorColor, 255); uint sepCol2 = ColorToARGB(m_isDarkTheme ? C'45,52,66' : C'195,202,215', 255); //--- Draw primary separator below the header strip m_canvasSidebar.Line(sepL, m_headerGripHeight - 1, sepR, m_headerGripHeight - 1, sepCol); //--- Draw first secondary separator below the header row m_canvasSidebar.Line(sepL, m_categoryButtonSize, sepR, m_categoryButtonSize, sepCol2); //--- Draw second secondary separator below the theme toggle row m_canvasSidebar.Line(sepL, m_categoryButtonSize + 20, sepR, m_categoryButtonSize + 20, sepCol2); //--- Set font to Webdings for the close button icon m_canvasSidebar.FontSet("Webdings", CategoryIconSize); string closeSym = CharToString((uchar)114); // Webdings char 114 = close/X glyph //--- Measure close icon dimensions for centring int clW = m_canvasSidebar.TextWidth(closeSym), clH = m_canvasSidebar.TextHeight(closeSym); //--- Draw the close icon centred in the top header button slot m_canvasSidebar.TextOut((m_sidebarWidth - clW) / 2, (m_categoryButtonSize - clH) / 2, closeSym, ColorToARGB(m_themeColors.buttonIconColor, 255)); //--- Compute row extents for the theme toggle button slot int row3Y = m_categoryButtonSize + 20, row3H = m_headerGripHeight - m_categoryButtonSize - 20; //--- Set font to Wingdings for the theme toggle icon m_canvasSidebar.FontSet("Wingdings", CategoryIconSize); string themeSym = CharToString((uchar)91); // Wingdings char 91 = sun/moon glyph //--- Measure theme icon dimensions for centring int thW = m_canvasSidebar.TextWidth(themeSym), thH = m_canvasSidebar.TextHeight(themeSym); //--- Draw the theme toggle icon centred in its row slot m_canvasSidebar.TextOut((m_sidebarWidth - thW) / 2, row3Y + (row3H - thH) / 2, themeSym, ColorToARGB(m_themeColors.buttonIconColor, 255)); } //+------------------------------------------------------------------+ //| CLASS 7 — Top-level sidebar shell exposing the public interface | //+------------------------------------------------------------------+ class CToolsSidebar : public CSidebarRenderer { public: CToolsSidebar() { InitDefaults(); } // Construct and apply default state ~CToolsSidebar() { Destroy(); } // Destruct and clean up all resources //--- Initialise the sidebar and build all canvas objects bool Init(long chartId); //--- Destroy all canvas objects and release resources void Destroy(); //--- Handle incoming chart events (reserved for future interaction) void OnEvent(const int id, const long &lp, const double &dp, const string &sp) {} private: //--- Set all member variables to their compile-time default values void InitDefaults(); }; //+------------------------------------------------------------------+ //| Set all member variables to their compile-time default values | //+------------------------------------------------------------------+ void CToolsSidebar::InitDefaults() { //--- Reset chart reference to default m_chartId = 0; //--- Set the sidebar bitmap label object name m_nameSidebar = "ToolsPalette_Sidebar"; //--- Set supersampling factor for high-res rendering m_supersampleFactor = 4; //--- Set default button size and spacing m_categoryButtonSize = 36; m_categoryButtonPadding = 6; //--- Set panel corner rounding radius m_panelCornerRadius = 10; //--- Set header and grip strip height m_headerGripHeight = 92; //--- Set sidebar panel width m_sidebarWidth = 48; //--- Reset computed height and visible category count m_sidebarHeight = 0; m_sidebarMaxVisibleCats = 0; //--- Reset panel position to origin m_panelX = 0; m_panelY = CanvasY; //--- Default snap state to left edge m_snapState = SNAP_LEFT; //--- Apply the starting theme from user input m_isDarkTheme = StartDark; } //+------------------------------------------------------------------+ //| Initialise the sidebar and build all canvas objects | //+------------------------------------------------------------------+ bool CToolsSidebar::Init(long chartId) { //--- Reset all members to defaults before initialising InitDefaults(); //--- Store the target chart identifier m_chartId = chartId; //--- Populate all category definitions InitAllCategories(); //--- Apply the active theme colour set ApplyTheme(); //--- Compute and set the sidebar panel height CalcSidebarHeight(); //--- Create all canvas layers; abort on failure if (!CreateAllCanvases(m_sidebarWidth, m_sidebarHeight)) return false; //--- Position the sidebar panel on the chart ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Set the sidebar Z-order to render above chart objects ObjectSetInteger(0, m_nameSidebar, OBJPROP_ZORDER, 100); //--- Draw the initial sidebar frame DrawSidebar(); return true; } //+------------------------------------------------------------------+ //| Destroy all canvas objects and release resources | //+------------------------------------------------------------------+ void CToolsSidebar::Destroy() { //--- Delegate canvas cleanup to the layer base class DestroyAllCanvases(); } //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ CToolsSidebar g_sidebar; // Global sidebar instance //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- Initialise the sidebar and fail if canvas creation fails if (!g_sidebar.Init(ChartID())) return INIT_FAILED; //--- Force a chart redraw to display the sidebar immediately ChartRedraw(); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- //--- Destroy the sidebar and remove all canvas objects g_sidebar.Destroy(); //--- Force a chart redraw to clear the sidebar from view ChartRedraw(); } //+------------------------------------------------------------------+ //| Expert chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lp, const double &dp, const string &sp) { //--- //--- Forward the chart event to the sidebar event handler g_sidebar.OnEvent(id, lp, dp, sp); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+