preview
MQL5 Trading Tools (Part 14): Pixel-Perfect Scrollable Text Canvas with Antialiasing and Rounded Scrollbar

MQL5 Trading Tools (Part 14): Pixel-Perfect Scrollable Text Canvas with Antialiasing and Rounded Scrollbar

MetaTrader 5Trading systems |
225 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Introduction

In our previous article (Part 13), we developed a canvas-based price dashboard in MetaQuotes Language 5 (MQL5) using the CCanvas class for interactive panels visualizing price graphs with line plots and fog effects, alongside stats for account metrics and bar details, supporting background images, gradients, theme toggling, dragging, and resizing with event handling. In Part 14, we enhance it with a pixel-perfect scrollable text canvas with antialiasing and a rounded scrollbar to bypass MQL5 limits. This enhancement introduces a text panel for usage guides, featuring smooth scrolling via custom, antialiased elements. It includes a hover-expand scrollbar with buttons/slider, wheel support, themed backgrounds/opacity, and dynamic line wrapping/coloring, all integrated seamlessly for comprehensive user instructions. We will cover the following topics:

  1. Understanding the Pixel-Perfect Scrollable Text Canvas Framework
  2. Implementation in MQL5
  3. Backtesting
  4. Conclusion

By the end, you’ll have an MQL5 canvas dashboard that now includes a detailed and interactive text guide panel, which is ready for further customization. Let’s dive in!


Understanding the Pixel-Perfect Scrollable Text Canvas Framework

The pixel-perfect scrollable text canvas framework addresses MQL5's limitations in native text scrolling by utilizing custom pixel-level rendering with antialiasing for smooth edges, a rounded scrollbar that expands on hover for improved usability, and interactive elements such as up/down buttons and a draggable slider to navigate long content, including usage guides. It supports themed backgrounds with adjustable opacity, dynamic line wrapping to fit panel width while preserving colors for headings/links, and mouse wheel scrolling within the text body to bypass chart zoom interference, ensuring precise control without relying on built-in objects. Integration with the dashboard enables seamless updates on events, maintaining consistency across graph/stats/, and text panels for a cohesive monitoring tool.

We chose not to use the static MQL5 objects for the lines and want to fully explore the capabilities of the canvas. The good thing is that with the canvas, we don't really need to worry about text overflow over the borders, like we were experiencing with the past articles where we used the native objects; the canvas clips the texts automatically, helping us achieve a website scroll effect. Also, the rounded dynamic scrollbar was inspired by the appealing MetaQuotes terminals overlay that they have in the recent updates. Have a look at what we want to achieve at the end from the inspiration.

METAQUOTES SCROLLBAR INSPIRATION

We plan to extend inputs/enumerations for text panel options like height/opacity, add a text canvas object with creation/destruction logic, define globals for scrolling states/positions/dimensions/colors, implement text wrapping with color detection/heading bolding, draw rounded scrollbar with hover effects/buttons/slider using arcs/circles/rectangles, handle mouse events for hovers/clicks/drags/wheel in the text area to update scroll/tooltip/scroll disable, and ensure theme toggles/minimize/resizing adjust the text panel dynamically. In brief, here is a visual representation of our objectives.

TEXT CANVAS OBJECTIVES FRAMEWORK


Implementation in MQL5

To enhance the program in MQL5, we will need to declare the new text canvas panel and add some extra inputs and global variables to dynamically control its rendering.

//+------------------------------------------------------------------+
//|                                       Canvas Dashboard PART2.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

//+------------------------------------------------------------------+
//| Canvas objects                                                   |
//+------------------------------------------------------------------+

//---

CCanvas canvasText;                  // Declare text canvas

//+------------------------------------------------------------------+
//| Canvas names                                                     |
//+------------------------------------------------------------------+

//---

string canvasTextName = "TextCanvas";   // Set text canvas name

//--- Added Extra inputs

input bool EnableTextPanel            = true; // Enable Text Panel
input int TextPanelHeight             = 200; // Text Panel Height
input double TextBackgroundOpacityPercent = 85.0; // Text Background Opacity Percent

input double StatsHeaderBgOpacityPercent = 20.0; // Stats Header Bg Opacity Percent
input int StatsHeaderBgRadius         = 8; // Stats Header Bg Radius

//--- Extended the globals

uint bg_pixels_text[];               //--- Store text background pixels

int prev_mouse_state = 0;            //--- Initialize previous mouse state
int last_mouse_x = 0, last_mouse_y = 0; //--- Initialize last mouse positions
int header_height = 27;              //--- Set header height
int gap_y = 7;                       //--- Set Y gap
int button_size = 25;                //--- Set button size
int theme_x_offset = -75;            //--- Set theme X offset
int minimize_x_offset = -50;         //--- Set minimize X offset
int close_x_offset = -25;            //--- Set close X offset
bool panels_minimized = false;       //--- Initialize panels minimized flag
color HeaderColor = C'60,60,60';     //--- Set header color
color HeaderHoverColor = clrRed;     //--- Set header hover color
color HeaderDragColor = clrMediumBlue; //--- Set header drag color

The first thing we do to implement the enhancements is declare an additional CCanvas object named "canvasText" for the new text panel, which will handle the scrollable usage guide content. We add a string constant "canvasTextName" set to 'TextCanvas' to identify this canvas uniquely, similar to the others. Next, we extend the input parameters to support the text panel: "EnableTextPanel" as a boolean defaulting to true to toggle it on/off, "TextPanelHeight" at two hundred pixels for its height, "TextBackgroundOpacityPercent" at 85.0 for background transparency control, "StatsHeaderBgOpacityPercent" at 20.0 for stats header background opacity, and "StatsHeaderBgRadius" at eight for rounded corners on stats headers. We include a new uint array "bg_pixels_text" to store scaled background pixels for the text panel, ensuring consistency with graph and stats if backgrounds are used.

Next, we retain and initialize globals for mouse interaction. These include "prev_mouse_state" at zero, and "last_mouse_x" and "last_mouse_y" at zero for tracking positions. We set "header_height" at twenty-seven and "gap_y" at seven for vertical spacing. The "button_size" is twenty-five. For button placements relative to the header right edge, we use offsets: "theme_x_offset" at negative seventy-five, "minimize_x_offset" at negative fifty, and "close_x_offset" at negative twenty-five. We also keep "panels_minimized" as false to start expanded. Colors are defined: "HeaderColor" as medium gray, "HeaderHoverColor" as red, and "HeaderDragColor" as medium blue for header states. The next step is to add the text to render on the text panel and define its control globals as follows.

string text_usage_text = 
"\nCanvas Dashboard Usage Guide\n\n"
"Welcome to the Canvas Dashboard – Your Interactive Tool for Real-Time Market Monitoring in MetaTrader 5!\n\n"
"Enhance your trading experience with this dynamic dashboard that visualizes price data, account stats, and interactive controls. Designed for ease of use, it allows customization through dragging, resizing, theme switching, and more, while providing essential market insights at a glance.\n\n"
"Key Features:\n"
"- Price Graph Panel: Displays recent bar closes with a fog gradient fill, background image support, and resize indicators.\n"
"- Stats Panel: Shows account balance, equity, and current bar OHLC values with customizable backgrounds (single color or gradient).\n"
"- Header Controls: Drag to move the dashboard; buttons for theme toggle (dark/light), minimize/maximize panels, and close.\n"
"- Text Panel: Scrollable guide (this panel) with hover-expand scrollbar, up/down buttons, and slider for navigation.\n"
"- Interactivity: Hover for highlights/tooltips; resize via borders (bottom, right, corner); wheel scroll in text area.\n"
"- Theme Support: Switch between dark and light modes for better visibility on different chart backgrounds.\n"
"- Background Options: Enable images with fog overlay; blend modes for transparency.\n\n"
"Usage Instructions:\n"
"1. Move the Dashboard: Click and drag the header (excluding buttons) to reposition on the chart.\n"
"2. Resize Panels: Hover near the graph's bottom/right edges; click and drag when the icon appears (arrows for direction).\n"
"3. Toggle Theme: Click the '[' icon in the header to switch between dark and light modes.\n"
"4. Minimize/Maximize: Click the 'r' or 'o' icon to hide/show panels (header remains visible).\n"
"5. Navigate Text: Use the scrollbar (hovers to expand with buttons/slider); click up/down or drag slider; wheel scroll in body.\n"
"6. Close Dashboard: Click the 'X' icon in the header to remove it from the chart.\n\n"
"Important Notes:\n"
"- Risk Disclaimer: This dashboard is for informational purposes only. Always verify data and trade responsibly.\n"
"- Compatibility Check: Ensure chart settings allow mouse events; test on demo for interactions.\n"
"- Optimization Tips: Adjust input parameters like graph bars, fonts, colors, and opacity for your setup.\n"
"- Security Measures: No account modifications; use on trusted platforms.\n"
"- Legal Notice: No guarantees of accuracy. Consult professionals as needed.\n\n"
"Contact Methods:\n"
"NB:\n"
"********************************************\n"
" >*** FOR SUPPORT, QUERIES, OR CUSTOMIZATIONS, REACH OUT IMMEDIATELY: ***<\n"
" __________________________________________\n\n"
"1. Email: mutiiriallan.forex@gmail.com (Primary Support Channel)\n"
"2. Telegram Channel: @ForexAlgo-Trader (Updates & Community)\n"
"3. Telegram Group: https://t.me/Forex_Algo_Trader (Direct Assistance & Discussions)\n\n"
"********************************************\n\n"
"Thank you for choosing our Canvas Dashboard solutions. Use wisely, monitor confidently, and elevate your trading journey! 🚀\n"; //--- Set text usage content

int text_scroll_pos = 0;                   //--- Initialize text scroll position
int text_max_scroll = 0;                   //--- Initialize text max scroll
int text_slider_height = 20;               //--- Set text slider height
bool text_movingStateSlider = false;       //--- Initialize text slider moving flag
int text_mlbDownY_Slider = 0;              //--- Initialize text slider down Y
int text_mlbDown_YD_Slider = 0;            //--- Initialize text slider down YD
int text_total_height = 0;                 //--- Initialize text total height
int text_visible_height = 0;               //--- Initialize text visible height
bool text_scroll_visible = false;          //--- Initialize text scroll visible flag
bool text_mouse_in_body = false;           //--- Initialize text mouse in body flag
bool prev_text_mouse_in_body = false;      //--- Initialize previous text mouse in body flag
bool text_scroll_up_hovered = false;       //--- Initialize text scroll up hovered flag
bool text_scroll_down_hovered = false;     //--- Initialize text scroll down hovered flag
bool text_scroll_slider_hovered = false;   //--- Initialize text scroll slider hovered flag
bool text_scroll_area_hovered = false;     //--- Initialize text scroll area hovered flag
const int TEXT_MAX_LINES = 100;            //--- Set text max lines
int text_adjustedLineHeight = 0;           //--- Initialize text adjusted line height

int text_scrollbar_full_width = 16;        //--- Set text scrollbar full width
int text_scrollbar_thin_width = 2;         //--- Set text scrollbar thin width
int text_track_width = 16;                 //--- Set text track width
int text_scrollbar_margin = 5;             //--- Set text scrollbar margin
int text_button_size = 16;                 //--- Set text button size
color text_leader_color_dark = C'45,45,45'; //--- Set dark text leader color
color text_leader_color_light = C'200,200,200'; //--- Set light text leader color
color text_button_bg_dark = C'60,60,60';        //--- Set dark text button bg
color text_button_bg_light = C'220,220,220';    //--- Set light text button bg
color text_button_bg_hover_dark = C'70,70,70';  //--- Set dark text button bg hover
color text_button_bg_hover_light = C'180,180,180'; //--- Set light text button bg hover
color text_arrow_color_dark = C'150,150,150';   //--- Set dark text arrow color
color text_arrow_color_light = C'50,50,50';     //--- Set light text arrow color
color text_arrow_color_disabled_dark = C'80,80,80'; //--- Set dark disabled arrow color
color text_arrow_color_disabled_light = C'150,150,150'; //--- Set light disabled arrow color
color text_arrow_color_hover_dark = C'100,100,100'; //--- Set dark hover arrow color
color text_arrow_color_hover_light = C'0,0,0';  //--- Set light hover arrow color
color text_slider_bg_dark = C'80,80,80';        //--- Set dark slider bg
color text_slider_bg_light = C'150,150,150';    //--- Set light slider bg
color text_slider_bg_hover_dark = C'100,100,100';  //--- Set dark slider bg hover
color text_slider_bg_hover_light = C'100,100,100'; //--- Set light slider bg hover

color text_bg_light = clrWhite;                 //--- Set light text bg
color text_bg_dark = clrBlack;                  //--- Set dark text bg
color text_base_light = clrBlack;               //--- Set light text base
color text_base_dark = clrWhite;                //--- Set dark text base

Here, we define the "text_usage_text" string containing the full content for the usage guide to be displayed in the text panel, structured with newlines for paragraphs, sections like welcome message, key features with bullet points for panels/controls/interactivity/themes/backgrounds, usage instructions as numbered steps for moving/resizing/toggling/navigating/closing, important notes on risks/compatibility/optimization/security/legal, contact methods with email/Telegram, and a thank-you note, all formatted to provide comprehensive user guidance. This is just an arbitrary text formatting that we thought of to use as a demonstration. Feel free to go ahead and alter it to suit your specific needs.

We then initialize variables for text scrolling management: "text_scroll_pos" at zero for current offset, "text_max_scroll" at zero for maximum scrollable amount, "text_slider_height" at twenty for slider size, "text_movingStateSlider" false to track if dragging the slider, "text_mlbDownY_Slider" and "text_mlbDown_YD_Slider" at zero for mouse down y positions during slider interaction, "text_total_height" and "text_visible_height" at zero for full content and viewable heights, "text_scroll_visible" false to control scrollbar display, "text_mouse_in_body" and "prev_text_mouse_in_body" false for current/previous mouse in text body states, hover flags like "text_scroll_up_hovered" false for up/down/slider/area, constant "TEXT_MAX_LINES" at one hundred as a buffer limit, and "text_adjustedLineHeight" at zero for line spacing. We set dimensions for the scrollbar: "text_scrollbar_full_width" at sixteen for expanded, "text_scrollbar_thin_width" at two for collapsed, "text_track_width" at sixteen for track, "text_scrollbar_margin" at five for padding, "text_button_size" at sixteen for up/down buttons. We define themed colors: leader/button bg/normal/hover, arrow normal/disabled/hover for dark/light modes using C'...' notations, and base bg/text as white/black for light or black/white for dark to ensure visibility across themes.

The next thing we will do is update the statistics canvas by adding the rounded rectangles. We will define a function to achieve that first.

//+------------------------------------------------------------------+
//| Fill rounded rectangle                                           |
//+------------------------------------------------------------------+
void FillRoundedRectangle(CCanvas &cvs, int x, int y, int w, int h, int radius, uint argb_color)
  {
   if (radius <= 0) {
      cvs.FillRectangle(x, y, x + w - 1, y + h - 1, argb_color);                //--- Fill rectangle if no radius
      return;                          //--- Exit
     }
   radius = MathMin(radius, MathMin(w / 2, h / 2));                             //--- Adjust radius

   cvs.Arc(x + radius, y + radius, radius, radius, DegreesToRadians(180), DegreesToRadians(90), argb_color); //--- Draw top-left arc
   cvs.Arc(x + w - radius - 1, y + radius, radius, radius, DegreesToRadians(270), DegreesToRadians(90), argb_color); //--- Draw top-right arc
   cvs.Arc(x + w - radius - 1, y + h - radius - 1, radius, radius, DegreesToRadians(0), DegreesToRadians(90), argb_color); //--- Draw bottom-right arc
   cvs.Arc(x + radius, y + h - radius - 1, radius, radius, DegreesToRadians(90), DegreesToRadians(90), argb_color); //--- Draw bottom-left arc

   cvs.FillCircle(x + radius, y + radius, radius, argb_color);                  //--- Fill top-left circle
   cvs.FillCircle(x + w - radius - 1, y + radius, radius, argb_color);          //--- Fill top-right circle
   cvs.FillCircle(x + w - radius - 1, y + h - radius - 1, radius, argb_color);  //--- Fill bottom-right circle
   cvs.FillCircle(x + radius, y + h - radius - 1, radius, argb_color);          //--- Fill bottom-left circle

   cvs.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb_color); //--- Fill horizontal areas

   cvs.FillRectangle(x, y + radius, x + w - 1, y + h - radius - 1, argb_color); //--- Fill vertical areas
  }

We implement the "FillRoundedRectangle" function to draw a rectangle with rounded corners on a given CCanvas reference object, taking parameters for position x/y, width w/height h, corner radius, and ARGB color. If radius is zero or less, we simply fill a standard rectangle from (x,y) to (x+w-1, y+h-1) with the FillRectangle method and exit early. Otherwise, we clamp the radius to the minimum of half the width or height using MathMin to avoid overflow. We draw quarter arcs with Arc for each corner: top-left from 180 to 270 degrees, top-right from 270 to 360, bottom-right from 0 to 90, bottom-left from 90 to 180, all converted to radians with a helper like "DegreesToRadians". We will define this function later.

Then, we fill full circles at each corner center with FillCircle to solidify the rounded areas. Then, we fill the horizontal middle strip from left radius to right minus radius across full height, and vertical sides from top radius to bottom minus radius across full width, both with "FillRectangle" to complete the shape without gaps. The helper conversion function is as follows.

//+------------------------------------------------------------------+
//| Convert degrees to radians                                       |
//+------------------------------------------------------------------+
double DegreesToRadians(double degrees) {
   return((M_PI * degrees) / 180.0); //--- Perform conversion
}

We just divide the multiplication of 'pi' or M_PI by the target degrees by 180. Then, we can use the rounded rectangle function to draw our headings in the stats panel by updating the function.

//+------------------------------------------------------------------+
//| Update stats on canvas                                           |
//+------------------------------------------------------------------+
void UpdateStatsOnCanvas()
  {

//---

string headerText = "Account Stats";                               //--- Set header text
canvasStats.FontSet("Arial Bold", StatsHeaderFontSize);            //--- Set font
int textW = canvasStats.TextWidth(headerText);                     //--- Get text width
int textH = canvasStats.TextHeight(headerText);                    //--- Get text height
int pad = 5;                                                       //--- Set padding
int rectX = (statsWidth - textW) / 2 - pad;                        //--- Calculate rect X
int rectY = yPos - pad / 2;                                        //--- Calculate rect Y
int rectW = textW + 2 * pad;                                       //--- Calculate rect width
int rectH = textH + pad;                                           //--- Calculate rect height
uchar alpha = (uchar)(255 * (StatsHeaderBgOpacityPercent / 100.0)); //--- Calculate alpha
uint argbHeaderBg = ColorToARGB(GetStatsHeaderBgColor(), alpha);   //--- Convert bg to ARGB

color header_border = is_dark_theme ? clrWhite : clrBlack;         //--- Get border color
uint argbHeaderBorder = ColorToARGB(header_border, 255);           //--- Convert to ARGB
FillRoundedRectangle(canvasStats, rectX - 1, rectY - 1, rectW + 2, rectH + 2, StatsHeaderBgRadius + 1, argbHeaderBorder); //--- Fill outer rectangle

FillRoundedRectangle(canvasStats, rectX, rectY, rectW, rectH, StatsHeaderBgRadius, argbHeaderBg); //--- Fill inner rectangle

uint argbHeader = ColorToARGB(headerColor, 255);                   //--- Convert header to ARGB
canvasStats.TextOut(statsWidth / 2, yPos, headerText, argbHeader, TA_CENTER); //--- Draw header

//---

   }

To implement the changes, we modify the "UpdateStatsOnCanvas" function to enhance header rendering with rounded backgrounds for a more polished look. For each header like 'Account Stats' or 'Current Bar Stats', we set the font to Arial Bold at "StatsHeaderFontSize" with FontSet, measure text width and height using TextWidth and TextHeight, define padding at five pixels, and calculate rectangle position/dimensions centered with padding: x as (width minus text width)/2 minus pad, y as current "yPos" minus half pad, width as text plus double pad, height as text plus pad.

We compute alpha from "StatsHeaderBgOpacityPercent" over 100.0 times 255 cast to uchar, convert background color from "GetStatsHeaderBgColor" to ARGB with that alpha. For borders, we select white in dark theme or black in light, convert to full opacity ARGB, and call "FillRoundedRectangle" twice: once for the outer border at position minus one, size plus two, radius plus one using border ARGB; then the inner at base position/size/radius with background ARGB. We convert the header color to ARGB at 255, draw the text centered at stats width /2 and "yPos" with TextOut and center alignment, then increment "yPos" by thirty for spacing before the next section. The rest of the function remains as we defined it previously. Upon compilation, we now get the following outcome.

UPDATED STATISTICS HEADINGS WITH ROUNDED RECTANGLES

We can see that the statistics panel headings are now in rounded rectangles. The next thing we want to do is render the text panel. We will create a function for it as well, like the other panels, but first, we will need to wrap the text so it is dynamic, alongside other helper functions that we will need later. Here is the approach we used to achieve that.

//+------------------------------------------------------------------+
//| Lighten color                                                    |
//+------------------------------------------------------------------+
color LightenColor(color colorValue, double factor)
  {
   int blue = (int)MathMin(255, (colorValue & 0xFF) * factor);         //--- Lighten blue
   int green = (int)MathMin(255, ((colorValue >> 8) & 0xFF) * factor); //--- Lighten green
   int red = (int)MathMin(255, ((colorValue >> 16) & 0xFF) * factor);  //--- Lighten red
   return (color)(blue | (green << 8) | (red << 16));                  //--- Return lightened color
  }

//+------------------------------------------------------------------+
//| Calculate slider height                                          |
//+------------------------------------------------------------------+
int TextCalculateSliderHeight()
  {
   int scroll_area_height = TextPanelHeight - 2 - 2 * text_button_size; //--- Calculate area height
   int slider_min_height = 20;         //--- Set min height
   if (text_total_height <= text_visible_height) return scroll_area_height; //--- Return full if no scroll
   double visible_ratio = (double)text_visible_height / text_total_height;  //--- Calculate ratio
   int height = (int)MathFloor(scroll_area_height * visible_ratio);         //--- Calculate height
   return MathMax(slider_min_height, height);                           //--- Return max of min and calculated
  }

//+------------------------------------------------------------------+
//| Scroll text up                                                   |
//+------------------------------------------------------------------+
void TextScrollUp()
  {
   if (text_adjustedLineHeight > 0 && text_scroll_pos > 0)
     {
      text_scroll_pos = MathMax(0, text_scroll_pos - text_adjustedLineHeight); //--- Scroll up
      UpdateTextOnCanvas();                                            //--- Update canvas
     }
  }

//+------------------------------------------------------------------+
//| Scroll text down                                                 |
//+------------------------------------------------------------------+
void TextScrollDown()
  {
   if (text_adjustedLineHeight > 0 && text_scroll_pos < text_max_scroll)
     {
      text_scroll_pos = MathMin(text_max_scroll, text_scroll_pos + text_adjustedLineHeight); //--- Scroll down
      UpdateTextOnCanvas();                                            //--- Update canvas
     }
  }

//+------------------------------------------------------------------+
//| Update text hover effects                                        |
//+------------------------------------------------------------------+
void TextUpdateHoverEffects(int local_x, int local_y)
  {
   if (!text_scroll_visible) return;                                    //--- Exit if no scroll
   int scrollbar_x = canvasText.Width() - text_track_width - 1;         //--- Get scrollbar X
   int scrollbar_y = 1;                                                 //--- Set scrollbar Y
   int scrollbar_height = TextPanelHeight - 2;                          //--- Get height
   text_scroll_up_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 &&
                             local_y >= scrollbar_y && local_y <= scrollbar_y + text_button_size - 1); //--- Check up hover
   int down_y = scrollbar_y + scrollbar_height - text_button_size;      //--- Calculate down Y
   text_scroll_down_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 &&
                               local_y >= down_y && local_y <= down_y + text_button_size - 1); //--- Check down hover
   int scroll_area_height = scrollbar_height - 2 * text_button_size;    //--- Calculate area height
   int slider_y = scrollbar_y + text_button_size + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y
   text_scroll_slider_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 &&
                                 local_y >= slider_y && local_y <= slider_y + text_slider_height - 1); //--- Check slider hover
  }

//+------------------------------------------------------------------+
//| Get line color                                                   |
//+------------------------------------------------------------------+
color GetLineColor(string lineText)
  {
   if (StringLen(lineText) == 0 || lineText == " ") return C'25,25,25'; //--- Return gray for empty
   if (StringFind(lineText, "mutiiriallan.forex@gmail.com") >= 0) return C'255,100,100';   //--- Return red for email
   if (StringFind(lineText, "https://t.me/Forex_Algo_Trader") >= 0) return C'150,100,200'; //--- Return purple for link
   if (StringFind(lineText, "@ForexAlgo-Trader") >= 0) return C'100,150,255';              //--- Return blue for channel
   if (StringFind(lineText, "http") >= 0 || StringFind(lineText, "t.me") >= 0) return C'100,150,255'; //--- Return blue for links
   string start3 = StringSubstr(lineText, 0, 3);                        //--- Get start substring
   if ((start3 == "1. " || start3 == "2. " || start3 == "3. " || start3 == "4. " || start3 == "5. ") &&
       StringFind(lineText, "Initial Setup Instructions") < 0) return C'255,200,100';      //--- Return orange for numbered
   return clrWhite;                                                     //--- Return white default
  }

//+------------------------------------------------------------------+
//| Check if heading                                                 |
//+------------------------------------------------------------------+
bool IsHeading(string lineText)
  {
   if (StringLen(lineText) == 0) return false;                          //--- False for empty
   if (StringGetCharacter(lineText, StringLen(lineText) - 1) == ':') return true; //--- True for colon end
   if (StringFind(lineText, "Canvas Dashboard Usage Guide") >= 0) return true;    //--- True for guide
   if (StringFind(lineText, "Key Features") >= 0) return true;          //--- True for features
   if (StringFind(lineText, "Usage Instructions") >= 0) return true;    //--- True for instructions
   if (StringFind(lineText, "Important Notes") >= 0) return true;       //--- True for notes
   if (StringFind(lineText, "Contact Methods") >= 0) return true;       //--- True for contacts
   if (StringFind(lineText, "NB:") >= 0) return true;                   //--- True for NB
   return false;                                                        //--- Return false
  }

//+------------------------------------------------------------------+
//| Wrap text                                                        |
//+------------------------------------------------------------------+
void WrapText(const string inputText, const string font, const int fontSize, const int maxWidth, string &wrappedLines[], color &wrappedColors[])
  {
   ArrayResize(wrappedLines, 0);                                        //--- Reset lines
   ArrayResize(wrappedColors, 0);                                       //--- Reset colors
   string paragraphs[];                                                 //--- Declare paragraphs
   int numParagraphs = StringSplit(inputText, '\n', paragraphs);        //--- Split by newline
   for (int p = 0; p < numParagraphs; p++)
     {
      string para = paragraphs[p];                                      //--- Get paragraph
      color paraColor = GetLineColor(para);                             //--- Get color
      if (StringLen(para) == 0)
        {
         int size = ArraySize(wrappedLines);                            //--- Get size
         ArrayResize(wrappedLines, size + 1);                           //--- Resize
         wrappedLines[size] = " ";                                      //--- Add space
         ArrayResize(wrappedColors, size + 1);                          //--- Resize colors
         wrappedColors[size] = C'25,25,25';                             //--- Set gray
         continue;                                                      //--- Continue
        }
      string words[];                                                   //--- Declare words
      int numWords = StringSplit(para, ' ', words);                     //--- Split by space
      string currentLine = "";                                          //--- Initialize line
      for (int w = 0; w < numWords; w++)
        {
         string testLine = currentLine + (StringLen(currentLine) > 0 ? " " : "") + words[w]; //--- Build test line
         canvasText.FontSet(font, fontSize);                            //--- Set font
         int textW = canvasText.TextWidth(testLine);                    //--- Get width
         if (textW <= maxWidth)
           {
            currentLine = testLine;                                     //--- Update line
           }
         else
           {
            if (StringLen(currentLine) > 0)
              {
               int size = ArraySize(wrappedLines);                     //--- Get size
               ArrayResize(wrappedLines, size + 1);                    //--- Resize
               wrappedLines[size] = currentLine;                       //--- Add line
               ArrayResize(wrappedColors, size + 1);                   //--- Resize colors
               wrappedColors[size] = paraColor;                        //--- Set color
              }
            currentLine = words[w];                                    //--- Start new line
           }
        }
      if (StringLen(currentLine) > 0)
        {
         int size = ArraySize(wrappedLines);                           //--- Get size
         ArrayResize(wrappedLines, size + 1);                          //--- Resize
         wrappedLines[size] = currentLine;                             //--- Add last line
         ArrayResize(wrappedColors, size + 1);                         //--- Resize colors
         wrappedColors[size] = paraColor;                              //--- Set color
        }
     }
  }

We define a couple of helper functions. First, we implement the "LightenColor" function to brighten a color by multiplying its RGB components with a factor greater than one, clamping to 255 to avoid overflow, for use in theme adjustments like text colors in dark mode. It takes color "colorValue" and a double factor, extracts blue/green/red with mask/shifts, multiplies each by the factor cast to int with MathMin 255, and combines with bit shifts/OR cast to color.

Next, we create the "TextCalculateSliderHeight" function to compute the scrollbar slider size proportional to visible content over the total, ensuring a minimum for usability. It calculates area height as "TextPanelHeight" minus borders minus double "text_button_size", sets min twenty, returns full area if no scroll, else floors area times visible ratio, returns max of min and value. We then define "TextScrollUp" to scroll text upward by decrementing "text_scroll_pos" one "text_adjustedLineHeight" if positive and greater than zero, clamped to zero, calls "UpdateTextOnCanvas" to redraw. Similarly, "TextScrollDown" scrolls downward by incrementing "text_scroll_pos" one line height if less than "text_max_scroll", clamped to max, updates canvas.

The "TextUpdateHoverEffects" function detects hovers over scrollbar parts based on local coordinates, for visual feedback. If not visible, exits. Computes scrollbar positions/dimensions, checks "text_scroll_up_hovered" if area hovered and in top button bounds, "text_scroll_down_hovered" for bottom, "text_scroll_slider_hovered" for current slider position range, all conditional on "text_scroll_area_hovered". We implement "GetLineColor" to categorize line colors by content for styling: gray for empty/space, red for email, purple/blue for specific links/channel/http/t.me, orange for numbered lists, not certain phrases, default white. "IsHeading" identifies headings: false for empty, true if ends with colon or matches phrases like guide/features/instructions/notes/contacts/NB. Actually, for the identifications here, we could choose to have HTML characters like format, but for simplicity, we added the ones we want to recognize in the function. There are many ideas, so follow the one that suits you.

Finally, "WrapText" breaks input into width-constrained lines with associated colors. Resets output arrays, splits by newline to paragraphs, for each gets color, adds space/gray if empty. Splits paragraphs to words, builds/tests lines: if test fits measured width (temp font set), append; else add current with color, start new. Adds final line if present. With these helper functions, we can now render the main canvas using the following logic.

//+------------------------------------------------------------------+
//| Update text on canvas                                            |
//+------------------------------------------------------------------+
void UpdateTextOnCanvas()
  {
   canvasText.Erase(0);                //--- Clear text canvas
   
   int textWidth = canvasText.Width(); //--- Get text width
   int textHeight = TextPanelHeight;   //--- Get text height
   
   color text_bg = is_dark_theme ? text_bg_dark : text_bg_light;                  //--- Get bg color
   uint argb_bg = ColorToARGB(text_bg, (uchar)(255 * (TextBackgroundOpacityPercent / 100.0))); //--- Convert to ARGB
   canvasText.FillRectangle(0, 0, textWidth - 1, textHeight - 1, argb_bg);        //--- Fill background
   
   uint argbBorder = ColorToARGB(GetBorderColor(), 255);                          //--- Convert border
   canvasText.Line(0, 0, textWidth - 1, 0, argbBorder);                           //--- Draw top
   canvasText.Line(textWidth - 1, 0, textWidth - 1, textHeight - 1, argbBorder);  //--- Draw right
   canvasText.Line(textWidth - 1, textHeight - 1, 0, textHeight - 1, argbBorder); //--- Draw bottom
   canvasText.Line(0, textHeight - 1, 0, 0, argbBorder); //--- Draw left
   
   int padding = 10;                   //--- Set padding
   int textAreaX = 1 + padding;        //--- Calculate area X
   int textAreaY = 1;                  //--- Set area Y
   int textAreaWidth = textWidth - 2 - padding * 2; //--- Calculate area width
   int textAreaHeight = textHeight - 2; //--- Calculate area height
   string font = "Arial";              //--- Set font
   int fontSize = 16;                  //--- Set font size
   canvasText.FontSet(font, fontSize); //--- Set font
   int lineHeight = canvasText.TextHeight("A"); //--- Get line height
   text_adjustedLineHeight = lineHeight + 3; //--- Adjust line height
   
   text_visible_height = textAreaHeight; //--- Set visible height
   static string wrappedLines[];       //--- Declare wrapped lines
   static color wrappedColors[];       //--- Declare wrapped colors
   static bool wrapped = false;        //--- Initialize wrapped flag
   bool need_scroll = false;           //--- Initialize scroll need
   int reserved_width = 0;             //--- Initialize reserved width
   if (!wrapped)
     {
      WrapText(text_usage_text, font, fontSize, textAreaWidth, wrappedLines, wrappedColors); //--- Wrap text
      wrapped = true;                  //--- Set wrapped flag
     }
   int numLines = ArraySize(wrappedLines);                 //--- Get number of lines
   text_total_height = numLines * text_adjustedLineHeight; //--- Calculate total height
   need_scroll = text_total_height > text_visible_height;  //--- Check if scroll needed
   if (need_scroll)
     {
      reserved_width = text_track_width; //--- Set reserved width
      textAreaWidth -= reserved_width;   //--- Adjust area width
      WrapText(text_usage_text, font, fontSize, textAreaWidth, wrappedLines, wrappedColors); //--- Rewrap text
      numLines = ArraySize(wrappedLines); //--- Update num lines
      text_total_height = numLines * text_adjustedLineHeight;               //--- Update total height
     }
   text_max_scroll = MathMax(0, text_total_height - text_visible_height);   //--- Calculate max scroll
   text_scroll_visible = need_scroll;                                       //--- Set scroll visible
   text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Adjust scroll pos
   if (text_scroll_visible)
     {
      int scrollbar_y = 1;                //--- Set scrollbar Y
      int scrollbar_height = textHeight - 2; //--- Calculate scrollbar height
      int scroll_area_height = scrollbar_height - 2 * text_button_size;      //--- Calculate area height
      text_slider_height = TextCalculateSliderHeight();                      //--- Calculate slider height
      int scrollbar_x = textWidth - text_track_width - 1;                    //--- Calculate scrollbar X
      color leader_color = is_dark_theme ? text_leader_color_dark : text_leader_color_light; //--- Get leader color
      uint argb_leader = ColorToARGB(leader_color, 255);                     //--- Convert to ARGB
      canvasText.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + text_track_width - 1, scrollbar_y + scrollbar_height - 1, argb_leader); //--- Fill leader
      
      int slider_y = scrollbar_y + text_button_size + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y
      
      if (text_scroll_area_hovered)
        {
         color button_bg = is_dark_theme ? text_button_bg_dark : text_button_bg_light;                   //--- Get button bg
         color button_bg_hover = is_dark_theme ? text_button_bg_hover_dark : text_button_bg_hover_light; //--- Get hover bg
         color up_bg = text_scroll_up_hovered ? button_bg_hover : button_bg;                             //--- Determine up bg
         uint argb_up_bg = ColorToARGB(up_bg, 255);                                                      //--- Convert to ARGB
         canvasText.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + text_track_width - 1, scrollbar_y + text_button_size - 1, argb_up_bg); //--- Fill up button
         color arrow_color = is_dark_theme ? text_arrow_color_dark : text_arrow_color_light;             //--- Get arrow color
         color arrow_color_disabled = is_dark_theme ? text_arrow_color_disabled_dark : text_arrow_color_disabled_light; //--- Get disabled arrow
         color arrow_color_hover = is_dark_theme ? text_arrow_color_hover_dark : text_arrow_color_hover_light; //--- Get hover arrow
         color up_arrow = (text_scroll_pos == 0) ? arrow_color_disabled : (text_scroll_up_hovered ? arrow_color_hover : arrow_color); //--- Determine up arrow color
         uint argb_up_arrow = ColorToARGB(up_arrow, 255);                                                //--- Convert to ARGB
         canvasText.FontSet("Webdings", 22);                                                             //--- Set font
         int arrow_x = scrollbar_x + text_track_width / 2;                                               //--- Calculate arrow X
         int arrow_y = scrollbar_y + (text_button_size / 2) - (canvasText.TextHeight(CharToString(0x35)) / 2); //--- Calculate arrow Y
         canvasText.TextOut(arrow_x, arrow_y, CharToString(0x35), argb_up_arrow, TA_CENTER);             //--- Draw up arrow
         
         int down_y = scrollbar_y + scrollbar_height - text_button_size;                                 //--- Calculate down Y
         color down_bg = text_scroll_down_hovered ? button_bg_hover : button_bg;                         //--- Determine down bg
         uint argb_down_bg = ColorToARGB(down_bg, 255);                                                  //--- Convert to ARGB
         canvasText.FillRectangle(scrollbar_x, down_y, scrollbar_x + text_track_width - 1, down_y + text_button_size - 1, argb_down_bg); //--- Fill down button
         color down_arrow = (text_scroll_pos >= text_max_scroll) ? arrow_color_disabled : (text_scroll_down_hovered ? arrow_color_hover : arrow_color); //--- Determine down arrow
         uint argb_down_arrow = ColorToARGB(down_arrow, 255);                                            //--- Convert to ARGB
         int down_arrow_x = scrollbar_x + text_track_width / 2;                                          //--- Calculate down arrow X
         int down_arrow_y = down_y + (text_button_size / 2) - (canvasText.TextHeight(CharToString(0x36)) / 2); //--- Calculate down arrow Y
         canvasText.TextOut(down_arrow_x, down_arrow_y, CharToString(0x36), argb_down_arrow, TA_CENTER); //--- Draw down arrow
         
         int slider_x = scrollbar_x + text_scrollbar_margin;                                             //--- Calculate slider X
         int slider_w = text_track_width - 2 * text_scrollbar_margin;                                    //--- Calculate slider width
         int cap_radius = slider_w / 2;                                                                  //--- Calculate cap radius
         color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light;             //--- Get slider bg
         color slider_bg_hover_color = is_dark_theme ? text_slider_bg_hover_dark : text_slider_bg_hover_light; //--- Get hover bg
         color slider_bg = text_scroll_slider_hovered || text_movingStateSlider ? slider_bg_hover_color : slider_bg_color; //--- Determine bg
         uint argb_slider = ColorToARGB(slider_bg, 255);                                                 //--- Convert to ARGB

         canvasText.Arc(slider_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc
         canvasText.FillCircle(slider_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider);   //--- Fill top circle

         canvasText.Arc(slider_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc
         canvasText.FillCircle(slider_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, argb_slider); //--- Fill bottom circle

         canvasText.FillRectangle(slider_x, slider_y + cap_radius, slider_x + slider_w, slider_y + text_slider_height - cap_radius, argb_slider); //--- Fill middle
        }
      else
        {
         int thin_w = text_scrollbar_thin_width;                                                         //--- Set thin width
         int thin_x = scrollbar_x + (text_track_width - thin_w) / 2;                                     //--- Calculate thin X
         int cap_radius = thin_w / 2;                                                                    //--- Calculate cap radius
         color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light;             //--- Get bg
         uint argb_slider = ColorToARGB(slider_bg_color, 255);                                           //--- Convert to ARGB

         canvasText.Arc(thin_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top
         canvasText.FillCircle(thin_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider);     //--- Fill top

         canvasText.Arc(thin_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom
         canvasText.FillCircle(thin_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, argb_slider); //--- Fill bottom

         canvasText.FillRectangle(thin_x, slider_y + cap_radius, thin_x + thin_w, slider_y + text_slider_height - cap_radius, argb_slider); //--- Fill middle
        }
     }
   
   color text_base = is_dark_theme ? text_base_dark : text_base_light;                                   //--- Get base color
   for (int line = 0; line < numLines; line++)
     {
      string lineText = wrappedLines[line];   //--- Get line text
      if (StringLen(lineText) == 0) continue; //--- Skip empty
      color lineColor = wrappedColors[line];  //--- Get line color
      if (is_dark_theme) lineColor = (lineColor == clrWhite) ? clrWhite : LightenColor(lineColor, 1.5);  //--- Adjust for dark
      else lineColor = (lineColor == clrWhite) ? clrBlack : DarkenColor(lineColor, 0.7);                 //--- Adjust for light
      int line_y = textAreaY + line * text_adjustedLineHeight - text_scroll_pos;                         //--- Calculate line Y
      if (line_y + text_adjustedLineHeight < 0 || line_y > textAreaHeight) continue;                     //--- Skip out of view
      if (IsHeading(lineText))
        {
         canvasText.FontSet("Arial Bold", fontSize); //--- Set bold font
         lineColor = clrDodgerBlue;                  //--- Set heading color
        }
      else canvasText.FontSet(font, fontSize);       //--- Set regular font
      uint argbText = ColorToARGB(lineColor, 255);   //--- Convert to ARGB
      canvasText.TextOut(textAreaX, line_y, lineText, argbText, TA_LEFT); //--- Draw line
     }
   
   canvasText.Update();                       //--- Update text canvas
  }

Here, we implement the "UpdateTextOnCanvas" function to render the scrollable text content on the "canvasText" object, handling backgrounds, borders, dynamic wrapping, themed text with color adjustments, and a custom scrollbar with hover effects for navigation. We clear the canvas with Erase set to zero, get current width/height, determine themed background color (dark or light), convert to ARGB with opacity from "TextBackgroundOpacityPercent" over 100.0 times 255 cast to uchar, and fill the rectangle from (0,0) to width/height minus one using the FillRectangle method. We convert border color from "GetBorderColor" to ARGB at 255 and draw top/right/bottom/left lines with Line.

We set padding at ten, compute text area as x one plus padding, y one, width as total minus borders minus double padding, height minus borders. Set font to Arial at sixteen with FontSet, get line height from A character with TextHeight, adjust "text_adjustedLineHeight" by adding three for spacing. We set "text_visible_height" to the area height. If not wrapped (static "wrapped" false), call "WrapText" with usage text, font/size/area width to populate static "wrappedLines" and "wrappedColors", and set "wrapped" true. Get number of lines from array size, compute "text_total_height" as lines times adjusted height. Check if scroll needed if total exceeds visible, if so set "reserved_width" to "text_track_width", reduce area width, rewrap text, update number of lines/total height.

We calculate "text_max_scroll" as max zero or total minus visible, set "text_scroll_visible" to need_scroll, clamp "text_scroll_pos" between zero and max. If visible, compute scrollbar positions: y at one, height as text minus borders, area height minus double "text_button_size", "text_slider_height" from "TextCalculateSliderHeight", x as width minus track minus one. Get themed leader color, convert to ARGB, fill leaderboard rectangle. Compute slider y based on position ratio times (area minus slider). If "text_scroll_area_hovered", get themed button bg/hover, determine up bg if "text_scroll_up_hovered", convert to ARGB, fill up button rectangle. Get arrow colors normal/disabled/hover, determine up arrow color disabled if position is zero else hover/normal, convert, set Webdings font at twenty-two, compute arrow x/y centered, draw up arrow '0x35' with TextOut center aligned. Similarly, for the down button at the bottom y, bg/hover, fill, down arrow color disabled if position is at max, else hover/normal, draw down '0x36' centered.

For the slider, compute x as x plus margin, w as track minus double margin, cap radius half w, get themed slider bg/hover, determine bg if hovered or moving, convert to ARGB. Draw/fill top/bottom arcs/circles with "Arc"/FillCircle using radians from "DegreesToRadians", fill the middle rectangle. Else if not hovered, draw a thin slider at the center x with a thin width, a cap radius of half, themed bg ARGB, draw/fill top/bottom arcs/circles, fill thin middle.

We get themed base color, loop over "wrappedLines": skip empty, get/adjust color (if white/black keep, else lighten 1.5 in dark or darken 0.7 in light), compute line y as area y plus line times adjusted minus "text_scroll_pos", skip if out of view (negative or beyond area). If "IsHeading", set bold font and dodger blue color; else regular font. Convert to ARGB, draw left-aligned at area x and line y. Finally, update the canvas with Update to display the text. We call the function in the initialization after defining its canvas area as follows.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Other panels logic
   
   if (EnableTextPanel)
     {
      int textY = currentCanvasY + header_height + gap_y + currentHeight + PanelGap; //--- Calculate text Y
      if (!canvasText.CreateBitmapLabel(0, 0, canvasTextName, currentCanvasX, textY, header_width, TextPanelHeight, COLOR_FORMAT_ARGB_NORMALIZE))
        {
         Print("Failed to create Text Canvas");       //--- Log text creation failure
        }
      textCreated = true;                             //--- Set text created flag
     }
   
   if (EnableTextPanel) UpdateTextOnCanvas();         //--- Update text if enabled
   
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true); //--- Enable mouse wheel events
   ChartRedraw();                                     //--- Redraw chart
   return(INIT_SUCCEEDED);                            //--- Return success
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if (textCreated) canvasText.Destroy();             //--- Destroy text if created
   ChartRedraw();                                     //--- Redraw chart
  }


//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
   static datetime lastBarTime = 0;                   //--- Initialize last time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current time
   if (currentBarTime > lastBarTime)
     {
      UpdateGraphOnCanvas();                         //--- Update graph
      if (EnableStatsPanel) UpdateStatsOnCanvas();   //--- Update stats
      if (EnableTextPanel) UpdateTextOnCanvas();     //--- Update text
      ChartRedraw();                                 //--- Redraw chart
      lastBarTime = currentBarTime;                  //--- Update last time
     }
  }

First, we modify the OnInit event handler to incorporate creation of the text panel if "EnableTextPanel" is true. After other panels' logic, we calculate its y position as "currentCanvasY" plus "header_height", "gap_y", "currentHeight", and "PanelGap", then create the bitmap label with CreateBitmapLabel at subwindow zero, name "canvasTextName", position, header width, "TextPanelHeight", and COLOR_FORMAT_ARGB_NORMALIZE, printing an error if failed, setting "textCreated" true on success. We call "UpdateTextOnCanvas" if enabled to initially render the text. We enable mouse wheel events with ChartSetInteger using CHART_EVENT_MOUSE_WHEEL true, which we will need to identify the chart wheel move for dynamic scrolling, redraw with ChartRedraw, and return INIT_SUCCEEDED.

Then, we update the "OnDeinit" event handler to conditionally destroy the text canvas if "textCreated" is true using "canvasText.Destroy", then redraw. We adjust the OnTick event handler to include text panel updates on new bars. Using static "lastBarTime" at zero, get current bar time with iTime, if greater, call "UpdateGraphOnCanvas", optional "UpdateStatsOnCanvas" if "EnableStatsPanel", "UpdateTextOnCanvas" if "EnableTextPanel", redraw, update last bar time. Upon compilation, we get the following outcome.

RENDERED TEXT PANEL

From the image, we can see that the text panel is now rendered. What we need to do now is update the chart event handler to handle chart events for scrolling the text that we have rendered. Here is the logic we used to achieve that. We have added only the changes that we took to achieve the final outcome and eliminated the other logic that we implemented in the previous versions.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if (id == CHARTEVENT_CHART_CHANGE)
     {
      //--- Existing logic
      
      if (EnableTextPanel) UpdateTextOnCanvas(); //--- Update text
      ChartRedraw();                             //--- Redraw chart
     }
   else if (id == CHARTEVENT_MOUSE_MOVE)
     {
      //--- Existing logic
      
      if (EnableTextPanel && !panels_minimized)
        {
         int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X
         int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y
         int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE);     //--- Get width
         int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE);     //--- Get height
         bool is_over_text = (mouse_x >= text_canvas_x && mouse_x <= text_canvas_x + text_canvas_w &&
                              mouse_y >= text_canvas_y && mouse_y <= text_canvas_y + text_canvas_h); //--- Check over text
         bool prev_scroll_hovered = text_scroll_area_hovered;                             //--- Store prev scroll hover
         text_scroll_area_hovered = false;         //--- Reset area hover
         if (is_over_text)
           {
            int local_x = mouse_x - text_canvas_x; //--- Get local X
            int local_y = mouse_y - text_canvas_y; //--- Get local Y
            if (local_x >= text_canvas_w - text_track_width - 1)
              {
               text_scroll_area_hovered = true;    //--- Set area hover
              }
            bool prev_up = text_scroll_up_hovered; //--- Store prev up
            bool prev_down = text_scroll_down_hovered;     //--- Store prev down
            bool prev_slider = text_scroll_slider_hovered; //--- Store prev slider
            TextUpdateHoverEffects(local_x, local_y);      //--- Update hovers
            if (prev_scroll_hovered != text_scroll_area_hovered || prev_up != text_scroll_up_hovered || prev_down != text_scroll_down_hovered || prev_slider != text_scroll_slider_hovered)
              {
               UpdateTextOnCanvas();               //--- Update text
               ChartRedraw();                      //--- Redraw chart
              }
            text_mouse_in_body = (local_x < text_canvas_w - text_track_width - 1); //--- Set body flag
           }
         else
           {
            bool need_redraw = prev_scroll_hovered || text_scroll_up_hovered || text_scroll_down_hovered || text_scroll_slider_hovered; //--- Check redraw need
            if (need_redraw)
              {
               text_scroll_area_hovered = false;   //--- Reset area
               text_scroll_up_hovered = false;     //--- Reset up
               text_scroll_down_hovered = false;   //--- Reset down
               text_scroll_slider_hovered = false; //--- Reset slider
               UpdateTextOnCanvas();               //--- Update text
               ChartRedraw();                      //--- Redraw chart
              }
            text_mouse_in_body = false;            //--- Reset body flag
           }
         // New: Toggle CHART_MOUSE_SCROLL on enter/leave text body
         if (text_mouse_in_body != prev_text_mouse_in_body)
           {
            ChartSetInteger(0, CHART_MOUSE_SCROLL, !text_mouse_in_body); //--- Toggle scroll
            prev_text_mouse_in_body = text_mouse_in_body;                //--- Update prev
           }
        }
      
      if (mouse_state == 1 && prev_mouse_state == 0)
        {
         
         //--- Existing logic
         
         if (EnableTextPanel && !panels_minimized && text_scroll_visible && text_scroll_area_hovered)
           {
            int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X
            int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y
            int local_x = mouse_x - text_canvas_x;  //--- Get local X
            int local_y = mouse_y - text_canvas_y;  //--- Get local Y
            int scrollbar_x = canvasText.Width() - text_track_width - 1;                     //--- Get scrollbar X
            int scrollbar_y = 1;                    //--- Set scrollbar Y
            int scrollbar_height = TextPanelHeight - 2;                                      //--- Get height
            int scroll_area_y = scrollbar_y + text_button_size;                              //--- Calculate area Y
            int scroll_area_height = scrollbar_height - 2 * text_button_size;                //--- Calculate area height
            int slider_y = scroll_area_y + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y
            if (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1)
              {
               if (local_y >= scrollbar_y && local_y <= scrollbar_y + text_button_size - 1)
                 {
                  TextScrollUp();                   //--- Scroll up
                 }
               else if (local_y >= scrollbar_y + scrollbar_height - text_button_size && local_y <= scrollbar_y + scrollbar_height - 1)
                 {
                  TextScrollDown();                 //--- Scroll down
                 }
               else if (local_y >= scroll_area_y && local_y <= scroll_area_y + scroll_area_height - 1)
                 {
                  if (local_y >= slider_y && local_y <= slider_y + text_slider_height - 1)
                    {
                     text_movingStateSlider = true;  //--- Set moving slider
                     text_mlbDownY_Slider = local_y; //--- Set down Y
                     text_mlbDown_YD_Slider = slider_y;                                      //--- Set down YD
                     ChartSetInteger(0, CHART_MOUSE_SCROLL, false);                          //--- Disable scroll
                    }
                  else
                    {
                     int new_slider_y = local_y - text_slider_height / 2;                    //--- Calculate new Y
                     new_slider_y = MathMax(scroll_area_y, MathMin(new_slider_y, scroll_area_y + scroll_area_height - text_slider_height)); //--- Clamp Y
                     double ratio = (double)(new_slider_y - scroll_area_y) / (scroll_area_height - text_slider_height); //--- Calculate ratio
                     text_scroll_pos = (int)MathRound(ratio * text_max_scroll);              //--- Update pos
                    }
                  UpdateTextOnCanvas();             //--- Update text
                  ChartRedraw();                    //--- Redraw chart
                 }
              }
           }
        }

      else if (text_movingStateSlider && mouse_state == 1)
        {
         int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE);    //--- Get text Y
         int local_y = mouse_y - text_canvas_y;                                              //--- Get local Y
         int delta_y = local_y - text_mlbDownY_Slider;                                       //--- Calculate delta Y
         int new_slider_y = text_mlbDown_YD_Slider + delta_y;                                //--- Calculate new slider Y
         int scrollbar_y = 1;                                                                //--- Set scrollbar Y
         int scrollbar_height = TextPanelHeight - 2;                                         //--- Get height
         int slider_min_y = scrollbar_y + text_button_size;                                  //--- Calculate min Y
         int slider_max_y = scrollbar_y + scrollbar_height - text_button_size - text_slider_height;  //--- Calculate max Y
         new_slider_y = MathMax(slider_min_y, MathMin(new_slider_y, slider_max_y));                   //--- Clamp Y
         double scroll_ratio = (double)(new_slider_y - slider_min_y) / (slider_max_y - slider_min_y); //--- Calculate ratio
         int new_scroll_pos = (int)MathRound(scroll_ratio * text_max_scroll);                //--- Calculate new pos
         if (new_scroll_pos != text_scroll_pos)
           {
            text_scroll_pos = new_scroll_pos; //--- Update pos
            UpdateTextOnCanvas();             //--- Update text
            ChartRedraw();                    //--- Redraw chart
           }
        }
      else if (mouse_state == 0 && prev_mouse_state == 1)
        {
         //--- Existing logic
         
         if (text_movingStateSlider)
           {
            text_movingStateSlider = false;  //--- Reset slider moving
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll
            UpdateTextOnCanvas();            //--- Update text
            ChartRedraw();                   //--- Redraw chart
           }
        }
      
      last_mouse_x = mouse_x;               //--- Update last X
      last_mouse_y = mouse_y;               //--- Update last Y
      prev_mouse_state = mouse_state;       //--- Update prev state
     }
   else if (id == CHARTEVENT_MOUSE_WHEEL)
     {
      int flg_keys = (int)(lparam >> 32);   //--- Get keys
      int mx = (int)(short)lparam;          //--- Get X
      int my = (int)(short)(lparam >> 16);  //--- Get Y
      int delta = (int)dparam;              //--- Get delta
      
      if (EnableTextPanel && !panels_minimized && text_scroll_visible)
        {
         int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE);       //--- Get text X
         int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE);       //--- Get text Y
         int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE);           //--- Get width
         int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE);           //--- Get height
         bool is_over_text_body = (mx >= text_canvas_x && mx <= text_canvas_x + text_canvas_w - text_track_width &&
                                   my >= text_canvas_y && my <= text_canvas_y + text_canvas_h); //--- Check over body
         if (is_over_text_body)
           {
            int scroll_step = 20;            //--- Set step
            text_scroll_pos += (delta > 0 ? -scroll_step : scroll_step);                        //--- Adjust pos
            text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll));            //--- Clamp pos
            UpdateTextOnCanvas();            //--- Update text
            
            int current_scale = (int)ChartGetInteger(0, CHART_SCALE);                           //--- Get scale
            int adjust = (delta > 0 ? 1 : -1); // Swap to (delta > 0 ? -1 : 1) if wheel direction is opposite
            int revert_scale = current_scale + adjust;                                          //--- Calculate revert
            revert_scale = MathMax(0, MathMin(5, revert_scale));                                //--- Clamp scale
            ChartSetInteger(0, CHART_SCALE, revert_scale);                                      //--- Set scale
            
            ChartRedraw();                    //--- Redraw chart
           }
        }
     }
  }

Here, we modify the OnChartEvent event handler to incorporate text panel interactivity, extending mouse move handling if "EnableTextPanel" is true and not "panels_minimized". We retrieve text canvas position/width/height with ObjectGetInteger for OBJPROP_XDISTANCE/"OBJPROP_YDISTANCE"/"OBJPROP_XSIZE"/"OBJPROP_YSIZE", check if the mouse is over the entire text area, store the previous "text_scroll_area_hovered", and reset it to false. If over, compute local x/y from mouse minus canvas position, if local x in scrollbar range (width minus "text_track_width" minus one to end), set area hovered true.

Store previous up/down/slider hovers, call "TextUpdateHoverEffects" with local coords to refresh them, if any hover or area changed from previous, call "UpdateTextOnCanvas" and redraw. Set "text_mouse_in_body" true if local x is left of the scrollbar. Else if not over text, but previous hovers indicate need redraw, reset area/up/down/slider hovers to false, update text/redraw. If "text_mouse_in_body" changed from "prev_text_mouse_in_body", toggle chart mouse scroll with ChartSetInteger "CHART_MOUSE_SCROLL" to false in body (true out) to allow wheel scrolling without chart zoom, update previous.

For mouse down (state one, previous zero), if text/scroll/area hovered, get local x/y/scrollbar x/y/height/area y/height/slider y as before, if local x in scrollbar: if local y in up button call "TextScrollUp", in down "TextScrollDown", else if in area: if in slider set "text_movingStateSlider" true, store "text_mlbDownY_Slider" as local y, "text_mlbDown_YD_Slider" as slider y, disable scroll; else compute new slider y as local y minus half "text_slider_height" clamped to area y to area y plus height minus "text_slider_height", ratio as (new minus area y) over (height minus "text_slider_height"), set "text_scroll_pos" to rounded ratio times "text_max_scroll"; then update text/redraw.

If "text_movingStateSlider" true and state one (held), get local y from mouse minus text y, delta as local minus "text_mlbDownY_Slider", new slider y as "text_mlbDown_YD_Slider" plus delta, clamp to slider min y (y plus "text_button_size") to max y (y plus height minus "text_button_size" minus "text_slider_height"), ratio as (new minus min) over (max minus min), new position as rounded ratio times "text_max_scroll", if differs from current update "text_scroll_pos", text, redraw.

For mouse up (state zero, previous one), if "text_movingStateSlider" is set to false, enable scroll true, update text, and redraw. We add to the chart change event an optional call to "UpdateTextOnCanvas" if "EnableTextPanel" is true before redraw. For mouse wheel event, cast "flg_keys" from "lparam" shift 32, mx from short "lparam", my from short "lparam" shift 16, delta from "dparam" as integers. If text/scroll visible, get text props, check if over body (mx/my in x to x plus width minus "text_track_width", y to y plus height), if so step "text_scroll_pos" by twenty negative/positive on delta >0/<0, clamp zero to "text_max_scroll", update text, get current chart scale with ChartGetInteger "CHART_SCALE", adjust +1/-1 on delta >0/<0 clamped 0-5, set back with "ChartSetInteger" to revert zoom, redraw. Upon compilation, we get the following outcome.

TEXT CANVAS TEST

From the visualization, we can see that we have enhanced the canvas-based dashboard by adding the text panel with all the interactions done, hence achieving our objectives. What now remains is testing the workability of the system, and that is handled in the preceding section.


Backtesting

We did the testing, and below is the compiled visualization in a single Graphics Interchange Format (GIF) bitmap image format.

COMPLETE CANVAS DASHBOARD BACKTEST


Conclusion

In conclusion, we’ve enhanced the canvas-based price dashboard in MQL5 by implementing a pixel-perfect scrollable text canvas for usage guides, bypassing native limitations with antialiasing for smooth rendering, a rounded custom scrollbar that expands on hover with buttons and a slider for navigation, themed elements, dynamic wrapping with color adjustments, and wheel/click/drag support for seamless interaction. The system integrates the text panel with existing graph/stats, maintaining drag/resize/theme/minimize functionalities, and ensures efficient event-driven updates for a comprehensive monitoring tool. With this pixel-perfect scrollable text canvas enhancement, you’re equipped to provide detailed user guidance within the dashboard, ready for further optimization in your trading journey. Happy trading!

Attached files |
Features of Custom Indicators Creation Features of Custom Indicators Creation
Creation of Custom Indicators in the MetaTrader trading system has a number of features.
Larry Williams Market Secrets (Part 8): Combining Volatility, Structure and Time Filters Larry Williams Market Secrets (Part 8): Combining Volatility, Structure and Time Filters
An in-depth walkthrough of building a Larry Williams inspired volatility breakout Expert Advisor in MQL5, combining swing structure, volatility-based entries, trade day of the week filtering, time filters, and flexible risk management, with a complete implementation and reproducible test setup.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Creating Custom Indicators in MQL5 (Part 6): Evolving RSI Calculations with Smoothing, Hue Shifts, and Multi-Timeframe Support Creating Custom Indicators in MQL5 (Part 6): Evolving RSI Calculations with Smoothing, Hue Shifts, and Multi-Timeframe Support
In this article, we build a versatile RSI indicator in MQL5 supporting multiple variants, data sources, and smoothing methods for improved analysis. We add hue shifts for color visuals, dynamic boundaries for overbought/oversold zones, and notifications for trend alerts. It includes multi-timeframe support with interpolation, offering us a customizable RSI tool for diverse strategies.