DearMQL5/UI.mqh

477 lines
18 KiB
MQL5

feat: initial release of DearMQL5 v1.0.0 - Immediate-Mode GUI Engine for MetaTrader 5 DearMQL5 is an institutional-grade, immediate-mode graphical user interface (GUI) engine designed from the ground up for MetaTrader 5 (MQL5), inspired by the ergonomics and performance of Dear ImGui. ### Core Architecture & Execution Pipeline - Immediate-Mode Paradigm: Declarative frame-based rendering pipeline with zero memory allocation churn during render loops and deterministic execution. - Modular Per-Hub Canvas Surfaces (CUWindowSurface): Replaces full-screen canvas overlays with isolated, window-bounded CCanvas bitmaps, reducing memory usage by up to 93% (~570 KB vs 8.3 MB for 1080p) and preventing click-swallowing. - 0-Cost Hardware Texture Drag Engine: Absolute-anchor drag mechanics update native MT5 OBJPROP_XDISTANCE and OBJPROP_YDISTANCE properties directly on the compositor, bypassing CPU rasterization completely (0.00% CPU usage). - Chart Scroll Protection: Automatically suppresses chart history sliding (CHART_MOUSE_SCROLL) during active HUD dragging and safely restores it on release. - Two-Phase Hit-Testing: Interactive controls (buttons, sliders, toggles) receive first-priority click focus, cleanly separating widget actions from window drags. ### Subsystems & Components - Core State & Input (Core/): * UInputState: Mouse/keyboard tracking, deltas, and chart scroll lock. * UState: Retained state hash map in pure MQL5 with 64-bit FNV-1a hashing. * UIdStack: Hierarchical identifier scoping preventing widget ID collisions. * ULayout: Automatic cursor placement supporting SameLine, Indent, and Unindent. * UEventQueue: Non-blocking circular UI event queue for decoupled handling. * UTheme: Institutional OneDark/Nord dark color palette and typography metrics. - Rendering Backends (Render/): * URendererCanvas: Isolated per-hub surfaces with 32-bit ARGB alpha transparency. * URendererNative: Native MT5 object pool (OBJ_RECTANGLE_LABEL) with diff caching. * UFont: Anti-aliased font metrics and width cache. - Complete Widget Suite (Widgets/): * UButton: Click-on-release standard, accent, and emergency panic buttons. * UToggle: Smooth draggable/clickable iOS-style toggle switches. * USlider: Draggable horizontal float and integer sliders. * UProgressBar: Adaptive progress bars with dynamic green->amber->red color ramps. * UHeader: Collapsible section headers with immediate-mode state retention. * ULabel & USeparator: Formatted text, colored metrics, and horizontal dividers. - Window Management (Windows/): * UWindow: Window container with titlebar, collapse toggle, and coordinate clipping. * UWindowManager: Multi-window coordinator with GlobalVariable persistence across chart timeframe changes and backward-compatible key resolution. ### Tooling, Documentation & Reference Implementation - Examples/EA_DearMQL5_Demo.mq5: Complete reference Prop-Firm cockpit HUD. - README.md: Comprehensive institutional English documentation for GitHub and Algo Forge. - Docs/ARCHITECTURE.md: Technical architecture specification with Mermaid lifecycle.
2026-10-12 08:18:03 -03:00
//+------------------------------------------------------------------+
//| UI.mqh |
//| Copyright 2026, DearMQL5 GUI Engine |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, DearMQL5 GUI Engine"
#property link "https://www.mql5.com"
#property strict
// Backend selection: Default is high-performance CCanvas (isolated Per-Hub OBJ_BITMAP_LABEL)
// Define UI_BACKEND_NATIVE prior to including UI.mqh if native MT5 object pool is preferred
#ifndef UI_BACKEND_NATIVE
#define UI_BACKEND_CANVAS
#endif
// Core Subsystems
#include "Util\UMath.mqh"
#include "Util\UDiagnostics.mqh"
#include "Core\UTheme.mqh"
#include "Core\UIdStack.mqh"
#include "Core\UState.mqh"
#include "Core\UInputState.mqh"
#include "Core\UEventQueue.mqh"
#include "Core\ULayout.mqh"
#include "Render\UFont.mqh"
#ifdef UI_BACKEND_CANVAS
#include "Render\URendererCanvas.mqh"
#else
#include "Render\URendererNative.mqh"
#endif
// Widgets & Windows
#include "Widgets\UWidget.mqh"
#include "Widgets\ULabel.mqh"
#include "Widgets\USeparator.mqh"
#include "Widgets\UButton.mqh"
#include "Widgets\UToggle.mqh"
#include "Widgets\USlider.mqh"
#include "Widgets\UProgressBar.mqh"
#include "Widgets\UHeader.mqh"
#include "Windows\UWindow.mqh"
#include "Windows\UWindowManager.mqh"
/// Primary public facade for DearMQL5 immediate-mode GUI operations in MQL5.
class UI
{
public:
//+------------------------------------------------------------------+
//| Lifecycle & System Control |
//+------------------------------------------------------------------+
/// Initializes the GUI engine on the target chart and subwindow.
static bool Init(const long chart_id = 0, const int subwindow = 0)
{
UInputState::SetChartId((chart_id == 0) ? ChartID() : chart_id);
#ifdef UI_BACKEND_CANVAS
return(URendererCanvas::Init(chart_id, subwindow));
#else
URendererNative::Init(chart_id, subwindow);
return(true);
#endif
}
/// Shuts down the GUI engine and cleans up all graphics resources.
static void Shutdown(void)
{
UInputState::EnableChartScroll();
#ifdef UI_BACKEND_CANVAS
URendererCanvas::Shutdown();
#else
URendererNative::Shutdown();
#endif
}
/// Feeds OnChartEvent events directly into DearMQL5 input pipeline.
static void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(id == CHARTEVENT_MOUSE_MOVE)
{
UInputState::OnMouseMove((int)lparam, (int)dparam, (uint)StringToInteger(sparam));
}
}
/// Begins a new GUI frame. Must be called before any windows or widgets.
static void NewFrame(void)
{
UDiagnostics::BeginFrame();
UIdStack::Reset();
UWindow::ResetScope();
UInputState::NewFrame();
UState::NextFrame();
#ifdef UI_BACKEND_CANVAS
URendererCanvas::BeginFrame();
#else
URendererNative::BeginFrame();
#endif
}
/// Finalizes and renders the current GUI frame to chart.
static void Render(void)
{
#ifdef UI_BACKEND_CANVAS
URendererCanvas::EndFrame();
#else
URendererNative::EndFrame();
#endif
UInputState::EndFrame();
UDiagnostics::EndFrame();
}
//+------------------------------------------------------------------+
//| Windows Containers |
//+------------------------------------------------------------------+
/// Begins an immediate-mode window. Returns true if window body is visible.
static bool Begin(const string title,
bool &io_open,
const int default_x = 20,
const int default_y = 40,
const int default_w = 260,
const int default_h = 320,
const uint flags = U_WINDOW_NONE)
{
int x = 0, y = 0, w = 0, h = 0;
bool collapsed = false;
bool visible = UWindow::Begin(title, io_open, default_x, default_y, default_w, default_h, flags, x, y, w, h, collapsed);
SUTheme th = UTheme::Get();
ulong win_id = UIdStack::GetId(title);
#ifdef UI_BACKEND_CANVAS
URendererCanvas::BeginWindowSurface(win_id, title, x, y, w, h);
// Draw Window Shadow & Outer Border
URendererCanvas::DrawRectFilled(x, y, w, h, th.window_bg);
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
// Draw Titlebar
if((flags & U_WINDOW_NO_TITLEBAR) == 0)
{
URendererCanvas::DrawRectFilled(x, y, w, th.window_title_height, th.window_titlebar);
URendererCanvas::DrawLine(x, y + th.window_title_height, x + w, y + th.window_title_height, th.window_border);
// Title Text
URendererCanvas::DrawText(x + th.padding_x, y + 4, title, th.text, th.font_name, th.font_size, 0);
// Collapse icon
if((flags & U_WINDOW_NO_COLLAPSE) == 0)
{
string icon = collapsed ? "[+]" : "[-]";
URendererCanvas::DrawText(x + w - 24, y + 4, icon, th.text_dim, th.font_name, th.font_size, 0);
}
}
#else
URendererNative::DrawRectFilled(x, y, w, h, th.window_bg);
if((flags & U_WINDOW_NO_TITLEBAR) == 0)
{
URendererNative::DrawRectFilled(x, y, w, th.window_title_height, th.window_titlebar);
URendererNative::DrawText(x + th.padding_x, y + 4, title, th.text, th.font_name, th.font_size);
}
#endif
return(visible);
}
/// Ends the current window scope.
static void End(void)
{
UWindow::End();
#ifdef UI_BACKEND_CANVAS
URendererCanvas::EndWindowSurface();
#endif
}
//+------------------------------------------------------------------+
//| Layout Controls |
//+------------------------------------------------------------------+
/// Directs the next widget to be placed on the same line horizontally.
static void SameLine(const int spacing = -1)
{
ULayout::SameLine(spacing);
}
/// Indents subsequent widgets to the right.
static void Indent(const int amount = 16)
{
ULayout::Indent(amount);
}
/// Restores indentation to the left.
static void Unindent(const int amount = 16)
{
ULayout::Unindent(amount);
}
/// Draws a horizontal divider line.
static void Separator(void)
{
int x, y, w, h;
USeparator::GetBoundingBox(w, h);
ULayout::AllocateSpace(w, h, x, y);
SUTheme th = UTheme::Get();
#ifdef UI_BACKEND_CANVAS
URendererCanvas::DrawLine(x, y + 2, x + w, y + 2, th.separator);
#else
URendererNative::DrawLine(x, y + 2, x + w, y + 2, th.separator);
#endif
}
//+------------------------------------------------------------------+
//| Text & Metric Widgets |
//+------------------------------------------------------------------+
/// Renders plain text.
static void Text(const string text)
{
TextColored(text, UTheme::Get().text);
}
/// Renders colored text (e.g. green for profit, red for drawdown).
static void TextColored(const string text, const uint argb)
{
int x = 0, y = 0, w = 0, h = 0;
ULabel::GetBoundingBox(text, w, h);
ULayout::AllocateSpace(w, h, x, y);
SUTheme th = UTheme::Get();
#ifdef UI_BACKEND_CANVAS
URendererCanvas::DrawText(x, y + 2, text, argb, th.font_name, th.font_size, 0);
#else
URendererNative::DrawText(x, y + 2, text, argb, th.font_name, th.font_size);
#endif
}
//+------------------------------------------------------------------+
//| Interactive Button Widgets |
//+------------------------------------------------------------------+
/// Renders a clickable button. Returns true on the frame it was clicked.
static bool Button(const string label, const int custom_w = 0, const int custom_h = 0)
{
return(ButtonColor(label, 0, custom_w, custom_h));
}
/// Renders a colored button (e.g. accent for Buy, danger for Panic Close).
static bool ButtonColor(const string label, const uint bg_color, const int custom_w = 0, const int custom_h = 0)
{
ulong id = UIdStack::GetId(label);
int x = 0, y = 0, w = 0, h = 0;
UButton::GetBoundingBox(label, custom_w, custom_h, w, h);
ULayout::AllocateSpace(w, h, x, y);
bool hovered = false, clicked = false, active = false;
UWidget::Behavior(id, x, y, w, h, hovered, clicked, active);
uint btn_color = UButton::GetStateColor(hovered, active, bg_color);
SUTheme th = UTheme::Get();
#ifdef UI_BACKEND_CANVAS
URendererCanvas::DrawRectFilled(x, y, w, h, btn_color);
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
// Center text inside button
int tw = UFont::MeasureTextWidth(label, th.font_size, th.font_name);
int tx = x + (w - tw) / 2;
int ty = y + (h - UFont::MeasureTextHeight(th.font_size)) / 2;
URendererCanvas::DrawText(tx, ty, label, th.text, th.font_name, th.font_size, 0);
#else
URendererNative::DrawRectFilled(x, y, w, h, btn_color);
URendererNative::DrawText(x + th.padding_x, y + 4, label, th.text, th.font_name, th.font_size);
#endif
if(clicked)
UEventQueue::Push(U_EVENT_CLICK, id, 0.0, label);
return(clicked);
}
//+------------------------------------------------------------------+
//| Toggle Switch Widgets |
//+------------------------------------------------------------------+
/// Renders an ON/OFF toggle switch. Returns true if state changed.
static bool Toggle(const string label, bool &io_value)
{
ulong id = UIdStack::GetId(label);
int x = 0, y = 0, w = 0, h = 0;
UToggle::GetBoundingBox(label, w, h);
ULayout::AllocateSpace(w, h, x, y);
bool hovered = false, clicked = false, active = false;
UWidget::Behavior(id, x, y, w, h, hovered, clicked, active);
if(clicked)
{
io_value = !io_value;
UEventQueue::Push(U_EVENT_VALUE_CHANGE, id, io_value ? 1.0 : 0.0, label);
}
SUTheme th = UTheme::Get();
int sw_w = 34;
int sw_h = 16;
int sw_y = y + (h - sw_h) / 2;
uint track_color = io_value ? th.accent : th.widget_bg;
int knob_x = io_value ? (x + sw_w - sw_h) : x;
#ifdef UI_BACKEND_CANVAS
URendererCanvas::DrawRectFilled(x, sw_y, sw_w, sw_h, track_color);
URendererCanvas::DrawRectFilled(knob_x, sw_y, sw_h, sw_h, th.text);
if(StringLen(label) > 0)
URendererCanvas::DrawText(x + sw_w + th.item_spacing_x, y + 2, label, th.text, th.font_name, th.font_size, 0);
#else
URendererNative::DrawRectFilled(x, sw_y, sw_w, sw_h, track_color);
URendererNative::DrawRectFilled(knob_x, sw_y, sw_h, sw_h, th.text);
if(StringLen(label) > 0)
URendererNative::DrawText(x + sw_w + th.item_spacing_x, y + 2, label, th.text, th.font_name, th.font_size);
#endif
return(clicked);
}
//+------------------------------------------------------------------+
//| Slider Widgets |
//+------------------------------------------------------------------+
/// Draggable horizontal slider for float/double parameters.
static bool SliderFloat(const string label,
double &io_value,
const double min_val,
const double max_val,
const string format = "%.2f",
const int custom_w = 0)
{
ulong id = UIdStack::GetId(label);
int x = 0, y = 0, w = 0, h = 0;
USlider::GetBoundingBox(label, custom_w, w, h);
ULayout::AllocateSpace(w, h, x, y);
bool hovered = false, clicked = false, active = false;
UWidget::Behavior(id, x, y, w, h, hovered, clicked, active);
bool changed = false;
if(active && UInputState::IsMouseDown())
{
changed = USlider::ProcessDrag(x, w, min_val, max_val, io_value);
if(changed)
UEventQueue::Push(U_EVENT_VALUE_CHANGE, id, io_value, label);
}
SUTheme th = UTheme::Get();
double fraction = (max_val > min_val) ? ((io_value - min_val) / (max_val - min_val)) : 0.0;
fraction = UMath::Clamp(fraction, 0.0, 1.0);
int fill_w = (int)(w * fraction);
string display_text = StringFormat("%s: " + format, label, io_value);
#ifdef UI_BACKEND_CANVAS
// Track background
URendererCanvas::DrawRectFilled(x, y, w, h, th.widget_bg);
// Active fill
if(fill_w > 0)
URendererCanvas::DrawRectFilled(x, y, fill_w, h, th.accent);
// Border
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
// Value text centered
URendererCanvas::DrawText(x + th.padding_x, y + 2, display_text, th.text, th.font_name, th.font_size, 0);
#else
URendererNative::DrawRectFilled(x, y, w, h, th.widget_bg);
if(fill_w > 0)
URendererNative::DrawRectFilled(x, y, fill_w, h, th.accent);
URendererNative::DrawText(x + th.padding_x, y + 2, display_text, th.text, th.font_name, th.font_size);
#endif
return(changed);
}
/// Draggable horizontal slider for integer parameters.
static bool SliderInt(const string label, int &io_value, const int min_val, const int max_val, const int custom_w = 0)
{
double dval = (double)io_value;
bool changed = SliderFloat(label, dval, (double)min_val, (double)max_val, "%.0f", custom_w);
if(changed)
io_value = (int)MathRound(dval);
return(changed);
}
//+------------------------------------------------------------------+
//| Progress & Status Widgets |
//+------------------------------------------------------------------+
/// Visual progress bar with optional high-value alert color ramp (for Prop Firm limits).
static void ProgressBar(const double fraction,
const int custom_w = 0,
const int custom_h = 0,
const string overlay_text = "",
const bool alert_on_high = false)
{
int x = 0, y = 0, w = 0, h = 0;
UProgressBar::GetBoundingBox(custom_w, custom_h, w, h);
ULayout::AllocateSpace(w, h, x, y);
SUTheme th = UTheme::Get();
double clamped_frac = UMath::Clamp(fraction, 0.0, 1.0);
int fill_w = (int)(w * clamped_frac);
uint fill_color = UProgressBar::GetProgressColor(clamped_frac, alert_on_high);
#ifdef UI_BACKEND_CANVAS
// Track
URendererCanvas::DrawRectFilled(x, y, w, h, th.widget_bg);
// Fill
if(fill_w > 0)
URendererCanvas::DrawRectFilled(x, y, fill_w, h, fill_color);
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
// Overlay text if present
if(StringLen(overlay_text) > 0)
{
int tw = UFont::MeasureTextWidth(overlay_text, th.font_size - 1, th.font_name);
int tx = x + (w - tw) / 2;
int ty = y + (h - UFont::MeasureTextHeight(th.font_size - 1)) / 2;
URendererCanvas::DrawText(tx, ty, overlay_text, th.text, th.font_name, th.font_size - 1, 0);
}
#else
URendererNative::DrawRectFilled(x, y, w, h, th.widget_bg);
if(fill_w > 0)
URendererNative::DrawRectFilled(x, y, fill_w, h, fill_color);
if(StringLen(overlay_text) > 0)
URendererNative::DrawText(x + th.padding_x, y + 1, overlay_text, th.text, th.font_name, th.font_size - 1);
#endif
}
//+------------------------------------------------------------------+
//| Collapsing Section Header |
//+------------------------------------------------------------------+
/// Collapsible section header. Returns true if section contents should be drawn.
static bool CollapsingHeader(const string label, const bool default_open = true)
{
ulong id = UIdStack::GetId(label);
int x = 0, y = 0, w = 0, h = 0;
UHeader::GetBoundingBox(w, h);
ULayout::AllocateSpace(w, h, x, y);
bool hovered = false, clicked = false, active = false;
UWidget::Behavior(id, x, y, w, h, hovered, clicked, active);
bool is_open = UHeader::ProcessHeader(id, clicked, default_open);
SUTheme th = UTheme::Get();
uint bg = hovered ? th.widget_hover : th.widget_bg;
string icon = is_open ? "[-] " : "[+] ";
string full_title = icon + label;
#ifdef UI_BACKEND_CANVAS
URendererCanvas::DrawRectFilled(x, y, w, h, bg);
URendererCanvas::DrawRect(x, y, w, h, th.window_border);
URendererCanvas::DrawText(x + th.padding_x, y + 3, full_title, th.text, th.font_name, th.font_size, 0);
#else
URendererNative::DrawRectFilled(x, y, w, h, bg);
URendererNative::DrawText(x + th.padding_x, y + 3, full_title, th.text, th.font_name, th.font_size);
#endif
return(is_open);
}
};