2971 lines
122 KiB
MQL5
2971 lines
122 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| AI Canvas Render.mqh |
| |||
//| Copyright 2026, Allan Munene Mutiiria. |
| |||
//| https://t.me/Forex_Algo_Trader |
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "Copyright 2026, Allan Munene Mutiiria."
| |||
#property link "https://t.me/Forex_Algo_Trader"
| |||
| |||
//--- Include guard
| |||
#ifndef AI_CANVAS_RENDER_MQH
| |||
#define AI_CANVAS_RENDER_MQH
| |||
| |||
//--- Include required modules
| |||
#include <Canvas/Canvas.mqh>
| |||
#include "AI Canvas Theme.mqh"
| |||
#include "AI Canvas State.mqh"
| |||
#include "AI Canvas Primitives.mqh"
| |||
#include "AI Canvas Scrollbar.mqh"
| |||
#include "AI Canvas Editor.mqh"
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Canvas Objects |
| |||
//+------------------------------------------------------------------+
| |||
CAiCanvasFast g_ai_canvMain; // Main dashboard canvas (direct-buffer fast subclass)
| |||
CCanvas g_ai_canvSmallPopup; // Small history popup canvas
| |||
CCanvas g_ai_canvBigPopup; // Big popup canvas
| |||
CCanvas g_ai_canvSearchPopup; // Search popup canvas
| |||
CAiCanvasFast g_ai_canvPrompt; // Prompt-pane overlay canvas (direct-buffer fast subclass)
| |||
bool g_ai_canvPromptExists = false; // Prompt overlay init flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Persistent Off-screen Clip Buffers |
| |||
//+------------------------------------------------------------------+
| |||
CAiCanvasFast g_ai_canvChatTmp; // Chat-pane scratch canvas
| |||
bool g_ai_canvChatTmpReady = false; // Chat scratch init flag
| |||
int g_ai_canvChatTmpW = 0; // Chat scratch width
| |||
int g_ai_canvChatTmpH = 0; // Chat scratch height
| |||
| |||
CAiCanvasFast g_ai_canvSearchTmp; // Search popup scratch canvas
| |||
bool g_ai_canvSearchTmpReady = false; // Search scratch init flag
| |||
int g_ai_canvSearchTmpW = 0; // Search scratch width
| |||
int g_ai_canvSearchTmpH = 0; // Search scratch height
| |||
| |||
//--- Popup-free backdrop snapshot used to repaint overlays cheaply
| |||
CAiCanvasFast g_ai_canvBackdrop;
| |||
bool g_ai_canvBackdropExists = false;
| |||
int g_ai_canvBackdropW = 0;
| |||
int g_ai_canvBackdropH = 0;
| |||
| |||
bool g_ai_canvMainExists = false; // Main canvas init flag
| |||
bool g_ai_canvSmallExists = false; // Small popup init flag
| |||
bool g_ai_canvBigExists = false; // Big popup init flag
| |||
bool g_ai_canvSearchExists = false; // Search popup init flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Drawing Primitives and Editor Instances |
| |||
//+------------------------------------------------------------------+
| |||
CAiCanvasPrimitives g_ai_prim; // Drawing primitives helper
| |||
CAiEditor g_ai_editor; // Main prompt editor
| |||
CAiEditor g_ai_searchEditor; // Search query editor
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Test if prompt editor is empty or whitespace-only |
| |||
//+------------------------------------------------------------------+
| |||
bool Ai_PromptIsEmpty()
| |||
{
| |||
//--- Read editor text and check length
| |||
const string txt = g_ai_editor.GetText();
| |||
const int n = StringLen(txt);
| |||
if(n == 0)
| |||
return true;
| |||
//--- Walk characters looking for non-whitespace
| |||
for(int i = 0; i < n; i++)
| |||
{
| |||
const ushort c = StringGetCharacter(txt, i);
| |||
if(c != ' ' && c != '\t' && c != '\n' && c != '\r')
| |||
return false;
| |||
}
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Scroll States |
| |||
//+------------------------------------------------------------------+
| |||
AiScrollState g_ai_chatScroll; // Chat pane scroll state
| |||
AiScrollState g_ai_bigScroll; // Big popup scroll state
| |||
AiScrollState g_ai_searchScroll; // Search popup scroll state
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Image Pixel Buffers |
| |||
//+------------------------------------------------------------------+
| |||
//--- Identify sidebar icons by id since they draw as vectors, not cached buffers
| |||
#define AI_ICON_SEARCH 0
| |||
#define AI_ICON_NEWCHAT 1
| |||
#define AI_ICON_CLEAR 2
| |||
#define AI_ICON_HISTORY 3
| |||
bool g_ai_imagesLoaded = false; // Images loaded flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Hover Code Constants |
| |||
//+------------------------------------------------------------------+
| |||
#define AI_HOV_NONE 0 // No hover
| |||
#define AI_HOV_CLOSE 1 // Close button
| |||
#define AI_HOV_TOGGLE 2 // Sidebar toggle
| |||
#define AI_HOV_NEW_CHAT 3 // New chat button
| |||
#define AI_HOV_CLEAR 4 // Clear chat button
| |||
#define AI_HOV_HISTORY 5 // History button
| |||
#define AI_HOV_SEARCH 6 // Search button
| |||
#define AI_HOV_CHART 7 // Chart action
| |||
#define AI_HOV_SIGNAL 8 // Signal action half
| |||
#define AI_HOV_SEND 9 // Send button
| |||
#define AI_HOV_REGEN 10 // Regenerate button
| |||
#define AI_HOV_EXPORT 11 // Export button
| |||
#define AI_HOV_BIG_CLOSE 12 // Big popup close
| |||
#define AI_HOV_SEARCH_CLOSE 13 // Search popup close
| |||
#define AI_HOV_SEE_MORE 14 // See more link
| |||
#define AI_HOV_THEME 15 // Theme toggle
| |||
#define AI_HOV_DRAG 16 // Header drag area
| |||
#define AI_HOV_FOOTER_DD 17 // Footer dropdown anchor
| |||
#define AI_HOV_SIGNAL_DD 18 // Signal chevron half
| |||
#define AI_HOV_SCROLL_FAB 19 // Scroll-to-bottom FAB
| |||
#define AI_HOV_SIDE_CHAT_BASE 100 // Sidebar chat row base
| |||
#define AI_HOV_SIDE_DEL_BASE 200 // Sidebar delete base
| |||
#define AI_HOV_SMALL_CHAT_BASE 300 // Small popup chat row base
| |||
#define AI_HOV_SMALL_DEL_BASE 400 // Small popup delete base
| |||
#define AI_HOV_BIG_CHAT_BASE 500 // Big popup chat row base
| |||
#define AI_HOV_BIG_DEL_BASE 600 // Big popup delete base
| |||
#define AI_HOV_SEARCH_CHAT_BASE 700 // Search popup chat row base
| |||
#define AI_HOV_SEARCH_DEL_BASE 800 // Search popup delete base
| |||
#define AI_HOV_FOOTER_DD_ITEM_BASE 900 // Footer dropdown item base
| |||
#define AI_HOV_USER_EDIT_BASE 1000 // User edit pencil base
| |||
| |||
//--- Map each hover code to its panel region bitmask for the fast hover frame
| |||
#define AI_RG_HEADER 1
| |||
#define AI_RG_SIDEBAR 2
| |||
#define AI_RG_FOOTER 4
| |||
#define AI_RG_CHAT 8
| |||
#define AI_RG_UNKNOWN 16
| |||
//+------------------------------------------------------------------+
| |||
//| Map a hover code to its panel region bitmask |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_HovRegionMask(int h)
| |||
{
| |||
if(h == AI_HOV_NONE)
| |||
return 0;
| |||
if(h == AI_HOV_CLOSE || h == AI_HOV_THEME || h == AI_HOV_DRAG)
| |||
return AI_RG_HEADER;
| |||
if(h == AI_HOV_TOGGLE || h == AI_HOV_NEW_CHAT || h == AI_HOV_CLEAR
| |||
|| h == AI_HOV_HISTORY || h == AI_HOV_SEARCH)
| |||
return AI_RG_SIDEBAR;
| |||
if(h >= AI_HOV_SIDE_CHAT_BASE && h < AI_HOV_SIDE_DEL_BASE + 100)
| |||
return AI_RG_SIDEBAR;
| |||
if(h == AI_HOV_SIGNAL || h == AI_HOV_SIGNAL_DD || h == AI_HOV_SEND
| |||
|| h == AI_HOV_REGEN || h == AI_HOV_EXPORT)
| |||
return AI_RG_FOOTER;
| |||
if(h == AI_HOV_SCROLL_FAB)
| |||
return AI_RG_CHAT;
| |||
if(h >= AI_HOV_USER_EDIT_BASE && h < AI_HOV_USER_EDIT_BASE + 100)
| |||
return AI_RG_CHAT;
| |||
return AI_RG_UNKNOWN;
| |||
}
| |||
| |||
//--- Width of chevron-half zone in split signal button
| |||
#define AI_SIGNAL_DD_ZONE_W 28
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Hover and Mouse Tracking |
| |||
//+------------------------------------------------------------------+
| |||
int g_ai_hover = AI_HOV_NONE; // Current hover code
| |||
int g_ai_mouseLx = -1; // Last canvas-local mouse X
| |||
int g_ai_mouseLy = -1; // Last canvas-local mouse Y
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Action Button Rectangles |
| |||
//+------------------------------------------------------------------+
| |||
int g_ai_regenL = 0, g_ai_regenT = 0, g_ai_regenR = 0, g_ai_regenB = 0; // Regenerate button rect
| |||
int g_ai_exportL = 0, g_ai_exportT = 0, g_ai_exportR = 0, g_ai_exportB = 0; // Export button rect
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Scroll-to-bottom FAB Rectangle |
| |||
//+------------------------------------------------------------------+
| |||
int g_ai_scrollFabL = 0, g_ai_scrollFabT = 0, g_ai_scrollFabR = 0, g_ai_scrollFabB = 0; // FAB rect
| |||
bool g_ai_scrollFabVisible = false; // FAB visibility flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Per-User-Message Edit Pencil State |
| |||
//+------------------------------------------------------------------+
| |||
int g_ai_userEditRectL[]; // Wide hover rect left bounds
| |||
int g_ai_userEditRectT[]; // Wide hover rect top bounds
| |||
int g_ai_userEditRectR[]; // Wide hover rect right bounds
| |||
int g_ai_userEditRectB[]; // Wide hover rect bottom bounds
| |||
int g_ai_userEditClickL[]; // Narrow click rect left bounds
| |||
int g_ai_userEditClickT[]; // Narrow click rect top bounds
| |||
int g_ai_userEditClickR[]; // Narrow click rect right bounds
| |||
int g_ai_userEditClickB[]; // Narrow click rect bottom bounds
| |||
string g_ai_userEditPrompt[]; // Cached prompt text per pencil
| |||
| |||
bool g_ai_chatScrollPin = false; // Chat scroll auto-pin to bottom flag
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute theme-aware border color for given fill |
| |||
//+------------------------------------------------------------------+
| |||
color AiBorderForBg(color bg)
| |||
{
| |||
//--- Lighten on dark theme, darken on light theme
| |||
if(g_ai_darkTheme)
| |||
return AiLightenColor(bg, 0.35);
| |||
return AiDarkenColor(bg, 0.78);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get current sidebar width |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_SidebarW()
| |||
{
| |||
//--- Return expanded or collapsed width
| |||
return g_ai_sidebarExpanded ? AI_SIDEBAR_W_EXPANDED : AI_SIDEBAR_W_COLLAPSED;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get current dashboard width |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_DashboardW()
| |||
{
| |||
//--- Sum sidebar and main pane widths
| |||
return Ai_SidebarW() + AI_MAIN_W;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get current dashboard height |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_DashboardH()
| |||
{
| |||
//--- Sum all vertical sections
| |||
return AI_HEADER_H + AI_PAD + AI_DISPLAY_H + AI_PAD + AI_PROMPT_H + AI_PAD + AI_FOOTER_H;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Load and scale all UI bitmap resources |
| |||
//+------------------------------------------------------------------+
| |||
bool Ai_LoadImages()
| |||
{
| |||
//--- Skip loading since all marks draw as vectors; kept as a no-op for callers
| |||
g_ai_imagesLoaded = true;
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get close button rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetCloseBtnRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Compute button dimensions and anchor to top-right
| |||
const int btnH = AI_HEADER_H - 7;
| |||
const int btnW = (int)(btnH * 1.5);
| |||
outR = Ai_DashboardW();
| |||
outL = outR - btnW;
| |||
outT = 0;
| |||
outB = outT + btnH;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get theme toggle button rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetThemeBtnRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Anchor to left of close button
| |||
const int btnH = AI_HEADER_H - 7;
| |||
const int btnW = btnH;
| |||
int cL, cT, cR, cB;
| |||
Ai_GetCloseBtnRect(cL, cT, cR, cB);
| |||
outR = cL;
| |||
outL = outR - btnW;
| |||
outT = 0;
| |||
outB = btnH;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute Y position of sidebar item by index |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_SidebarItemY(int i)
| |||
{
| |||
//--- Account for logo height and prior items
| |||
int logoH = g_ai_sidebarExpanded ? 81 : 30;
| |||
int y = AI_HEADER_H + 10 + logoH + 8;
| |||
y += i * (AI_BUTTON_H + 6);
| |||
return y;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get sidebar button rectangle by index |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetSidebarBtnRect(int idx, int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Compute rect using sidebar width and item Y
| |||
const int sw = Ai_SidebarW();
| |||
outL = 5;
| |||
outR = sw - 5;
| |||
outT = Ai_SidebarItemY(idx);
| |||
outB = outT + AI_BUTTON_H;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get sidebar toggle button rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetToggleBtnRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Anchor to bottom of sidebar
| |||
const int sw = Ai_SidebarW();
| |||
outL = 5;
| |||
outR = sw - 5;
| |||
outB = Ai_DashboardH() - 8;
| |||
outT = outB - AI_BUTTON_H;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get top of sidebar chat list |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_SidebarChatListTop()
| |||
{
| |||
//--- Below history button
| |||
return Ai_SidebarItemY(3) + AI_BUTTON_H + 8;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get sidebar chat row rectangle by row index |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetSidebarChatRowRect(int row, int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Compute row layout
| |||
const int sw = Ai_SidebarW();
| |||
const int rowH = 24;
| |||
outL = 8;
| |||
outR = sw - 8;
| |||
outT = Ai_SidebarChatListTop() + row * (rowH + 3);
| |||
outB = outT + rowH;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get main pane left X |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_MainContentX()
| |||
{
| |||
//--- Return the sidebar width as the main pane left edge
| |||
return Ai_SidebarW();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get chat pane top Y |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_ChatPaneT()
| |||
{
| |||
//--- Return the header bottom plus padding
| |||
return AI_HEADER_H + AI_PAD;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get chat pane bottom Y |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_ChatPaneB()
| |||
{
| |||
//--- Return the chat pane top plus display height
| |||
return Ai_ChatPaneT() + AI_DISPLAY_H;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get prompt pane top Y |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_PromptPaneT()
| |||
{
| |||
//--- Return the chat pane bottom plus padding
| |||
return Ai_ChatPaneB() + AI_PAD;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get prompt pane bottom Y |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_PromptPaneB()
| |||
{
| |||
//--- Return the prompt pane top plus prompt height
| |||
return Ai_PromptPaneT() + AI_PROMPT_H;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get footer top Y |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_FooterT()
| |||
{
| |||
//--- Return the prompt pane bottom plus padding
| |||
return Ai_PromptPaneB() + AI_PAD;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get footer bottom Y |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_FooterB()
| |||
{
| |||
//--- Return the footer top plus footer height
| |||
return Ai_FooterT() + AI_FOOTER_H;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get chat pane rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetChatPaneRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Inset main pane bounds by side padding
| |||
outL = Ai_MainContentX() + AI_SIDE_PAD;
| |||
outT = Ai_ChatPaneT();
| |||
outR = Ai_DashboardW() - AI_SIDE_PAD;
| |||
outB = Ai_ChatPaneB();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get prompt pane rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetPromptPaneRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Inset main pane bounds by side padding
| |||
outL = Ai_MainContentX() + AI_SIDE_PAD;
| |||
outT = Ai_PromptPaneT();
| |||
outR = Ai_DashboardW() - AI_SIDE_PAD;
| |||
outB = Ai_PromptPaneB();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute footer dropdown / signal button width |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_GetFooterDropdownWidth()
| |||
{
| |||
//--- Find longest action label width
| |||
int longestPx = 0;
| |||
for(int i = 0; i < AI_FOOTER_DD_COUNT; i++)
| |||
{
| |||
int w = AiTextWidth(AI_FOOTER_DD_LABELS[i], "Arial Bold", 9);
| |||
if(w > longestPx)
| |||
longestPx = w;
| |||
}
| |||
//--- Compose action zone + chevron zone width
| |||
const int actionZone = 8 + 18 + 8 + longestPx + 4;
| |||
int total = actionZone + AI_SIGNAL_DD_ZONE_W;
| |||
//--- Floor to minimum width
| |||
if(total < 140)
| |||
total = 140;
| |||
return total;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get signal button rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetSignalBtnRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Compute width and vertically center in footer
| |||
const int btnH = 32;
| |||
outL = Ai_MainContentX() + AI_SIDE_PAD;
| |||
outR = outL + Ai_GetFooterDropdownWidth();
| |||
outT = Ai_FooterT() + (AI_FOOTER_H - btnH) / 2;
| |||
outB = outT + btnH;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get send button rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetSendBtnRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Anchor to right edge of main pane
| |||
const int btnH = 32;
| |||
outR = Ai_DashboardW() - AI_SIDE_PAD;
| |||
outL = outR - 44;
| |||
outT = Ai_FooterT() + (AI_FOOTER_H - btnH) / 2;
| |||
outB = outT + btnH;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Compute X coordinate of signal button separator |
| |||
//+------------------------------------------------------------------+
| |||
int Ai_SignalSeparatorX()
| |||
{
| |||
//--- Subtract chevron zone width from right edge
| |||
int sL, sT, sR, sB;
| |||
Ai_GetSignalBtnRect(sL, sT, sR, sB);
| |||
return sR - AI_SIGNAL_DD_ZONE_W;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get signal button action half rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetSignalActionRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Use signal button bounds with separator as right edge
| |||
int sL, sT, sR, sB;
| |||
Ai_GetSignalBtnRect(sL, sT, sR, sB);
| |||
outL = sL;
| |||
outT = sT;
| |||
outR = Ai_SignalSeparatorX();
| |||
outB = sB;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get signal button chevron half rectangle |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetSignalDdRect(int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Use separator as left edge of dd half
| |||
int sL, sT, sR, sB;
| |||
Ai_GetSignalBtnRect(sL, sT, sR, sB);
| |||
outL = Ai_SignalSeparatorX();
| |||
outT = sT;
| |||
outR = sR;
| |||
outB = sB;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render dashboard header |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderHeader()
| |||
{
| |||
//--- Fill header background
| |||
const int dW = Ai_DashboardW();
| |||
g_ai_prim.FillRoundRectHR(g_ai_canvMain, 0, 0, dW, AI_HEADER_H, 0,
| |||
ColorToARGB(g_ai_headerBg, 255));
| |||
//--- Draw the MQL5 wordmark as vector text (was a stamped 104x40 bitmap)
| |||
AiDrawLogoMQL5(g_ai_canvMain, AI_SIDE_PAD, (AI_HEADER_H - 40) / 2, 104, 40);
| |||
//--- Stamp title text
| |||
const string title = "ChatGPT AI EA";
| |||
int titleY = (AI_HEADER_H - AiTextHeight("Arial Bold", AI_FONT_TITLE)) / 2;
| |||
AiStampTextAA(g_ai_canvMain, AI_SIDE_PAD + 104 + 8, titleY,
| |||
title, "Arial Bold", AI_FONT_TITLE, g_ai_titleText);
| |||
//--- Header server-time readout removed to avoid a per-frame text stamp
| |||
//--- Render theme toggle button
| |||
int tL, tT, tR, tB;
| |||
Ai_GetThemeBtnRect(tL, tT, tR, tB);
| |||
const bool themeHov = (g_ai_hover == AI_HOV_THEME);
| |||
if(themeHov)
| |||
{
| |||
const color hoverBg = AiDarkenColor(g_ai_headerBg, 0.85);
| |||
g_ai_canvMain.FillRectangle(tL, tT, tR - 1, tB - 1, ColorToARGB(hoverBg, 255));
| |||
}
| |||
//--- Show a SUN in light mode and a MOON in dark so the toggle previews the switch
| |||
const int themeSz = 16;
| |||
const int thX = tL + (tR - tL - themeSz) / 2;
| |||
const int thY = tT + (tB - tT - themeSz) / 2;
| |||
const color themeBtnBg = themeHov ? AiDarkenColor(g_ai_headerBg, 0.85) : g_ai_headerBg;
| |||
if(g_ai_darkTheme)
| |||
AiDrawIconMoon(g_ai_canvMain, thX, thY, themeSz,
| |||
ColorToARGB(g_ai_titleText, 255), ColorToARGB(themeBtnBg, 255));
| |||
else
| |||
AiDrawIconSun(g_ai_canvMain, thX, thY, themeSz, ColorToARGB(g_ai_titleText, 255));
| |||
//--- Render close button
| |||
int cL, cT, cR, cB;
| |||
Ai_GetCloseBtnRect(cL, cT, cR, cB);
| |||
const bool closeHov = (g_ai_hover == AI_HOV_CLOSE);
| |||
color closeFg = closeHov ? g_ai_closeColorHover : g_ai_closeColor;
| |||
if(closeHov)
| |||
{
| |||
const uint hoverArgb = ColorToARGB(g_ai_closeBgHover, 255);
| |||
g_ai_canvMain.FillRectangle(cL, cT, cR - 1, cB - 1, hoverArgb);
| |||
}
| |||
//--- Draw the close X as a vector sized against the button
| |||
const int closeSz = 20;
| |||
AiDrawIconX(g_ai_canvMain, cL + (cR - cL - closeSz) / 2,
| |||
cT + (cB - cT - closeSz) / 2, closeSz, ColorToARGB(closeFg, 255));
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render sidebar background and divider line |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderSidebarBg()
| |||
{
| |||
//--- Fill background and draw right divider
| |||
const int sw = Ai_SidebarW();
| |||
const int dh = Ai_DashboardH();
| |||
g_ai_prim.FillRoundRectHR(g_ai_canvMain, 0, AI_HEADER_H, sw, dh - AI_HEADER_H, 0,
| |||
ColorToARGB(g_ai_sidebarBg, 255));
| |||
g_ai_canvMain.LineVertical(sw - 1, AI_HEADER_H, dh - 1,
| |||
ColorToARGB(g_ai_border, 255));
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render sidebar logo at appropriate size |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderSidebarLogo()
| |||
{
| |||
//--- Pick logo by sidebar expansion state
| |||
const int sw = Ai_SidebarW();
| |||
//--- Draw the orb vector at native resolution for both sizes
| |||
if(g_ai_sidebarExpanded)
| |||
AiDrawLogoOrb(g_ai_canvMain, (sw - 81) / 2, AI_HEADER_H + 10, 81);
| |||
else
| |||
AiDrawLogoOrb(g_ai_canvMain, (sw - 30) / 2, AI_HEADER_H + 10, 30);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render a single sidebar button |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderSidebarBtn(int idx, color bgIdle, color bgHover, int hovId,
| |||
const int iconId, const string label)
| |||
{
| |||
//--- Get button rect and pick fill color by hover state
| |||
int bL, bT, bR, bB;
| |||
Ai_GetSidebarBtnRect(idx, bL, bT, bR, bB);
| |||
color bg = (g_ai_hover == hovId) ? bgHover : bgIdle;
| |||
//--- Fill button and draw border
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 6,
| |||
ColorToARGB(bg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 6, 1,
| |||
ColorToARGB(AiBorderForBg(bg), 255));
| |||
//--- Position icon based on expansion state
| |||
const int sw = Ai_SidebarW();
| |||
int iconX = g_ai_sidebarExpanded ? (bL + 8) : (bL + (bR - bL - 22) / 2);
| |||
int iconY = bT + (bB - bT - 22) / 2;
| |||
//--- vector icon, drawn at 22px in its brand colour
| |||
switch(iconId)
| |||
{
| |||
case AI_ICON_SEARCH:
| |||
AiDrawIconSearch(g_ai_canvMain, iconX, iconY, 22, ColorToARGB(C'53,52,54', 255));
| |||
break;
| |||
case AI_ICON_NEWCHAT:
| |||
AiDrawIconNewChat(g_ai_canvMain, iconX, iconY, 22, ColorToARGB(C'28,153,246', 255));
| |||
break;
| |||
case AI_ICON_CLEAR:
| |||
AiDrawIconClear(g_ai_canvMain, iconX, iconY, 22, ColorToARGB(C'244,66,54', 255));
| |||
break;
| |||
case AI_ICON_HISTORY:
| |||
AiDrawIconHistory(g_ai_canvMain, iconX, iconY, 22, ColorToARGB(C'251,36,18', 255));
| |||
break;
| |||
}
| |||
//--- Stamp label only when expanded
| |||
if(g_ai_sidebarExpanded)
| |||
{
| |||
int lblY = bT + (bB - bT - AiTextHeight("Arial", AI_FONT_BUTTON)) / 2;
| |||
AiStampTextAA(g_ai_canvMain, iconX + 22 + 6, lblY,
| |||
label, "Arial", AI_FONT_BUTTON, g_ai_titleText);
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render sidebar chat list |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderSidebarChats()
| |||
{
| |||
//--- Bail when sidebar is collapsed
| |||
if(!g_ai_sidebarExpanded)
| |||
return;
| |||
//--- Cap visible rows at 7
| |||
const int n = MathMin(ArraySize(g_ai_chats), 7);
| |||
const int sw = Ai_SidebarW();
| |||
//--- Render each chat row
| |||
for(int i = 0; i < n; i++)
| |||
{
| |||
//--- Compute chat index (most recent first)
| |||
int chatIdx = ArraySize(g_ai_chats) - 1 - i;
| |||
int bL, bT, bR, bB;
| |||
Ai_GetSidebarChatRowRect(i, bL, bT, bR, bB);
| |||
//--- Determine hover state
| |||
bool hovRow = (g_ai_hover == AI_HOV_SIDE_CHAT_BASE + i)
| |||
|| (g_ai_hover == AI_HOV_SIDE_DEL_BASE + i);
| |||
bool hovDel = (g_ai_hover == AI_HOV_SIDE_DEL_BASE + i);
| |||
color bg = hovRow ? g_ai_chatItemBgHover : g_ai_chatItemBg;
| |||
//--- Fill row and draw border
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 4,
| |||
ColorToARGB(bg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 4, 1,
| |||
ColorToARGB(AiBorderForBg(bg), 255));
| |||
//--- Fit title text to available width
| |||
string fullText = g_ai_chats[chatIdx].title;
| |||
const int sbDeleteIconW = 14;
| |||
const int sbAvailW = (bR - bL) - 18 - sbDeleteIconW;
| |||
string labelText = Ai_FitTextToWidth(fullText, "Arial", AI_FONT_LABEL, sbAvailW);
| |||
//--- Pick text color based on active state
| |||
color textCol = (g_ai_chats[chatIdx].id == g_ai_currentChatId)
| |||
? g_ai_chatItemActiveText : g_ai_titleText;
| |||
//--- Stamp title text
| |||
int lblY = bT + (bB - bT - AiTextHeight("Arial", AI_FONT_LABEL)) / 2;
| |||
AiStampTextAA(g_ai_canvMain, bL + 6, lblY, labelText,
| |||
"Arial", AI_FONT_LABEL, textCol);
| |||
//--- Stamp delete glyph on hover
| |||
if(hovRow)
| |||
{
| |||
color xCol = hovDel ? g_ai_chatItemDelHover : g_ai_subText;
| |||
int xW = 14, xH = 14; // vector X footprint (was a Wingdings 2 glyph)
| |||
AiDrawIconX(g_ai_canvMain, bR - 6 - xW, bT + (bB - bT - xH) / 2, 14, ColorToARGB(xCol, 255));
| |||
}
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render sidebar toggle button |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderToggleBtn()
| |||
{
| |||
//--- Get rect and pick fill by hover state
| |||
int bL, bT, bR, bB;
| |||
Ai_GetToggleBtnRect(bL, bT, bR, bB);
| |||
color bg = (g_ai_hover == AI_HOV_TOGGLE) ? g_ai_buttonToggleBgHover : g_ai_buttonToggleBg;
| |||
//--- Fill and border button
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 6,
| |||
ColorToARGB(bg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, bL, bT, bR - bL, bB - bT, 6, 1,
| |||
ColorToARGB(AiBorderForBg(bg), 255));
| |||
//--- Draw the sidebar toggle mark as a vector sized against the button
| |||
const int tgSz = 26;
| |||
AiDrawIconToggle(g_ai_canvMain, bL + (bR - bL - tgSz) / 2, bT + (bB - bT - tgSz) / 2,
| |||
tgSz, g_ai_sidebarExpanded, ColorToARGB(C'30,100,220', 255)); // same blue as the Get Chart Data pill
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render entire sidebar |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderSidebar()
| |||
{
| |||
//--- Render sidebar components in order
| |||
Ai_RenderSidebarBg();
| |||
Ai_RenderSidebarLogo();
| |||
Ai_RenderSidebarBtn(0, g_ai_buttonSearchBg, g_ai_buttonSearchBgHover,
| |||
AI_HOV_SEARCH, AI_ICON_SEARCH, "Search");
| |||
Ai_RenderSidebarBtn(1, g_ai_buttonNewChatBg, g_ai_buttonNewChatBgHover,
| |||
AI_HOV_NEW_CHAT, AI_ICON_NEWCHAT, "New Chat");
| |||
Ai_RenderSidebarBtn(2, g_ai_buttonClearBg, g_ai_buttonClearBgHover,
| |||
AI_HOV_CLEAR, AI_ICON_CLEAR, "Clear");
| |||
Ai_RenderSidebarBtn(3, g_ai_buttonHistoryBg, g_ai_buttonHistoryBgHover,
| |||
AI_HOV_HISTORY, AI_ICON_HISTORY, "History");
| |||
Ai_RenderSidebarChats();
| |||
Ai_RenderToggleBtn();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render chat pane with messages and scroll FAB |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderChatPane()
| |||
{
| |||
//--- Get pane rect and dimensions
| |||
int rL, rT, rR, rB;
| |||
Ai_GetChatPaneRect(rL, rT, rR, rB);
| |||
const int paneW = rR - rL;
| |||
const int paneH = rB - rT;
| |||
//--- Restore the solid dashboard background behind the pane before compositing
| |||
g_ai_canvMain.FillRectangle(rL, rT, rR - 1, rB - 1, ColorToARGB(g_ai_bg, 255));
| |||
//--- Fill pane background and draw border
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, rL, rT, paneW, paneH, 6,
| |||
ColorToARGB(g_ai_panelAlt, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, rL, rT, paneW, paneH, 6, 1,
| |||
ColorToARGB(g_ai_border, 255));
| |||
//--- Conversation parse cache - static across renders
| |||
static string s_cacheRoles[];
| |||
static string s_cacheContents[];
| |||
static string s_cacheTimestamps[];
| |||
static int s_cacheLen = -1;
| |||
static int s_cacheCharQ1 = -1;
| |||
static int s_cacheCharQ3 = -1;
| |||
//--- Compute cache fingerprint from history
| |||
const int curLen = StringLen(g_ai_conversationHistory);
| |||
const int curQ1 = (curLen >= 4)
| |||
? (int)StringGetCharacter(g_ai_conversationHistory, curLen / 4)
| |||
: -1;
| |||
const int curQ3 = (curLen >= 4)
| |||
? (int)StringGetCharacter(g_ai_conversationHistory, (curLen * 3) / 4)
| |||
: -1;
| |||
const bool cacheHit = (s_cacheLen == curLen)
| |||
&& (s_cacheCharQ1 == curQ1)
| |||
&& (s_cacheCharQ3 == curQ3);
| |||
//--- Working arrays for parsed messages
| |||
string msgRoles[];
| |||
string msgContents[];
| |||
string msgTimestamps[];
| |||
//--- Cache hit path - reuse parsed arrays
| |||
if(cacheHit)
| |||
{
| |||
const int cn = ArraySize(s_cacheRoles);
| |||
ArrayResize(msgRoles, cn);
| |||
ArrayResize(msgContents, cn);
| |||
ArrayResize(msgTimestamps, cn);
| |||
for(int ci = 0; ci < cn; ci++)
| |||
{
| |||
msgRoles[ci] = s_cacheRoles[ci];
| |||
msgContents[ci] = s_cacheContents[ci];
| |||
msgTimestamps[ci] = s_cacheTimestamps[ci];
| |||
}
| |||
}
| |||
//--- Cache miss path - parse and snapshot
| |||
else
| |||
{
| |||
//--- Split history into lines
| |||
string parts[];
| |||
const int numParts = StringSplit(g_ai_conversationHistory, '\n', parts);
| |||
//--- Initialize parser state
| |||
string curRole = "";
| |||
string curContent = "";
| |||
string curTs = "";
| |||
//--- Walk lines and group into role-tagged messages
| |||
for(int p = 0; p < numParts; p++)
| |||
{
| |||
string line = parts[p];
| |||
string trimmed = line;
| |||
StringTrimLeft(trimmed);
| |||
StringTrimRight(trimmed);
| |||
//--- Blank lines belong to current content
| |||
if(StringLen(trimmed) == 0)
| |||
{
| |||
if(curRole != "")
| |||
curContent += "\n";
| |||
continue;
| |||
}
| |||
//--- New user block
| |||
if(StringFind(trimmed, "You: ") == 0)
| |||
{
| |||
if(curRole != "")
| |||
{
| |||
int sz = ArraySize(msgRoles);
| |||
ArrayResize(msgRoles, sz + 1);
| |||
ArrayResize(msgContents, sz + 1);
| |||
ArrayResize(msgTimestamps, sz + 1);
| |||
msgRoles[sz] = curRole;
| |||
msgContents[sz] = curContent;
| |||
msgTimestamps[sz] = curTs;
| |||
}
| |||
curRole = "User";
| |||
curContent = StringSubstr(line, StringFind(line, "You: ") + 5);
| |||
curTs = "";
| |||
continue;
| |||
}
| |||
//--- New AI block
| |||
if(StringFind(trimmed, "AI: ") == 0)
| |||
{
| |||
if(curRole != "")
| |||
{
| |||
int sz = ArraySize(msgRoles);
| |||
ArrayResize(msgRoles, sz + 1);
| |||
ArrayResize(msgContents, sz + 1);
| |||
ArrayResize(msgTimestamps, sz + 1);
| |||
msgRoles[sz] = curRole;
| |||
msgContents[sz] = curContent;
| |||
msgTimestamps[sz] = curTs;
| |||
}
| |||
curRole = "AI";
| |||
curContent = StringSubstr(line, StringFind(line, "AI: ") + 4);
| |||
curTs = "";
| |||
continue;
| |||
}
| |||
//--- Timestamp ends current block
| |||
if(AiIsTimestamp(trimmed))
| |||
{
| |||
curTs = trimmed;
| |||
int sz = ArraySize(msgRoles);
| |||
ArrayResize(msgRoles, sz + 1);
| |||
ArrayResize(msgContents, sz + 1);
| |||
ArrayResize(msgTimestamps, sz + 1);
| |||
msgRoles[sz] = curRole;
| |||
msgContents[sz] = curContent;
| |||
msgTimestamps[sz] = curTs;
| |||
curRole = "";
| |||
curContent = "";
| |||
curTs = "";
| |||
continue;
| |||
}
| |||
//--- Continuation line
| |||
if(curRole != "")
| |||
curContent += "\n" + line;
| |||
}
| |||
//--- Flush trailing block
| |||
if(curRole != "")
| |||
{
| |||
int sz = ArraySize(msgRoles);
| |||
ArrayResize(msgRoles, sz + 1);
| |||
ArrayResize(msgContents, sz + 1);
| |||
ArrayResize(msgTimestamps, sz + 1);
| |||
msgRoles[sz] = curRole;
| |||
msgContents[sz] = curContent;
| |||
msgTimestamps[sz] = curTs;
| |||
}
| |||
//--- Snapshot to cache
| |||
const int sn = ArraySize(msgRoles);
| |||
ArrayResize(s_cacheRoles, sn);
| |||
ArrayResize(s_cacheContents, sn);
| |||
ArrayResize(s_cacheTimestamps, sn);
| |||
for(int si = 0; si < sn; si++)
| |||
{
| |||
s_cacheRoles[si] = msgRoles[si];
| |||
s_cacheContents[si] = msgContents[si];
| |||
s_cacheTimestamps[si] = msgTimestamps[si];
| |||
}
| |||
s_cacheLen = curLen;
| |||
s_cacheCharQ1 = curQ1;
| |||
s_cacheCharQ3 = curQ3;
| |||
}
| |||
//--- Find index of last AI message for action row placement
| |||
const int numMessages = ArraySize(msgRoles);
| |||
int lastAiIdx = -1;
| |||
for(int li = numMessages - 1; li >= 0; li--)
| |||
{
| |||
if(StringFind(msgRoles[li], "AI") < 0)
| |||
continue;
| |||
if(StringFind(msgContents[li], "Thinking") >= 0)
| |||
continue;
| |||
if(StringFind(msgContents[li], "Preparing the Request") >= 0)
| |||
continue;
| |||
lastAiIdx = li;
| |||
break;
| |||
}
| |||
//--- Empty conversation - render placeholder text
| |||
if(numMessages == 0)
| |||
{
| |||
const string empty = "Type your prompt and click Send to chat with the AI.";
| |||
int eW = AiTextWidth(empty, "Arial", AI_FONT_BODY);
| |||
int eH = AiTextHeight("Arial", AI_FONT_BODY);
| |||
AiStampTextAA(g_ai_canvMain, rL + (paneW - eW) / 2, rT + (paneH - eH) / 2,
| |||
empty, "Arial", AI_FONT_BODY, g_ai_subText);
| |||
g_ai_chatScroll.totalH = 0;
| |||
g_ai_chatScroll.viewportH = paneH - 2 * AI_TEXT_PAD;
| |||
AiScrollClamp(g_ai_chatScroll);
| |||
return;
| |||
}
| |||
//--- Setup content rect and bubble layout constants
| |||
const int contentL = rL + AI_TEXT_PAD;
| |||
const int contentT = rT + AI_TEXT_PAD;
| |||
const int contentR = rR - AI_TEXT_PAD;
| |||
const int contentB = rB - AI_TEXT_PAD;
| |||
const int sbW = 4;
| |||
const int sbRightPad = 2;
| |||
const int textRight = contentR - sbW - sbRightPad - 4;
| |||
const int contentW = textRight - contentL;
| |||
const int bubbleMaxW = (int)((double)contentW * 0.78);
| |||
const int bubblePadX = 10;
| |||
const int bubblePadY = 6;
| |||
const int bubbleTextW = bubbleMaxW - 2 * bubblePadX;
| |||
const int lineHBody = AiTextHeight("Arial", AI_FONT_BODY) + 2;
| |||
const int lineHTime = AiTextHeight("Arial", AI_FONT_TIMESTAMP) + 1;
| |||
const int msgGap = 8;
| |||
//--- Per-message wrapped text and bubble dimensions
| |||
string msgWrapped[];
| |||
int msgBubbleW[];
| |||
int msgBubbleH[];
| |||
string msgRespTime[];
| |||
ArrayResize(msgWrapped, numMessages);
| |||
ArrayResize(msgBubbleW, numMessages);
| |||
ArrayResize(msgBubbleH, numMessages);
| |||
ArrayResize(msgRespTime, numMessages);
| |||
//--- Cache the layout since the wrap pass is a pure function of its inputs
| |||
static string s_layWrapped[];
| |||
static int s_layBubbleW[];
| |||
static int s_layBubbleH[];
| |||
static string s_layRespTime[];
| |||
static int s_layLen = -1;
| |||
static int s_layQ1 = -1;
| |||
static int s_layQ3 = -1;
| |||
static int s_layTextW = -1;
| |||
const bool layoutHit = (s_layLen == curLen)
| |||
&& (s_layQ1 == curQ1)
| |||
&& (s_layQ3 == curQ3)
| |||
&& (s_layTextW == bubbleTextW)
| |||
&& (ArraySize(s_layWrapped) == numMessages);
| |||
//--- Layout cache hit - restore wrapped output and skip the wrap pass
| |||
if(layoutHit)
| |||
{
| |||
for(int lm = 0; lm < numMessages; lm++)
| |||
{
| |||
msgWrapped[lm] = s_layWrapped[lm];
| |||
msgBubbleW[lm] = s_layBubbleW[lm];
| |||
msgBubbleH[lm] = s_layBubbleH[lm];
| |||
msgRespTime[lm] = s_layRespTime[lm];
| |||
}
| |||
}
| |||
else
| |||
{
| |||
//--- Wrap each message and compute bubble size
| |||
for(int m = 0; m < numMessages; m++)
| |||
{
| |||
//--- Extract response time note if present
| |||
msgRespTime[m] = "";
| |||
string raw = msgContents[m];
| |||
int rtPos = StringFind(raw, "(Response in ");
| |||
if(rtPos >= 0)
| |||
{
| |||
int rtEnd = StringFind(raw, ")", rtPos);
| |||
if(rtEnd > rtPos)
| |||
{
| |||
msgRespTime[m] = StringSubstr(raw, rtPos, rtEnd - rtPos + 1);
| |||
int strip = rtPos;
| |||
if(strip > 0 && StringGetCharacter(raw, strip - 1) == '\n')
| |||
strip--;
| |||
string before = StringSubstr(raw, 0, strip);
| |||
string after = StringSubstr(raw, rtEnd + 1);
| |||
raw = before + after;
| |||
}
| |||
}
| |||
//--- Skip dot-replacement for loading placeholders
| |||
const bool isLoadingPlaceholder =
| |||
(StringFind(raw, "Preparing the Request") >= 0
| |||
|| StringFind(raw, "Thinking") >= 0);
| |||
string processed = isLoadingPlaceholder
| |||
? raw
| |||
: AiReplaceExactDoublePeriods(raw);
| |||
//--- Markdown line-kind preprocessing for AI bubbles
| |||
const bool isUserMsg = (StringFind(msgRoles[m], "User") >= 0);
| |||
const bool isAiMsg = !isUserMsg && !isLoadingPlaceholder;
| |||
if(isAiMsg)
| |||
{
| |||
string mdLines[];
| |||
StringSplit(processed, '\n', mdLines);
| |||
string rebuilt = "";
| |||
for(int li = 0; li < ArraySize(mdLines); li++)
| |||
{
| |||
string ln = mdLines[li];
| |||
//--- Find leading whitespace count for prefix detection
| |||
int wsCount = 0;
| |||
const int lnLen = StringLen(ln);
| |||
while(wsCount < lnLen)
| |||
{
| |||
ushort c = StringGetCharacter(ln, wsCount);
| |||
if(c == ' ' || c == '\t')
| |||
wsCount++;
| |||
else
| |||
break;
| |||
}
| |||
const string body = StringSubstr(ln, wsCount);
| |||
string sentLine = ln;
| |||
//--- Replace heading prefixes with sentinel chars
| |||
if(StringSubstr(body, 0, 4) == "### ")
| |||
{
| |||
sentLine = ShortToString(0x03) + StringSubstr(body, 4);
| |||
}
| |||
else
| |||
if(StringSubstr(body, 0, 3) == "## ")
| |||
{
| |||
sentLine = ShortToString(0x02) + StringSubstr(body, 3);
| |||
}
| |||
else
| |||
if(StringSubstr(body, 0, 2) == "# ")
| |||
{
| |||
sentLine = ShortToString(0x01) + StringSubstr(body, 2);
| |||
}
| |||
if(li > 0)
| |||
rebuilt += "\n";
| |||
rebuilt += sentLine;
| |||
}
| |||
processed = rebuilt;
| |||
}
| |||
//--- Split into paragraphs and wrap each
| |||
string para[];
| |||
StringSplit(processed, '\n', para);
| |||
string wrapped = "";
| |||
int maxLineW = 0;
| |||
int lineCount = 0;
| |||
int totalLineH = 0;
| |||
//--- Process each paragraph
| |||
for(int ls = 0; ls < ArraySize(para); ls++)
| |||
{
| |||
string p = para[ls];
| |||
//--- Empty paragraph emits blank line
| |||
if(StringLen(p) == 0)
| |||
{
| |||
if(wrapped != "")
| |||
wrapped += "\n";
| |||
lineCount++;
| |||
totalLineH += lineHBody;
| |||
continue;
| |||
}
| |||
//--- Setup default kind and font
| |||
int kind = AI_MD_KIND_BODY;
| |||
string lineFont = "Arial";
| |||
int lineSize = AI_FONT_BODY;
| |||
int lineH = lineHBody;
| |||
int lineIndent = 0;
| |||
string numPrefix = "";
| |||
//--- Detect heading sentinel for AI messages
| |||
if(isAiMsg && StringLen(p) >= 1)
| |||
{
| |||
const ushort sent = StringGetCharacter(p, 0);
| |||
if(sent == 0x01)
| |||
{
| |||
kind = AI_MD_KIND_H1;
| |||
lineFont = "Arial Bold";
| |||
lineSize = 13;
| |||
}
| |||
else
| |||
if(sent == 0x02)
| |||
{
| |||
kind = AI_MD_KIND_H2;
| |||
lineFont = "Arial Bold";
| |||
lineSize = 12;
| |||
}
| |||
else
| |||
if(sent == 0x03)
| |||
{
| |||
kind = AI_MD_KIND_H3;
| |||
lineFont = "Arial Bold";
| |||
lineSize = 11;
| |||
}
| |||
if(kind != AI_MD_KIND_BODY)
| |||
{
| |||
p = StringSubstr(p, 1);
| |||
lineH = AiTextHeightCached(lineFont, lineSize) + 2;
| |||
}
| |||
}
| |||
//--- Detect numbered list prefix
| |||
if(isAiMsg && kind == AI_MD_KIND_BODY)
| |||
{
| |||
const int pl = StringLen(p);
| |||
int dEnd = 0;
| |||
while(dEnd < pl)
| |||
{
| |||
const ushort dc = StringGetCharacter(p, dEnd);
| |||
if(dc >= '0' && dc <= '9')
| |||
dEnd++;
| |||
else
| |||
break;
| |||
}
| |||
if(dEnd > 0
| |||
&& dEnd + 1 < pl
| |||
&& StringGetCharacter(p, dEnd) == '.'
| |||
&& StringGetCharacter(p, dEnd + 1) == ' ')
| |||
{
| |||
numPrefix = StringSubstr(p, 0, dEnd + 2);
| |||
p = StringSubstr(p, dEnd + 2);
| |||
lineIndent = AiTextWidth(numPrefix, "Arial Bold", AI_FONT_BODY);
| |||
kind = AI_MD_KIND_NUMBERED;
| |||
}
| |||
}
| |||
//--- Detect dash or star bullet prefix
| |||
if(isAiMsg && kind == AI_MD_KIND_BODY)
| |||
{
| |||
const int pl4 = StringLen(p);
| |||
if(pl4 >= 2)
| |||
{
| |||
const ushort d0 = StringGetCharacter(p, 0);
| |||
const ushort d1 = StringGetCharacter(p, 1);
| |||
const bool isDashStart = (d0 == '-' && d1 == ' ');
| |||
const bool isStarStart = (d0 == '*' && d1 == ' ');
| |||
if(isDashStart || isStarStart)
| |||
{
| |||
numPrefix = StringSubstr(p, 0, 2);
| |||
p = StringSubstr(p, 2);
| |||
lineIndent = AiTextWidth(numPrefix, "Arial", AI_FONT_BODY);
| |||
kind = AI_MD_KIND_NUMBERED;
| |||
}
| |||
}
| |||
}
| |||
//--- Detect leading whitespace for hanging indent
| |||
string leadingWhitespace = "";
| |||
if(isAiMsg && kind == AI_MD_KIND_BODY)
| |||
{
| |||
int wsCount = 0;
| |||
const int pl3 = StringLen(p);
| |||
while(wsCount < pl3)
| |||
{
| |||
const ushort wc = StringGetCharacter(p, wsCount);
| |||
if(wc == ' ' || wc == '\t')
| |||
wsCount++;
| |||
else
| |||
break;
| |||
}
| |||
if(wsCount > 0 && wsCount < pl3)
| |||
{
| |||
leadingWhitespace = StringSubstr(p, 0, wsCount);
| |||
p = StringSubstr(p, wsCount);
| |||
//--- Snap indent up to numbered-prefix reference width
| |||
const int wsW = AiTextWidth(leadingWhitespace, "Arial", AI_FONT_BODY);
| |||
const int refW = AiTextWidth("3. ", "Arial Bold", AI_FONT_BODY);
| |||
lineIndent = (wsW > refW) ? wsW : refW;
| |||
}
| |||
}
| |||
//--- Compute available width accounting for indent
| |||
const int availW = bubbleTextW - lineIndent;
| |||
int paraLen = StringLen(p);
| |||
int start = 0;
| |||
bool isFirstWrap = true;
| |||
//--- Inline-style state for cross-wrap continuity
| |||
bool openBold = false, openItalic = false;
| |||
//--- Wrap paragraph into visual lines
| |||
while(start < paraLen)
| |||
{
| |||
//--- Find next visual line break point
| |||
string sub = StringSubstr(p, start, paraLen - start);
| |||
int w = AiTextWidth(sub, lineFont, lineSize);
| |||
string visualLine;
| |||
int consume;
| |||
if(w <= availW)
| |||
{
| |||
visualLine = sub;
| |||
consume = paraLen - start;
| |||
}
| |||
else
| |||
{
| |||
//--- Binary search for max prefix that fits
| |||
int lo = 1, hi = paraLen - start, fit = 1;
| |||
while(lo <= hi)
| |||
{
| |||
int mid = (lo + hi) / 2;
| |||
string pre = StringSubstr(p, start, mid);
| |||
int pw = AiTextWidth(pre, lineFont, lineSize);
| |||
if(pw <= availW)
| |||
{
| |||
fit = mid;
| |||
lo = mid + 1;
| |||
}
| |||
else
| |||
{
| |||
hi = mid - 1;
| |||
}
| |||
}
| |||
//--- Prefer breaking at last space
| |||
int breakAt = fit;
| |||
bool foundSp = false;
| |||
for(int k = fit; k >= 1; k--)
| |||
{
| |||
ushort cc = StringGetCharacter(p, start + k - 1);
| |||
if(cc == ' ')
| |||
{
| |||
breakAt = k - 1;
| |||
foundSp = true;
| |||
break;
| |||
}
| |||
}
| |||
if(foundSp)
| |||
{
| |||
visualLine = StringSubstr(p, start, breakAt);
| |||
consume = breakAt + 1;
| |||
}
| |||
else
| |||
{
| |||
visualLine = StringSubstr(p, start, fit);
| |||
consume = fit;
| |||
}
| |||
}
| |||
//--- Track maximum line width including indent
| |||
int vlW = AiTextWidth(visualLine, lineFont, lineSize);
| |||
int effectiveW = vlW + lineIndent;
| |||
if(effectiveW > maxLineW)
| |||
maxLineW = effectiveW;
| |||
//--- Compute markers to reopen styles on continuation
| |||
const string reopenMarkers = isFirstWrap
| |||
? ""
| |||
: AiMdReopenMarkers(openBold, openItalic);
| |||
//--- Compose output line with appropriate sentinels
| |||
string outLine;
| |||
if(kind == AI_MD_KIND_H1
| |||
|| kind == AI_MD_KIND_H2
| |||
|| kind == AI_MD_KIND_H3)
| |||
{
| |||
outLine = ShortToString((ushort)kind) + reopenMarkers + visualLine;
| |||
}
| |||
else
| |||
if(kind == AI_MD_KIND_NUMBERED)
| |||
{
| |||
if(isFirstWrap)
| |||
{
| |||
outLine = numPrefix + reopenMarkers + visualLine;
| |||
}
| |||
else
| |||
{
| |||
outLine = ShortToString(0x06) + IntegerToString(lineIndent)
| |||
+ ":" + reopenMarkers + visualLine;
| |||
}
| |||
}
| |||
else
| |||
{
| |||
//--- Body paragraphs with leading whitespace get indent sentinel
| |||
if(lineIndent > 0)
| |||
{
| |||
outLine = ShortToString(0x06) + IntegerToString(lineIndent)
| |||
+ ":" + reopenMarkers + visualLine;
| |||
}
| |||
else
| |||
{
| |||
outLine = reopenMarkers + visualLine;
| |||
}
| |||
}
| |||
//--- Append to wrapped text
| |||
if(wrapped != "")
| |||
wrapped += "\n";
| |||
wrapped += outLine;
| |||
lineCount++;
| |||
totalLineH += lineH;
| |||
//--- Update inline-style end state for next iteration
| |||
AiMdComputeEndState(visualLine, openBold, openItalic);
| |||
start += consume;
| |||
isFirstWrap = false;
| |||
}
| |||
}
| |||
//--- Floor at one line for empty messages
| |||
if(lineCount == 0)
| |||
{
| |||
lineCount = 1;
| |||
totalLineH = lineHBody;
| |||
}
| |||
//--- Store wrapped text and bubble dimensions
| |||
msgWrapped[m] = wrapped;
| |||
msgBubbleW[m] = MathMin(bubbleMaxW, maxLineW + 2 * bubblePadX);
| |||
msgBubbleH[m] = totalLineH + 2 * bubblePadY;
| |||
}
| |||
//--- Snapshot the wrap output into the layout cache
| |||
ArrayResize(s_layWrapped, numMessages);
| |||
ArrayResize(s_layBubbleW, numMessages);
| |||
ArrayResize(s_layBubbleH, numMessages);
| |||
ArrayResize(s_layRespTime, numMessages);
| |||
for(int sm = 0; sm < numMessages; sm++)
| |||
{
| |||
s_layWrapped[sm] = msgWrapped[sm];
| |||
s_layBubbleW[sm] = msgBubbleW[sm];
| |||
s_layBubbleH[sm] = msgBubbleH[sm];
| |||
s_layRespTime[sm] = msgRespTime[sm];
| |||
}
| |||
s_layLen = curLen;
| |||
s_layQ1 = curQ1;
| |||
s_layQ3 = curQ3;
| |||
s_layTextW = bubbleTextW;
| |||
//--- Close the layout-cache miss block
| |||
}
| |||
//--- Compute total content height
| |||
const int actionRowH = 22;
| |||
int totalH = 0;
| |||
for(int m = 0; m < numMessages; m++)
| |||
{
| |||
totalH += msgBubbleH[m];
| |||
if(StringLen(msgTimestamps[m]) > 0)
| |||
totalH += lineHTime + 2;
| |||
if(m == lastAiIdx)
| |||
totalH += actionRowH + 2;
| |||
totalH += msgGap;
| |||
}
| |||
//--- Update scroll state
| |||
g_ai_chatScroll.totalH = totalH;
| |||
g_ai_chatScroll.viewportH = contentB - contentT;
| |||
if(g_ai_chatScrollPin)
| |||
{
| |||
g_ai_chatScroll.scrollPx = AiScrollMax(g_ai_chatScroll);
| |||
g_ai_chatScrollPin = false;
| |||
}
| |||
AiScrollClamp(g_ai_chatScroll);
| |||
//--- Ensure persistent chat scratch canvas matches main size
| |||
const int cw = g_ai_canvMain.Width();
| |||
const int chC = g_ai_canvMain.Height();
| |||
if(!g_ai_canvChatTmpReady || g_ai_canvChatTmpW < cw || g_ai_canvChatTmpH < chC)
| |||
{
| |||
if(g_ai_canvChatTmpReady)
| |||
g_ai_canvChatTmp.Destroy();
| |||
const int newW = MathMax(cw, g_ai_canvChatTmpW);
| |||
const int newH = MathMax(chC, g_ai_canvChatTmpH);
| |||
if(!g_ai_canvChatTmp.CreateBitmap("AiChatPaneTmpPersistent", 0, 0, newW, newH, COLOR_FORMAT_ARGB_NORMALIZE))
| |||
return;
| |||
g_ai_canvChatTmpW = newW;
| |||
g_ai_canvChatTmpH = newH;
| |||
g_ai_canvChatTmpReady = true;
| |||
}
| |||
//--- Compute clip region and seed scratch canvas
| |||
const int clipL = rL + 1;
| |||
const int clipT = rT + 1;
| |||
const int clipR = rR - 1;
| |||
const int clipB = rB - 1;
| |||
const int seedL = MathMax(clipL, 0);
| |||
const int seedT = MathMax(clipT, 0);
| |||
const int seedR = MathMin(clipR, cw);
| |||
const int seedB = MathMin(clipB, chC);
| |||
//--- Copy current pane pixels to scratch for clip blending
| |||
g_ai_canvChatTmp.CopyRectFromCanvas(g_ai_canvMain, seedL, seedT, seedR, seedB);
| |||
//--- Alias scratch canvas as tmp for the render block
| |||
#define tmp g_ai_canvChatTmp
| |||
//--- Initialize render Y cursor and reset edit-pencil arrays
| |||
int curY = contentT - g_ai_chatScroll.scrollPx;
| |||
ArrayResize(g_ai_userEditRectL, 0);
| |||
ArrayResize(g_ai_userEditRectT, 0);
| |||
ArrayResize(g_ai_userEditRectR, 0);
| |||
ArrayResize(g_ai_userEditRectB, 0);
| |||
ArrayResize(g_ai_userEditClickL, 0);
| |||
ArrayResize(g_ai_userEditClickT, 0);
| |||
ArrayResize(g_ai_userEditClickR, 0);
| |||
ArrayResize(g_ai_userEditClickB, 0);
| |||
ArrayResize(g_ai_userEditPrompt, 0);
| |||
//--- Render each message bubble
| |||
for(int m = 0; m < numMessages; m++)
| |||
{
| |||
//--- Determine role and pick colors
| |||
bool isUser = (StringFind(msgRoles[m], "User") >= 0);
| |||
string processed = msgWrapped[m];
| |||
color bubBg = isUser ? g_ai_userBubbleBg : g_ai_aiBubbleBg;
| |||
color textCol = isUser ? g_ai_userBubbleText : g_ai_aiBubbleText;
| |||
//--- Re-detect placeholder flag and adjust colors
| |||
const bool isLoadingPlaceholder =
| |||
(StringFind(processed, "Preparing the Request") >= 0
| |||
|| StringFind(processed, "Thinking") >= 0);
| |||
if(StringFind(processed, "Preparing the Request") >= 0)
| |||
textCol = g_ai_loadingPrep;
| |||
if(StringFind(processed, "Thinking...") >= 0)
| |||
textCol = g_ai_loadingThink;
| |||
//--- Compute bubble position
| |||
const int bubW = msgBubbleW[m];
| |||
const int bubH = msgBubbleH[m];
| |||
const int bubX = isUser ? (textRight - bubW) : contentL;
| |||
const int bubY = curY;
| |||
//--- Skip rendering for offscreen bubbles
| |||
if(bubY + bubH < contentT || bubY > contentB)
| |||
{
| |||
curY += bubH;
| |||
if(StringLen(msgTimestamps[m]) > 0)
| |||
curY += lineHTime + 2;
| |||
curY += msgGap;
| |||
continue;
| |||
}
| |||
//--- Fill bubble background
| |||
g_ai_prim.FillRoundRectSharp(tmp, bubX, bubY, bubW, bubH, 8,
| |||
ColorToARGB(bubBg, 255));
| |||
//--- Render each visible line within bubble
| |||
string visLines[];
| |||
int nl = StringSplit(processed, '\n', visLines);
| |||
int textY = bubY + bubblePadY;
| |||
for(int i = 0; i < nl; i++)
| |||
{
| |||
string ln = visLines[i];
| |||
//--- User and placeholder bubbles use plain text path
| |||
if(isUser || isLoadingPlaceholder)
| |||
{
| |||
int lW = AiTextWidth(ln, "Arial", AI_FONT_BODY);
| |||
int lX = isUser ? (bubX + bubW - bubblePadX - lW) : (bubX + bubblePadX);
| |||
AiStampTextAA(tmp, lX, textY, ln, "Arial", AI_FONT_BODY, textCol);
| |||
textY += lineHBody;
| |||
continue;
| |||
}
| |||
//--- Detect and strip line-kind sentinel
| |||
int kind = AI_MD_KIND_BODY;
| |||
int lineSize = AI_FONT_BODY;
| |||
int lineH = lineHBody;
| |||
int contIndent = 0;
| |||
string body = ln;
| |||
if(StringLen(body) >= 1)
| |||
{
| |||
const ushort sent = StringGetCharacter(body, 0);
| |||
if(sent == 0x01)
| |||
{
| |||
kind = AI_MD_KIND_H1;
| |||
lineSize = 13;
| |||
body = StringSubstr(body, 1);
| |||
lineH = AiTextHeightCached("Arial Bold", lineSize) + 2;
| |||
}
| |||
else
| |||
if(sent == 0x02)
| |||
{
| |||
kind = AI_MD_KIND_H2;
| |||
lineSize = 12;
| |||
body = StringSubstr(body, 1);
| |||
lineH = AiTextHeightCached("Arial Bold", lineSize) + 2;
| |||
}
| |||
else
| |||
if(sent == 0x03)
| |||
{
| |||
kind = AI_MD_KIND_H3;
| |||
lineSize = 11;
| |||
body = StringSubstr(body, 1);
| |||
lineH = AiTextHeightCached("Arial Bold", lineSize) + 2;
| |||
}
| |||
else
| |||
if(sent == 0x06)
| |||
{
| |||
//--- Numbered continuation - parse indent width
| |||
int colonAt = StringFind(body, ":", 1);
| |||
if(colonAt > 1)
| |||
{
| |||
contIndent = (int)StringToInteger(StringSubstr(body, 1, colonAt - 1));
| |||
body = StringSubstr(body, colonAt + 1);
| |||
kind = AI_MD_KIND_NUMBERED;
| |||
}
| |||
else
| |||
{
| |||
body = StringSubstr(body, 1);
| |||
}
| |||
}
| |||
}
| |||
//--- Detect first-line list prefix (numbered or dash/star)
| |||
string numPrefix = "";
| |||
if(kind == AI_MD_KIND_BODY)
| |||
{
| |||
const int bl = StringLen(body);
| |||
//--- Try numbered first
| |||
int dEnd = 0;
| |||
while(dEnd < bl)
| |||
{
| |||
const ushort dc = StringGetCharacter(body, dEnd);
| |||
if(dc >= '0' && dc <= '9')
| |||
dEnd++;
| |||
else
| |||
break;
| |||
}
| |||
if(dEnd > 0
| |||
&& dEnd + 1 < bl
| |||
&& StringGetCharacter(body, dEnd) == '.'
| |||
&& StringGetCharacter(body, dEnd + 1) == ' ')
| |||
{
| |||
numPrefix = StringSubstr(body, 0, dEnd + 2);
| |||
body = StringSubstr(body, dEnd + 2);
| |||
kind = AI_MD_KIND_NUMBERED;
| |||
}
| |||
//--- Otherwise try dash or star
| |||
else
| |||
if(bl >= 2)
| |||
{
| |||
const ushort r0 = StringGetCharacter(body, 0);
| |||
const ushort r1 = StringGetCharacter(body, 1);
| |||
const bool isDash = (r0 == '-' && r1 == ' ');
| |||
const bool isStar = (r0 == '*' && r1 == ' ');
| |||
if(isDash || isStar)
| |||
{
| |||
numPrefix = StringSubstr(body, 0, 2);
| |||
body = StringSubstr(body, 2);
| |||
kind = AI_MD_KIND_NUMBERED;
| |||
}
| |||
}
| |||
}
| |||
//--- Compute starting X with prefix or continuation indent
| |||
int lX = bubX + bubblePadX;
| |||
if(kind == AI_MD_KIND_NUMBERED)
| |||
{
| |||
if(StringLen(numPrefix) > 0)
| |||
{
| |||
//--- Pick prefix font and color based on type
| |||
const ushort firstCh = StringGetCharacter(numPrefix, 0);
| |||
const bool isNumbered = (firstCh >= '0' && firstCh <= '9');
| |||
const string prefixFont = isNumbered ? "Arial Bold" : "Arial";
| |||
const color prefixCol = isNumbered ? g_ai_accent : textCol;
| |||
AiStampTextAA(tmp, lX, textY, numPrefix, prefixFont,
| |||
AI_FONT_BODY, prefixCol);
| |||
lX += AiTextWidth(numPrefix, prefixFont, AI_FONT_BODY);
| |||
}
| |||
else
| |||
{
| |||
lX += contIndent;
| |||
}
| |||
}
| |||
//--- Parse inline runs and stamp them
| |||
AiMdRun runs[];
| |||
AiMdParseInline(body, runs);
| |||
if(kind == AI_MD_KIND_H1 || kind == AI_MD_KIND_H2 || kind == AI_MD_KIND_H3)
| |||
{
| |||
for(int r = 0; r < ArraySize(runs); r++)
| |||
runs[r].bold = true;
| |||
}
| |||
AiMdStampRuns(tmp, lX, textY, runs, lineSize, textCol);
| |||
textY += lineH;
| |||
}
| |||
//--- Advance past bubble
| |||
curY += bubH;
| |||
//--- Render timestamp if present
| |||
if(StringLen(msgTimestamps[m]) > 0)
| |||
{
| |||
curY += 2;
| |||
int tsW = AiTextWidth(msgTimestamps[m], "Arial", AI_FONT_TIMESTAMP);
| |||
int tX = isUser ? (textRight - tsW) : contentL;
| |||
AiStampTextAA(tmp, tX, curY, msgTimestamps[m],
| |||
"Arial", AI_FONT_TIMESTAMP, g_ai_timestampText);
| |||
curY += lineHTime;
| |||
}
| |||
//--- Edit pencil affordance for user messages
| |||
if(isUser && StringLen(msgContents[m]) > 0)
| |||
{
| |||
//--- Compute pencil rect to left of bubble
| |||
const int peSz = actionRowH;
| |||
const int peGlyph = 14;
| |||
const int peGap = 4;
| |||
const int peR = bubX - peGap;
| |||
const int peL = peR - peSz;
| |||
const int peT = bubY + (bubH - peSz) / 2;
| |||
const int peB = peT + peSz;
| |||
//--- Compute wider hover region covering bubble + pencil
| |||
const int hovL = peL - 2;
| |||
const int hovT = bubY;
| |||
const int hovR = bubX + bubW;
| |||
const int hovB = bubY + bubH;
| |||
//--- Clip hover region to visible chat content area
| |||
const int hovTClip = MathMax(hovT, contentT);
| |||
const int hovBClip = MathMin(hovB, contentB);
| |||
const bool inHoverRegion =
| |||
(g_ai_mouseLx >= hovL && g_ai_mouseLx < hovR
| |||
&& g_ai_mouseLy >= hovTClip && g_ai_mouseLy < hovBClip);
| |||
//--- Register hit rects when pencil has horizontal room
| |||
if(peL >= contentL && hovBClip > hovTClip)
| |||
{
| |||
//--- Append entries to all parallel arrays
| |||
const int peIdx = ArraySize(g_ai_userEditRectL);
| |||
ArrayResize(g_ai_userEditRectL, peIdx + 1);
| |||
ArrayResize(g_ai_userEditRectT, peIdx + 1);
| |||
ArrayResize(g_ai_userEditRectR, peIdx + 1);
| |||
ArrayResize(g_ai_userEditRectB, peIdx + 1);
| |||
ArrayResize(g_ai_userEditClickL, peIdx + 1);
| |||
ArrayResize(g_ai_userEditClickT, peIdx + 1);
| |||
ArrayResize(g_ai_userEditClickR, peIdx + 1);
| |||
ArrayResize(g_ai_userEditClickB, peIdx + 1);
| |||
ArrayResize(g_ai_userEditPrompt, peIdx + 1);
| |||
//--- Wide hover rect
| |||
g_ai_userEditRectL[peIdx] = hovL;
| |||
g_ai_userEditRectT[peIdx] = hovTClip;
| |||
g_ai_userEditRectR[peIdx] = hovR;
| |||
g_ai_userEditRectB[peIdx] = hovBClip;
| |||
//--- Narrow click rect (pencil icon only)
| |||
g_ai_userEditClickL[peIdx] = peL;
| |||
g_ai_userEditClickT[peIdx] = peT;
| |||
g_ai_userEditClickR[peIdx] = peR;
| |||
g_ai_userEditClickB[peIdx] = peB;
| |||
g_ai_userEditPrompt[peIdx] = msgContents[m];
| |||
//--- Paint pencil only when cursor is in hover region
| |||
if(inHoverRegion)
| |||
{
| |||
//--- Detect hover on icon itself for darkened tint
| |||
const bool peHov =
| |||
(g_ai_mouseLx >= peL && g_ai_mouseLx < peR
| |||
&& g_ai_mouseLy >= peT && g_ai_mouseLy < peB);
| |||
color peBg = peHov ? AiDarkenColor(g_ai_userBubbleBg, 0.92)
| |||
: g_ai_userBubbleBg;
| |||
g_ai_prim.FillRoundRectSharp(tmp, peL, peT, peSz, peSz, 4,
| |||
ColorToARGB(peBg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(tmp, peL, peT, peSz, peSz, 4, 1,
| |||
ColorToARGB(AiBorderForBg(peBg), 255));
| |||
const int glyphInset = (peSz - peGlyph) / 2;
| |||
AiDrawIconEdit(tmp, peL + glyphInset, peT + glyphInset,
| |||
peGlyph, ColorToARGB(g_ai_titleText, 230));
| |||
}
| |||
}
| |||
}
| |||
//--- Render action row (regen + export) after last AI message
| |||
if(m == lastAiIdx)
| |||
{
| |||
curY += 2;
| |||
const int icSz = actionRowH;
| |||
const int glyphSz = 14;
| |||
const int icGap = 6;
| |||
//--- Stamp response time text
| |||
int rtX = contentL;
| |||
int rtY = curY + (icSz - AiTextHeightCached("Arial", AI_FONT_TIMESTAMP)) / 2;
| |||
int rtW = 0;
| |||
if(StringLen(msgRespTime[m]) > 0)
| |||
{
| |||
rtW = AiTextWidth(msgRespTime[m], "Arial", AI_FONT_TIMESTAMP);
| |||
AiStampTextAA(tmp, rtX, rtY, msgRespTime[m],
| |||
"Arial", AI_FONT_TIMESTAMP, g_ai_responseTimeNote);
| |||
}
| |||
//--- Compute regen and export button rects
| |||
int afterText = (rtW > 0) ? (rtX + rtW + 8) : contentL;
| |||
int regL = afterText;
| |||
int regT = curY;
| |||
int regR = regL + icSz;
| |||
int regB = regT + icSz;
| |||
int expL = regR + icGap;
| |||
int expT = curY;
| |||
int expR = expL + icSz;
| |||
int expB = expT + icSz;
| |||
//--- Cache rects for hit-testing
| |||
g_ai_regenL = regL;
| |||
g_ai_regenT = regT;
| |||
g_ai_regenR = regR;
| |||
g_ai_regenB = regB;
| |||
g_ai_exportL = expL;
| |||
g_ai_exportT = expT;
| |||
g_ai_exportR = expR;
| |||
g_ai_exportB = expB;
| |||
//--- Pick fill colors by hover state
| |||
color regBg = (g_ai_hover == AI_HOV_REGEN)
| |||
? AiDarkenColor(g_ai_aiBubbleBg, 0.92) : g_ai_aiBubbleBg;
| |||
color expBg = (g_ai_hover == AI_HOV_EXPORT)
| |||
? AiDarkenColor(g_ai_aiBubbleBg, 0.92) : g_ai_aiBubbleBg;
| |||
//--- Fill and border each button
| |||
g_ai_prim.FillRoundRectSharp(tmp, regL, regT, icSz, icSz, 4,
| |||
ColorToARGB(regBg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(tmp, regL, regT, icSz, icSz, 4, 1,
| |||
ColorToARGB(AiBorderForBg(regBg), 255));
| |||
g_ai_prim.FillRoundRectSharp(tmp, expL, expT, icSz, icSz, 4,
| |||
ColorToARGB(expBg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(tmp, expL, expT, icSz, icSz, 4, 1,
| |||
ColorToARGB(AiBorderForBg(expBg), 255));
| |||
//--- Draw button icons
| |||
const int glyphInsetReg = (icSz - glyphSz) / 2;
| |||
AiDrawIconRegen(tmp, regL + glyphInsetReg, regT + glyphInsetReg,
| |||
glyphSz, ColorToARGB(g_ai_titleText, 230));
| |||
const int glyphInsetExp = (icSz - glyphSz) / 2;
| |||
AiDrawIconExport(tmp, expL + glyphInsetExp, expT + glyphInsetExp,
| |||
glyphSz, ColorToARGB(g_ai_titleText, 230));
| |||
curY += icSz;
| |||
}
| |||
//--- Add gap before next message
| |||
curY += msgGap;
| |||
}
| |||
//--- Copy clipped chat content back to main canvas
| |||
tmp.CopyRectToCanvas(g_ai_canvMain, seedL, seedT, seedR, seedB);
| |||
//--- Undefine scratch alias
| |||
#undef tmp
| |||
//--- Draw chat scrollbar when content overflows
| |||
if(AiScrollVisible(g_ai_chatScroll))
| |||
{
| |||
g_ai_chatScroll.trackL = contentR - sbW - sbRightPad;
| |||
g_ai_chatScroll.trackT = contentT;
| |||
g_ai_chatScroll.trackR = g_ai_chatScroll.trackL + sbW;
| |||
g_ai_chatScroll.trackB = contentB;
| |||
AiScrollDraw(g_ai_canvMain, g_ai_chatScroll, g_ai_prim);
| |||
}
| |||
//--- Render scroll-to-bottom FAB
| |||
{
| |||
//--- Compute FAB geometry
| |||
const int fabSize = 30;
| |||
const int fabRadius = fabSize / 2;
| |||
const int fabIconSize = 16;
| |||
const int fabMargin = 8;
| |||
//--- Position centered horizontally near bottom
| |||
const int fabCenterX = (contentL + contentR) / 2;
| |||
const int fabL = fabCenterX - fabSize / 2;
| |||
const int fabB = contentB - fabMargin;
| |||
const int fabT = fabB - fabSize;
| |||
const int fabR = fabL + fabSize;
| |||
//--- Determine visibility from scroll distance
| |||
const int distFromBottom = AiScrollMax(g_ai_chatScroll) - g_ai_chatScroll.scrollPx;
| |||
const int viewportH = contentB - contentT;
| |||
const bool farFromBottom = (distFromBottom > viewportH / 2);
| |||
const bool shouldShow = AiScrollVisible(g_ai_chatScroll) && farFromBottom;
| |||
g_ai_scrollFabVisible = shouldShow;
| |||
g_ai_scrollFabL = fabL;
| |||
g_ai_scrollFabT = fabT;
| |||
g_ai_scrollFabR = fabR;
| |||
g_ai_scrollFabB = fabB;
| |||
//--- Render FAB when visible
| |||
if(shouldShow)
| |||
{
| |||
//--- Pick alpha and bg based on hover
| |||
const bool fabHov = (g_ai_hover == AI_HOV_SCROLL_FAB);
| |||
const uchar fabAlpha = fabHov ? (uchar)255 : (uchar)191;
| |||
color fabBg = fabHov ? AiDarkenColor(g_ai_panelAlt, 0.85)
| |||
: g_ai_panelAlt;
| |||
//--- Fill FAB background and border
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, fabL, fabT,
| |||
fabSize, fabSize, fabRadius,
| |||
ColorToARGB(fabBg, fabAlpha));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, fabL, fabT,
| |||
fabSize, fabSize, fabRadius, 1,
| |||
ColorToARGB(g_ai_borderAccent, fabAlpha));
| |||
//--- Draw centered arrow icon
| |||
const int glyphInset = (fabSize - fabIconSize) / 2;
| |||
const uchar arrowAlpha = (uchar)((230 * fabAlpha) / 255);
| |||
AiDrawIconArrowDown(g_ai_canvMain, fabL + glyphInset, fabT + glyphInset,
| |||
fabIconSize, ColorToARGB(g_ai_titleText, arrowAlpha));
| |||
}
| |||
}
| |||
}
| |||
//+------------------------------------------------------------------+
| |||
//| Render prompt pane and embedded editor |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderPromptPane()
| |||
{
| |||
//--- Get pane rect
| |||
int rL, rT, rR, rB;
| |||
Ai_GetPromptPaneRect(rL, rT, rR, rB);
| |||
//--- Restore the solid dashboard background behind the pane first
| |||
g_ai_canvMain.FillRectangle(rL, rT, rR - 1, rB - 1, ColorToARGB(g_ai_bg, 255));
| |||
//--- Fill background and draw border
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, rL, rT, rR - rL, rB - rT, 6,
| |||
ColorToARGB(g_ai_promptBg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, rL, rT, rR - rL, rB - rT, 6, 1,
| |||
ColorToARGB(g_ai_border, 255));
| |||
//--- Render editor inside padded inner rect
| |||
g_ai_editor.Render(g_ai_canvMain, rL + 4, rT + 4, rR - 4, rB - 4, g_ai_prim);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render prompt pane to overlay canvas (fast-path) |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderPromptPaneOverlay(bool blank = false)
| |||
{
| |||
//--- Bail when overlay does not exist
| |||
if(!g_ai_canvPromptExists)
| |||
return;
| |||
//--- Blank mode - clear overlay so popups underneath show through
| |||
if(blank)
| |||
{
| |||
g_ai_canvPrompt.Erase(0x00000000);
| |||
g_ai_canvPrompt.Update();
| |||
return;
| |||
}
| |||
//--- Get pane rect and dimensions
| |||
int rL, rT, rR, rB;
| |||
Ai_GetPromptPaneRect(rL, rT, rR, rB);
| |||
const int paneW = rR - rL;
| |||
const int paneH = rB - rT;
| |||
//--- Erase overlay to transparent
| |||
g_ai_canvPrompt.Erase(0x00000000);
| |||
//--- Render prompt pane in local coords
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvPrompt, 0, 0, paneW, paneH, 6,
| |||
ColorToARGB(g_ai_promptBg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvPrompt, 0, 0, paneW, paneH, 6, 1,
| |||
ColorToARGB(g_ai_border, 255));
| |||
//--- Render the editor onto the overlay and read back its scrollbar track
| |||
const int svTrackL = g_ai_editor.scroll.trackL;
| |||
const int svTrackT = g_ai_editor.scroll.trackT;
| |||
const int svTrackR = g_ai_editor.scroll.trackR;
| |||
const int svTrackB = g_ai_editor.scroll.trackB;
| |||
g_ai_editor.Render(g_ai_canvPrompt, 4, 4, paneW - 4, paneH - 4, g_ai_prim);
| |||
g_ai_editor.scroll.trackL = svTrackL;
| |||
g_ai_editor.scroll.trackT = svTrackT;
| |||
g_ai_editor.scroll.trackR = svTrackR;
| |||
g_ai_editor.scroll.trackB = svTrackB;
| |||
//--- Push overlay bitmap to chart
| |||
g_ai_canvPrompt.Update();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render footer with split signal button and send button |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderFooter()
| |||
{
| |||
//--- Fill footer background
| |||
g_ai_canvMain.FillRectangle(Ai_MainContentX(), Ai_FooterT(),
| |||
Ai_DashboardW() - 1, Ai_FooterB() - 1,
| |||
ColorToARGB(g_ai_bg, 255));
| |||
//--- Get signal button rect and split position
| |||
int sL, sT, sR, sB;
| |||
Ai_GetSignalBtnRect(sL, sT, sR, sB);
| |||
const int sepX = Ai_SignalSeparatorX();
| |||
const bool hovAct = (g_ai_hover == AI_HOV_SIGNAL);
| |||
const bool hovDd = (g_ai_hover == AI_HOV_SIGNAL_DD);
| |||
//--- Setup styling constants
| |||
const color sigBlue = (color)C'30,100,220';
| |||
const uchar fillIdle = 76;
| |||
const uchar fillHov = 128;
| |||
//--- Fill whole pill at base or action-hover alpha
| |||
{
| |||
const uchar a = hovAct ? fillHov : fillIdle;
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, sL, sT, sR - sL, sB - sT, 4,
| |||
ColorToARGB(sigBlue, a));
| |||
}
| |||
//--- Add hover tint to chevron half when hovered
| |||
if(hovDd && !hovAct)
| |||
{
| |||
const uchar extra = (uchar)(fillHov - fillIdle);
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, sepX, sT, sR - sepX, sB - sT, 4,
| |||
ColorToARGB(sigBlue, extra));
| |||
}
| |||
//--- Lift chevron half back to idle when action half is hovered
| |||
if(hovAct)
| |||
{
| |||
const uchar lift = (uchar)(fillHov - fillIdle);
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, sepX, sT, sR - sepX, sB - sT, 4,
| |||
ColorToARGB(g_ai_bg, lift));
| |||
}
| |||
//--- Draw outer pill border
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, sL, sT, sR - sL, sB - sT, 4, 1,
| |||
ColorToARGB(sigBlue, 255));
| |||
//--- Draw vertical separator between halves
| |||
const uint sepArgb = ColorToARGB(sigBlue, 255);
| |||
for(int yy = sT + 1; yy <= sB - 2; yy++)
| |||
AiBlendPixel(g_ai_canvMain, sepX, yy, sepArgb);
| |||
//--- Render action half content (icon + label)
| |||
const int ddi = MathMax(0, MathMin(g_ai_footerDropdownSelectedIdx, AI_FOOTER_DD_COUNT - 1));
| |||
const string ddLbl = AI_FOOTER_DD_LABELS[ddi];
| |||
const int contentH = sB - sT;
| |||
const int aIcSz = 18;
| |||
const int aIcX = sL + 8;
| |||
const int aIcY = sT + (contentH - aIcSz) / 2;
| |||
const uint aIcArgb = ColorToARGB(sigBlue, 255);
| |||
//--- Draw selected action's icon
| |||
const int aIconId = AI_FOOTER_DD_ICONS[ddi];
| |||
if(aIconId == 0)
| |||
AiDrawIconChart(g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
| |||
else
| |||
if(aIconId == 1)
| |||
AiDrawIconTwinBars(g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
| |||
else
| |||
if(aIconId == 2)
| |||
AiDrawIconLightning(g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
| |||
else
| |||
if(aIconId == 3)
| |||
AiDrawIconDay(g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
| |||
else
| |||
if(aIconId == 4)
| |||
AiDrawIconTrend(g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
| |||
else
| |||
if(aIconId == 5)
| |||
AiDrawIconLevel(g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
| |||
else
| |||
if(aIconId == 6)
| |||
AiDrawIconClose(g_ai_canvMain, aIcX, aIcY, aIcSz, aIcArgb);
| |||
//--- Stamp action label text
| |||
const int sLH = AiTextHeight("Arial Bold", 9);
| |||
AiStampTextAA(g_ai_canvMain, aIcX + aIcSz + 8, sT + (contentH - sLH) / 2,
| |||
ddLbl, "Arial Bold", 9, sigBlue);
| |||
//--- Draw chevron in chevron half
| |||
const int chCx = sepX + (sR - sepX) / 2;
| |||
const int chCy = sT + contentH / 2;
| |||
const uint chevArgb = ColorToARGB(sigBlue, 255);
| |||
AiDrawChevron(g_ai_canvMain, chCx, chCy, g_ai_showFooterDropdown, chevArgb);
| |||
//--- Get send button rect and disabled state
| |||
int xL, xT, xR, xB;
| |||
Ai_GetSendBtnRect(xL, xT, xR, xB);
| |||
const bool sendDisabled = Ai_PromptIsEmpty();
| |||
//--- Track last-rendered send disabled state for fast-path tracking
| |||
g_ai_lastRenderedSendDisabled = sendDisabled;
| |||
//--- Pick send button colors based on disabled and hover state
| |||
color xBg;
| |||
uint xIconArgb;
| |||
if(sendDisabled)
| |||
{
| |||
xBg = g_ai_buttonDisabledBg;
| |||
xIconArgb = ColorToARGB(g_ai_buttonDisabledIcon, 255);
| |||
}
| |||
else
| |||
{
| |||
xBg = (g_ai_hover == AI_HOV_SEND) ? g_ai_buttonSendBgHover : g_ai_buttonSendBg;
| |||
xIconArgb = ColorToARGB(clrWhite, 255);
| |||
}
| |||
//--- Fill and border send button
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, xL, xT, xR - xL, xB - xT, 6,
| |||
ColorToARGB(xBg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, xL, xT, xR - xL, xB - xT, 6, 1,
| |||
ColorToARGB(AiBorderForBg(xBg), 255));
| |||
//--- Draw send icon
| |||
const int sendSz = 18;
| |||
const int sendIcX = xL + (xR - xL - sendSz) / 2;
| |||
const int sendIcY = xT + (xB - xT - sendSz) / 2;
| |||
AiDrawIconSend(g_ai_canvMain, sendIcX, sendIcY, sendSz, xIconArgb);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Popup State |
| |||
//+------------------------------------------------------------------+
| |||
int g_ai_popupL = 0, g_ai_popupT = 0, g_ai_popupR = 0, g_ai_popupB = 0; // Popup outer rect
| |||
int g_ai_popupHovRow = -1; // Hovered row index
| |||
bool g_ai_popupHovDel = false; // Hovered delete zone flag
| |||
int g_ai_popupRowL[]; // Row left edges
| |||
int g_ai_popupRowT[]; // Row top edges
| |||
int g_ai_popupRowR[]; // Row right edges
| |||
int g_ai_popupRowB[]; // Row bottom edges
| |||
int g_ai_popupRowChatIdx[]; // Mapping from row to chat index
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Get popup rectangle for given anchor button |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_GetPopupRect(int anchorIdx, int &outL, int &outT, int &outR, int &outB)
| |||
{
| |||
//--- Search popup spans full main area
| |||
if(anchorIdx == 0)
| |||
{
| |||
g_ai_searchQuery = g_ai_searchEditor.GetText();
| |||
outL = Ai_MainContentX();
| |||
outT = AI_HEADER_H + 1;
| |||
outR = Ai_DashboardW() - 1;
| |||
outB = Ai_DashboardH() - 1;
| |||
return;
| |||
}
| |||
//--- History popup anchored to sidebar button
| |||
int bL, bT, bR, bB;
| |||
Ai_GetSidebarBtnRect(anchorIdx, bL, bT, bR, bB);
| |||
const int popupW = 280;
| |||
const int rowH = 44;
| |||
const int titleArea = 28;
| |||
const int bottomPad = 8;
| |||
//--- Cap visible rows at 8
| |||
int nRows = ArraySize(g_ai_chats);
| |||
if(nRows > 8)
| |||
nRows = 8;
| |||
if(nRows < 1)
| |||
nRows = 1;
| |||
const int popupH = titleArea + nRows * rowH + bottomPad;
| |||
//--- Position popup with bounds clamping
| |||
outL = bR - 3;
| |||
outT = bT;
| |||
const int dH = Ai_DashboardH();
| |||
if(outT + popupH > dH - 8)
| |||
outT = dH - 8 - popupH;
| |||
if(outT < AI_HEADER_H + 4)
| |||
outT = AI_HEADER_H + 4;
| |||
outR = outL + popupW;
| |||
outB = outT + popupH;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Extract first user prompt snippet from chat history |
| |||
//+------------------------------------------------------------------+
| |||
string Ai_FirstPromptSnippet(const string history)
| |||
{
| |||
//--- Bail on empty input
| |||
const int lenH = StringLen(history);
| |||
if(lenH == 0)
| |||
return "";
| |||
//--- Memoize this parse since it is a pure function of the history text
| |||
static string s_snipKey[];
| |||
static string s_snipVal[];
| |||
string fp = (string)lenH + "|"
| |||
+ (string)(int)StringGetCharacter(history, 0) + ","
| |||
+ (string)(int)StringGetCharacter(history, lenH / 2) + ","
| |||
+ (string)(int)StringGetCharacter(history, lenH - 1);
| |||
const int scN = ArraySize(s_snipKey);
| |||
for(int si = 0; si < scN; si++)
| |||
if(s_snipKey[si] == fp)
| |||
return s_snipVal[si];
| |||
//--- Locate first content marker
| |||
int p = StringFind(history, "You: ");
| |||
int prefixLen = 5;
| |||
if(p < 0)
| |||
{
| |||
p = StringFind(history, "AI: ");
| |||
prefixLen = 4;
| |||
if(p < 0)
| |||
return "";
| |||
}
| |||
const int contentStart = p + prefixLen;
| |||
//--- Walk lines collecting prompt content until terminator
| |||
string tail = StringSubstr(history, contentStart);
| |||
string lines[];
| |||
int n = StringSplit(tail, '\n', lines);
| |||
string out = "";
| |||
for(int i = 0; i < n; i++)
| |||
{
| |||
string line = lines[i];
| |||
StringTrimLeft(line);
| |||
StringTrimRight(line);
| |||
//--- Stop on blank, timestamp, or block boundary
| |||
if(StringLen(line) == 0)
| |||
break;
| |||
if(AiIsTimestamp(line))
| |||
break;
| |||
if(StringFind(line, "You: ") == 0)
| |||
break;
| |||
if(StringFind(line, "AI: ") == 0)
| |||
break;
| |||
//--- Append line with separator
| |||
if(StringLen(out) > 0)
| |||
out += " | ";
| |||
out += line;
| |||
}
| |||
//--- Trim, store in memo, and return
| |||
StringTrimLeft(out);
| |||
StringTrimRight(out);
| |||
ArrayResize(s_snipKey, scN + 1);
| |||
ArrayResize(s_snipVal, scN + 1);
| |||
s_snipKey[scN] = fp;
| |||
s_snipVal[scN] = out;
| |||
return out;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Search Box Rect State |
| |||
//+------------------------------------------------------------------+
| |||
int g_ai_popupSearchL = 0, g_ai_popupSearchT = 0; // Search box top-left
| |||
int g_ai_popupSearchR = 0, g_ai_popupSearchB = 0; // Search box bottom-right
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render search or history popup |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderPopup(int anchorIdx)
| |||
{
| |||
//--- Get popup rect and store globally
| |||
int pL, pT, pR, pB;
| |||
Ai_GetPopupRect(anchorIdx, pL, pT, pR, pB);
| |||
g_ai_popupL = pL;
| |||
g_ai_popupT = pT;
| |||
g_ai_popupR = pR;
| |||
g_ai_popupB = pB;
| |||
const int popupW = pR - pL;
| |||
const int popupH = pB - pT;
| |||
//--- Fill popup background (square for search, rounded for history)
| |||
if(anchorIdx == 0)
| |||
{
| |||
g_ai_canvMain.FillRectangle(pL, pT, pR - 1, pB - 1,
| |||
ColorToARGB(g_ai_panelAlt, 255));
| |||
}
| |||
else
| |||
{
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, pL, pT, popupW, popupH, 8,
| |||
ColorToARGB(g_ai_panelAlt, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, pL, pT, popupW, popupH, 8, 1,
| |||
ColorToARGB(g_ai_borderAccent, 255));
| |||
}
| |||
//--- Stamp popup title
| |||
const string title = (anchorIdx == 0) ? "Search Chats" : "Recent Chats";
| |||
AiStampTextAA(g_ai_canvMain, pL + 16, pT + 12, title,
| |||
"Arial Bold", AI_FONT_LABEL, g_ai_titleText);
| |||
//--- Compute layout heights
| |||
const int rowH = 44;
| |||
const int titleArea = (anchorIdx == 0) ? 36 : 28;
| |||
const int searchArea = (anchorIdx == 0) ? 38 : 0;
| |||
//--- Render search input box for search popup
| |||
if(anchorIdx == 0)
| |||
{
| |||
//--- Compute search box rect
| |||
int siL = pL + 16;
| |||
int siT = pT + titleArea;
| |||
int siR = pR - 16;
| |||
int siB = siT + searchArea - 6;
| |||
g_ai_popupSearchL = siL;
| |||
g_ai_popupSearchT = siT;
| |||
g_ai_popupSearchR = siR;
| |||
g_ai_popupSearchB = siB;
| |||
//--- Fill search box and draw border
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, siL, siT, siR - siL, siB - siT, 4,
| |||
ColorToARGB(g_ai_bg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, siL, siT, siR - siL, siB - siT, 4, 1,
| |||
ColorToARGB(g_ai_borderAccent, 255));
| |||
//--- Vertically center the search editor
| |||
const int boxH = siB - siT;
| |||
const int lineH = AiTextHeightCached("Arial", AI_FONT_BODY) + 2;
| |||
const int edL = siL + 8;
| |||
const int edT = siT + (boxH - lineH) / 2 - g_ai_searchEditor.padY;
| |||
const int edR = siR - 4;
| |||
const int edB = edT + lineH + 2 * g_ai_searchEditor.padY;
| |||
g_ai_searchEditor.Render(g_ai_canvMain, edL, edT, edR, edB, g_ai_prim);
| |||
}
| |||
else
| |||
{
| |||
//--- Reset search box rect when not search popup
| |||
g_ai_popupSearchL = g_ai_popupSearchT = g_ai_popupSearchR = g_ai_popupSearchB = 0;
| |||
}
| |||
//--- Build filtered chat list
| |||
int filteredIdx[];
| |||
const int total = ArraySize(g_ai_chats);
| |||
string queryLower = "";
| |||
if(anchorIdx == 0 && StringLen(g_ai_searchQuery) > 0)
| |||
{
| |||
queryLower = g_ai_searchQuery;
| |||
StringToLower(queryLower);
| |||
}
| |||
//--- Filter and lowercase cache (static across renders)
| |||
#define AI_SEARCH_HISTORY_CAP 4096
| |||
static string s_cacheLowerTitles[];
| |||
static string s_cacheLowerHistories[];
| |||
static int s_cacheChatCount = -1;
| |||
static int s_cacheTitleLenSum = -1;
| |||
static int s_cacheHistoryLenSum = -1;
| |||
static string s_cacheFilterQuery = "\x01";
| |||
static int s_cacheFilterIdx[];
| |||
//--- Compute current chats fingerprint
| |||
int curTitleLenSum = 0;
| |||
int curHistoryLenSum = 0;
| |||
for(int fi = 0; fi < total; fi++)
| |||
{
| |||
curTitleLenSum += StringLen(g_ai_chats[fi].title);
| |||
const int hLen = StringLen(g_ai_chats[fi].history);
| |||
curHistoryLenSum += (hLen < AI_SEARCH_HISTORY_CAP) ? hLen : AI_SEARCH_HISTORY_CAP;
| |||
}
| |||
const bool chatsCacheValid =
| |||
(s_cacheChatCount == total)
| |||
&& (s_cacheTitleLenSum == curTitleLenSum)
| |||
&& (s_cacheHistoryLenSum == curHistoryLenSum);
| |||
//--- Rebuild lowercase cache when chats change
| |||
if(!chatsCacheValid)
| |||
{
| |||
ArrayResize(s_cacheLowerTitles, total);
| |||
ArrayResize(s_cacheLowerHistories, total);
| |||
for(int li = 0; li < total; li++)
| |||
{
| |||
//--- Lowercase title
| |||
string tL = g_ai_chats[li].title;
| |||
StringToLower(tL);
| |||
//--- Lowercase capped history head
| |||
string hRaw = g_ai_chats[li].history;
| |||
if(StringLen(hRaw) > AI_SEARCH_HISTORY_CAP)
| |||
hRaw = StringSubstr(hRaw, 0, AI_SEARCH_HISTORY_CAP);
| |||
StringToLower(hRaw);
| |||
//--- Store in cache
| |||
s_cacheLowerTitles[li] = tL;
| |||
s_cacheLowerHistories[li] = hRaw;
| |||
}
| |||
s_cacheChatCount = total;
| |||
s_cacheTitleLenSum = curTitleLenSum;
| |||
s_cacheHistoryLenSum = curHistoryLenSum;
| |||
//--- Invalidate filter cache too
| |||
s_cacheFilterQuery = "\x01";
| |||
}
| |||
//--- Use filter cache when query and chats are unchanged
| |||
if(anchorIdx == 0
| |||
&& chatsCacheValid
| |||
&& s_cacheFilterQuery == g_ai_searchQuery)
| |||
{
| |||
//--- Reuse cached filter result
| |||
const int cn = ArraySize(s_cacheFilterIdx);
| |||
ArrayResize(filteredIdx, cn);
| |||
for(int ci = 0; ci < cn; ci++)
| |||
filteredIdx[ci] = s_cacheFilterIdx[ci];
| |||
}
| |||
else
| |||
{
| |||
//--- Filter chats with title-first short-circuit
| |||
for(int i = total - 1; i >= 0; i--)
| |||
{
| |||
if(StringLen(queryLower) > 0)
| |||
{
| |||
if(StringFind(s_cacheLowerTitles[i], queryLower) >= 0)
| |||
{
| |||
//--- Title hit - include without scanning history
| |||
}
| |||
else
| |||
if(StringFind(s_cacheLowerHistories[i], queryLower) < 0)
| |||
{
| |||
continue;
| |||
}
| |||
}
| |||
//--- Append index to filtered list
| |||
int sz = ArraySize(filteredIdx);
| |||
ArrayResize(filteredIdx, sz + 1);
| |||
filteredIdx[sz] = i;
| |||
}
| |||
//--- Snapshot to filter cache
| |||
if(anchorIdx == 0)
| |||
{
| |||
const int sn = ArraySize(filteredIdx);
| |||
ArrayResize(s_cacheFilterIdx, sn);
| |||
for(int si = 0; si < sn; si++)
| |||
s_cacheFilterIdx[si] = filteredIdx[si];
| |||
s_cacheFilterQuery = g_ai_searchQuery;
| |||
}
| |||
}
| |||
//--- Compute row content area bounds
| |||
const int rowsTop = pT + titleArea + searchArea;
| |||
const int rowsBot = pB - 8;
| |||
const int viewportH = MathMax(0, rowsBot - rowsTop);
| |||
const int totalFiltered = ArraySize(filteredIdx);
| |||
//--- Determine layout based on popup type
| |||
bool useScrollbar = false;
| |||
int visStart = 0;
| |||
int visCount = 0;
| |||
const int sbW = 4;
| |||
//--- Search popup uses virtualized list with scrollbar
| |||
if(anchorIdx == 0)
| |||
{
| |||
const int contentH = totalFiltered * rowH;
| |||
g_ai_searchScroll.totalH = contentH;
| |||
g_ai_searchScroll.viewportH = viewportH;
| |||
if(AiScrollVisible(g_ai_searchScroll))
| |||
useScrollbar = true;
| |||
AiScrollClamp(g_ai_searchScroll);
| |||
//--- Compute first partially visible row and count
| |||
visStart = MathMax(0, g_ai_searchScroll.scrollPx / rowH);
| |||
const int startScreenY = rowsTop + visStart * rowH - g_ai_searchScroll.scrollPx;
| |||
const int yRoom = rowsBot - startScreenY;
| |||
visCount = MathMin(totalFiltered - visStart, (yRoom + rowH - 1) / rowH + 1);
| |||
if(visCount < 0)
| |||
visCount = 0;
| |||
if(visStart + visCount > totalFiltered)
| |||
visCount = totalFiltered - visStart;
| |||
}
| |||
else
| |||
{
| |||
//--- History popup caps at 8 rows
| |||
visStart = 0;
| |||
visCount = MathMin(totalFiltered, 8);
| |||
}
| |||
//--- Allocate row hit-test arrays
| |||
ArrayResize(g_ai_popupRowL, visCount);
| |||
ArrayResize(g_ai_popupRowT, visCount);
| |||
ArrayResize(g_ai_popupRowR, visCount);
| |||
ArrayResize(g_ai_popupRowB, visCount);
| |||
ArrayResize(g_ai_popupRowChatIdx, visCount);
| |||
//--- Prepare clip canvas for search popup virtualization
| |||
const bool useClipCanvas = (anchorIdx == 0 && visCount > 0);
| |||
if(useClipCanvas)
| |||
{
| |||
//--- Ensure search scratch canvas matches main size
| |||
const int needW = g_ai_canvMain.Width();
| |||
const int needH = g_ai_canvMain.Height();
| |||
if(!g_ai_canvSearchTmpReady
| |||
|| g_ai_canvSearchTmpW < needW
| |||
|| g_ai_canvSearchTmpH < needH)
| |||
{
| |||
if(g_ai_canvSearchTmpReady)
| |||
g_ai_canvSearchTmp.Destroy();
| |||
const int newW = MathMax(needW, g_ai_canvSearchTmpW);
| |||
const int newH = MathMax(needH, g_ai_canvSearchTmpH);
| |||
if(g_ai_canvSearchTmp.CreateBitmap("AiSearchPopupTmpPersistent",
| |||
0, 0, newW, newH,
| |||
COLOR_FORMAT_ARGB_NORMALIZE))
| |||
{
| |||
g_ai_canvSearchTmpW = newW;
| |||
g_ai_canvSearchTmpH = newH;
| |||
g_ai_canvSearchTmpReady = true;
| |||
}
| |||
}
| |||
}
| |||
//--- Seed scratch canvas with main pixels for clip blending
| |||
if(useClipCanvas && g_ai_canvSearchTmpReady)
| |||
{
| |||
g_ai_canvSearchTmp.CopyRectFromCanvas(g_ai_canvMain, pL, rowsTop, pR, rowsBot);
| |||
}
| |||
//--- Render each visible row
| |||
for(int v = 0; v < visCount; v++)
| |||
{
| |||
//--- Get chat index for this row
| |||
const int filterPos = visStart + v;
| |||
const int chatIdx = filteredIdx[filterPos];
| |||
//--- Compute row coords with scroll offset
| |||
const int naturalY = rowsTop + filterPos * rowH;
| |||
const int rT = naturalY - ((anchorIdx == 0) ? g_ai_searchScroll.scrollPx : 0);
| |||
const int rB = rT + rowH - 2;
| |||
const int rL = pL + 10;
| |||
const int rR = pR - 10;
| |||
//--- Clip hit rect to visible row band
| |||
const int hitT = MathMax(rT, rowsTop);
| |||
const int hitB = MathMin(rB, rowsBot);
| |||
g_ai_popupRowL[v] = rL;
| |||
g_ai_popupRowT[v] = hitT;
| |||
g_ai_popupRowR[v] = rR;
| |||
g_ai_popupRowB[v] = hitB;
| |||
g_ai_popupRowChatIdx[v] = chatIdx;
| |||
//--- Skip painting offscreen rows
| |||
if(hitB <= hitT)
| |||
continue;
| |||
//--- Determine hover state and pick fill
| |||
bool hov = (g_ai_popupHovRow == v);
| |||
bool hovDel = (hov && g_ai_popupHovDel);
| |||
color bg = hov ? g_ai_chatItemBgHover : g_ai_chatItemBg;
| |||
//--- Render to clip canvas or main directly
| |||
if(useClipCanvas && g_ai_canvSearchTmpReady)
| |||
{
| |||
//--- Fill row and draw border on scratch
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvSearchTmp, rL, rT, rR - rL, rB - rT, 4,
| |||
ColorToARGB(bg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvSearchTmp, rL, rT, rR - rL, rB - rT, 4, 1,
| |||
ColorToARGB(AiBorderForBg(bg), 255));
| |||
//--- Compute available width for text
| |||
const int hpDeleteIconW = 14;
| |||
const int hpAvailW = (rR - rL) - 24 - hpDeleteIconW;
| |||
//--- Stamp title on top line
| |||
string titleText = Ai_FitTextToWidth(g_ai_chats[chatIdx].title,
| |||
"Arial", AI_FONT_LABEL, hpAvailW);
| |||
color titleCol = (g_ai_chats[chatIdx].id == g_ai_currentChatId)
| |||
? g_ai_chatItemActiveText : g_ai_titleText;
| |||
AiStampTextAA(g_ai_canvSearchTmp, rL + 8, rT + 6, titleText,
| |||
"Arial", AI_FONT_LABEL, titleCol);
| |||
//--- Stamp snippet on bottom line
| |||
string snippetRaw = Ai_FirstPromptSnippet(g_ai_chats[chatIdx].history);
| |||
string snippetText = Ai_FitTextToWidth(snippetRaw, "Arial", AI_FONT_SNIPPET, hpAvailW);
| |||
const int snippetY = rT + 6 + AiTextHeightCached("Arial", AI_FONT_LABEL) + 4;
| |||
AiStampTextAA(g_ai_canvSearchTmp, rL + 8, snippetY, snippetText,
| |||
"Arial", AI_FONT_SNIPPET, g_ai_subText);
| |||
//--- Stamp delete glyph on hover
| |||
if(hov)
| |||
{
| |||
color xCol = hovDel ? g_ai_chatItemDelHover : g_ai_subText;
| |||
int xW = 14, xH = 14; // vector X footprint
| |||
AiDrawIconX(g_ai_canvSearchTmp, rR - 8 - xW, rT + ((rB - rT) - xH) / 2, 14,
| |||
ColorToARGB(xCol, 255));
| |||
}
| |||
}
| |||
else
| |||
{
| |||
//--- Render directly to main canvas (history popup path)
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, rL, rT, rR - rL, rB - rT, 4,
| |||
ColorToARGB(bg, 255));
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, rL, rT, rR - rL, rB - rT, 4, 1,
| |||
ColorToARGB(AiBorderForBg(bg), 255));
| |||
//--- Compute available width for text
| |||
const int hp2DeleteIconW = 14;
| |||
const int hp2AvailW = (rR - rL) - 24 - hp2DeleteIconW;
| |||
//--- Stamp title and snippet
| |||
string titleText = Ai_FitTextToWidth(g_ai_chats[chatIdx].title,
| |||
"Arial", AI_FONT_LABEL, hp2AvailW);
| |||
color titleCol = (g_ai_chats[chatIdx].id == g_ai_currentChatId)
| |||
? g_ai_chatItemActiveText : g_ai_titleText;
| |||
AiStampTextAA(g_ai_canvMain, rL + 8, rT + 6, titleText,
| |||
"Arial", AI_FONT_LABEL, titleCol);
| |||
string snippetRaw = Ai_FirstPromptSnippet(g_ai_chats[chatIdx].history);
| |||
string snippetText = Ai_FitTextToWidth(snippetRaw, "Arial", AI_FONT_SNIPPET, hp2AvailW);
| |||
const int snippetY = rT + 6 + AiTextHeightCached("Arial", AI_FONT_LABEL) + 4;
| |||
AiStampTextAA(g_ai_canvMain, rL + 8, snippetY, snippetText,
| |||
"Arial", AI_FONT_SNIPPET, g_ai_subText);
| |||
//--- Stamp delete glyph on hover
| |||
if(hov)
| |||
{
| |||
color xCol = hovDel ? g_ai_chatItemDelHover : g_ai_subText;
| |||
int xW = 14, xH = 14; // vector X footprint (was a Wingdings 2 glyph)
| |||
AiDrawIconX(g_ai_canvMain, rR - 8 - xW, rT + ((rB - rT) - xH) / 2, 14, ColorToARGB(xCol, 255));
| |||
}
| |||
}
| |||
}
| |||
//--- Copy clipped rows back to main canvas
| |||
if(useClipCanvas && g_ai_canvSearchTmpReady)
| |||
{
| |||
g_ai_canvSearchTmp.CopyRectToCanvas(g_ai_canvMain, pL, rowsTop, pR, rowsBot);
| |||
}
| |||
//--- Draw search popup scrollbar when needed
| |||
if(anchorIdx == 0 && useScrollbar)
| |||
{
| |||
g_ai_searchScroll.trackL = pR - 7;
| |||
g_ai_searchScroll.trackT = rowsTop;
| |||
g_ai_searchScroll.trackR = g_ai_searchScroll.trackL + sbW;
| |||
g_ai_searchScroll.trackB = rowsBot;
| |||
AiScrollDraw(g_ai_canvMain, g_ai_searchScroll, g_ai_prim);
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Footer Dropdown State |
| |||
//+------------------------------------------------------------------+
| |||
int g_ai_footerDdL = 0, g_ai_footerDdT = 0, g_ai_footerDdR = 0, g_ai_footerDdB = 0; // Dropdown rect
| |||
int g_ai_footerDdItemL[]; // Item left edges
| |||
int g_ai_footerDdItemT[]; // Item top edges
| |||
int g_ai_footerDdItemR[]; // Item right edges
| |||
int g_ai_footerDdItemB[]; // Item bottom edges
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render footer dropdown popup |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderFooterDropdown()
| |||
{
| |||
//--- Get anchor rect and compute popup geometry
| |||
int aL, aT, aR, aB;
| |||
Ai_GetSignalBtnRect(aL, aT, aR, aB);
| |||
const int itemH = 32;
| |||
const int padTop = 4;
| |||
const int padBot = 4;
| |||
const int popupW = aR - aL;
| |||
const int popupH = padTop + AI_FOOTER_DD_COUNT * itemH + padBot;
| |||
//--- Decide whether to place above or below anchor
| |||
const int dH = Ai_DashboardH();
| |||
bool placeBelow = (aB + 4 + popupH) <= (dH - 6);
| |||
//--- Compute popup rect and store globally
| |||
int pL = aL;
| |||
int pT = placeBelow ? (aB + 4) : (aT - 4 - popupH);
| |||
int pR = pL + popupW;
| |||
int pB = pT + popupH;
| |||
g_ai_footerDdL = pL;
| |||
g_ai_footerDdT = pT;
| |||
g_ai_footerDdR = pR;
| |||
g_ai_footerDdB = pB;
| |||
//--- Fill popup body and border
| |||
const uint bodyArgb = ColorToARGB(g_ai_panelAlt, 200);
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, pL, pT, popupW, popupH, 6, bodyArgb);
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, pL, pT, popupW, popupH, 6, 1,
| |||
ColorToARGB(g_ai_borderAccent, 255));
| |||
//--- Allocate item rect arrays
| |||
ArrayResize(g_ai_footerDdItemL, AI_FOOTER_DD_COUNT);
| |||
ArrayResize(g_ai_footerDdItemT, AI_FOOTER_DD_COUNT);
| |||
ArrayResize(g_ai_footerDdItemR, AI_FOOTER_DD_COUNT);
| |||
ArrayResize(g_ai_footerDdItemB, AI_FOOTER_DD_COUNT);
| |||
//--- Render each dropdown item
| |||
for(int i = 0; i < AI_FOOTER_DD_COUNT; i++)
| |||
{
| |||
//--- Compute and store item rect
| |||
int iL = pL + 1;
| |||
int iT = pT + padTop + i * itemH;
| |||
int iR = pR - 1;
| |||
int iB = iT + itemH;
| |||
g_ai_footerDdItemL[i] = iL;
| |||
g_ai_footerDdItemT[i] = iT;
| |||
g_ai_footerDdItemR[i] = iR;
| |||
g_ai_footerDdItemB[i] = iB;
| |||
//--- Determine hover and active state
| |||
bool itemHov = (g_ai_hover == AI_HOV_FOOTER_DD_ITEM_BASE + i);
| |||
bool itemActive = (i == g_ai_footerDropdownSelectedIdx);
| |||
//--- Paint hover or active highlight
| |||
if(itemHov)
| |||
{
| |||
const uint hovArgb = ColorToARGB(g_ai_titleText, 30);
| |||
for(int yy = iT; yy < iB; yy++)
| |||
for(int xx = iL; xx < iR; xx++)
| |||
AiBlendPixel(g_ai_canvMain, xx, yy, hovArgb);
| |||
}
| |||
else
| |||
if(itemActive)
| |||
{
| |||
const uint actArgb = ColorToARGB(g_ai_titleText, 18);
| |||
for(int yy = iT; yy < iB; yy++)
| |||
for(int xx = iL; xx < iR; xx++)
| |||
AiBlendPixel(g_ai_canvMain, xx, yy, actArgb);
| |||
}
| |||
//--- Draw item icon and label
| |||
const int iconSz = 18;
| |||
const int iconX = iL + 8;
| |||
const int iconY = iT + (itemH - iconSz) / 2;
| |||
const uint iconArgb = ColorToARGB(g_ai_titleText, 235);
| |||
//--- Pick icon by item id
| |||
const int iconId = AI_FOOTER_DD_ICONS[i];
| |||
if(iconId == 0)
| |||
AiDrawIconChart(g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
| |||
else
| |||
if(iconId == 1)
| |||
AiDrawIconTwinBars(g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
| |||
else
| |||
if(iconId == 2)
| |||
AiDrawIconLightning(g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
| |||
else
| |||
if(iconId == 3)
| |||
AiDrawIconDay(g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
| |||
else
| |||
if(iconId == 4)
| |||
AiDrawIconTrend(g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
| |||
else
| |||
if(iconId == 5)
| |||
AiDrawIconLevel(g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
| |||
else
| |||
if(iconId == 6)
| |||
AiDrawIconClose(g_ai_canvMain, iconX, iconY, iconSz, iconArgb);
| |||
//--- Stamp item label
| |||
int lY = iT + (itemH - AiTextHeight("Arial Bold", 9)) / 2;
| |||
AiStampTextAA(g_ai_canvMain, iconX + iconSz + 8, lY,
| |||
AI_FOOTER_DD_LABELS[i], "Arial Bold", 9, g_ai_titleText);
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Set toast notification text and start timer |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_ShowToast(string text, bool isError)
| |||
{
| |||
//--- Store toast text and 5-second expiry
| |||
g_ai_toastText = text;
| |||
g_ai_toastIsError = isError;
| |||
g_ai_toastExpiryMs = GetTickCount64() + 5000;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render toast notification overlay |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderToast()
| |||
{
| |||
//--- Bail when no active toast
| |||
if(StringLen(g_ai_toastText) == 0)
| |||
return;
| |||
const ulong now = GetTickCount64();
| |||
if(now > g_ai_toastExpiryMs)
| |||
return;
| |||
//--- Setup padding and font
| |||
const int padX = 16;
| |||
const int padY = 8;
| |||
const string toastFont = "Arial Bold";
| |||
const int toastSize = 10;
| |||
//--- Measure text dimensions
| |||
const int textW = AiTextWidth(g_ai_toastText, toastFont, toastSize);
| |||
const int textH = AiTextHeight(toastFont, toastSize);
| |||
//--- Setup progress bar geometry
| |||
const int barH = 2;
| |||
const int barGap = 6;
| |||
const int boxW = textW + 2 * padX;
| |||
const int boxH = textH + barGap + barH + 2 * padY;
| |||
//--- Center horizontally below header
| |||
const int mainContentL = Ai_MainContentX();
| |||
const int mainContentR = Ai_DashboardW();
| |||
const int boxL = mainContentL + ((mainContentR - mainContentL) - boxW) / 2;
| |||
const int boxT = AI_HEADER_H + 6;
| |||
//--- Fill toast box
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, boxL, boxT, boxW, boxH, 8,
| |||
ColorToARGB(g_ai_toastBg, 255));
| |||
//--- Draw box border
| |||
g_ai_prim.DrawRoundRectBorderSharp(g_ai_canvMain, boxL, boxT, boxW, boxH, 8, 1,
| |||
ColorToARGB(g_ai_toastBorder, 255));
| |||
//--- Stamp toast text with success/error color
| |||
const color textCol = g_ai_toastIsError ? g_ai_toastError : g_ai_toastSuccess;
| |||
AiStampTextAA(g_ai_canvMain, boxL + padX, boxT + padY,
| |||
g_ai_toastText, toastFont, toastSize, textCol);
| |||
//--- Compute progress bar fill width from remaining lifetime
| |||
const int trackL = boxL + padX;
| |||
const int trackR = boxL + boxW - padX;
| |||
const int trackW = trackR - trackL;
| |||
const int trackY = boxT + padY + textH + barGap;
| |||
const ulong totalLifeMs = 5000;
| |||
const long remaining = (long)g_ai_toastExpiryMs - (long)now;
| |||
double ratio = (double)remaining / (double)totalLifeMs;
| |||
if(ratio < 0.0)
| |||
ratio = 0.0;
| |||
if(ratio > 1.0)
| |||
ratio = 1.0;
| |||
const int fillW = (int)(trackW * ratio);
| |||
const int fillL = trackL + (trackW - fillW) / 2;
| |||
//--- Draw track background
| |||
g_ai_canvMain.FillRectangle(trackL, trackY, trackR - 1, trackY + barH - 1,
| |||
ColorToARGB(g_ai_toastBorder, 255));
| |||
//--- Draw shrinking fill segment
| |||
if(fillW > 0)
| |||
{
| |||
g_ai_canvMain.FillRectangle(fillL, trackY, fillL + fillW - 1, trackY + barH - 1,
| |||
ColorToARGB(textCol, 255));
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render entire dashboard |
| |||
//+------------------------------------------------------------------+
| |||
//+------------------------------------------------------------------+
| |||
//| Snapshot the current canvas as the popup-free backdrop |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_SnapshotBackdrop()
| |||
{
| |||
if(!g_ai_canvMainExists)
| |||
return;
| |||
const int w = g_ai_canvMain.Width();
| |||
const int h = g_ai_canvMain.Height();
| |||
if(w <= 0 || h <= 0)
| |||
return;
| |||
//--- Lazily create / grow the backdrop canvas
| |||
if(!g_ai_canvBackdropExists || g_ai_canvBackdropW < w || g_ai_canvBackdropH < h)
| |||
{
| |||
if(g_ai_canvBackdropExists)
| |||
g_ai_canvBackdrop.Destroy();
| |||
const int nw = MathMax(w, g_ai_canvBackdropW);
| |||
const int nh = MathMax(h, g_ai_canvBackdropH);
| |||
if(!g_ai_canvBackdrop.CreateBitmap("AiBackdrop", 0, 0, nw, nh, COLOR_FORMAT_ARGB_NORMALIZE))
| |||
return;
| |||
g_ai_canvBackdropW = nw;
| |||
g_ai_canvBackdropH = nh;
| |||
g_ai_canvBackdropExists = true;
| |||
}
| |||
//--- Fast block copy of the whole panel (main UI + border, no overlays yet)
| |||
g_ai_canvBackdrop.CopyRectFromCanvas(g_ai_canvMain, 0, 0, w, h);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Clear corner pixels and stroke the outer rounded border |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_DrawPanelBorder(int dW, int dH, bool arcTL = true, bool arcTR = true,
| |||
bool arcBL = true, bool arcBR = true)
| |||
{
| |||
//--- Corner-clear pixel mask cache (tagged by corner 0=TL 1=TR 2=BL 3=BR)
| |||
const int crClear = 10;
| |||
static int s_cornerClearX[];
| |||
static int s_cornerClearY[];
| |||
static int s_cornerClearC[];
| |||
static int s_cornerClearW = -1;
| |||
static int s_cornerClearH = -1;
| |||
//--- Rebuild mask when dashboard size changes
| |||
if(s_cornerClearW != dW || s_cornerClearH != dH)
| |||
{
| |||
const double rdC = (double)crClear;
| |||
ArrayResize(s_cornerClearX, 0);
| |||
ArrayResize(s_cornerClearY, 0);
| |||
ArrayResize(s_cornerClearC, 0);
| |||
int n = 0;
| |||
//--- Walk all four corners and collect outside-radius pixels
| |||
for(int corner = 0; corner < 4; corner++)
| |||
{
| |||
int cxC = (corner == 0 || corner == 2) ? crClear : (dW - 1 - crClear);
| |||
int cyC = (corner == 0 || corner == 1) ? crClear : (dH - 1 - crClear);
| |||
int xMin = (corner == 0 || corner == 2) ? 0 : (dW - crClear - 1);
| |||
int xMax = (corner == 0 || corner == 2) ? crClear : (dW - 1);
| |||
int yMin = (corner == 0 || corner == 1) ? 0 : (dH - crClear - 1);
| |||
int yMax = (corner == 0 || corner == 1) ? crClear : (dH - 1);
| |||
for(int yy = yMin; yy <= yMax; yy++)
| |||
{
| |||
for(int xx = xMin; xx <= xMax; xx++)
| |||
{
| |||
if(xx < 0 || xx >= dW || yy < 0 || yy >= dH)
| |||
continue;
| |||
const double ddx = (double)(xx - cxC);
| |||
const double ddy = (double)(yy - cyC);
| |||
const double dist = MathSqrt(ddx * ddx + ddy * ddy);
| |||
if(dist > rdC + 0.5)
| |||
{
| |||
ArrayResize(s_cornerClearX, n + 1);
| |||
ArrayResize(s_cornerClearY, n + 1);
| |||
ArrayResize(s_cornerClearC, n + 1);
| |||
s_cornerClearX[n] = xx;
| |||
s_cornerClearY[n] = yy;
| |||
s_cornerClearC[n] = corner;
| |||
n++;
| |||
}
| |||
}
| |||
}
| |||
}
| |||
s_cornerClearW = dW;
| |||
s_cornerClearH = dH;
| |||
}
| |||
//--- Clear and re-stroke a corner arc only when its region was repainted, avoiding alpha buildup
| |||
const bool arcFlag[4] = { arcTL, arcTR, arcBL, arcBR };
| |||
const int ccN = ArraySize(s_cornerClearX);
| |||
for(int cci = 0; cci < ccN; cci++)
| |||
if(arcFlag[s_cornerClearC[cci]])
| |||
g_ai_canvMain.PixelSet(s_cornerClearX[cci], s_cornerClearY[cci], 0x00000000);
| |||
//--- Draw straight edges always since solid overwrites do not accumulate
| |||
const uint borderArgb = ColorToARGB(g_ai_borderAccent, 255);
| |||
g_ai_prim.DrawRoundRectBorderObStyle(g_ai_canvMain, 0, 0, dW, dH, 10, borderArgb,
| |||
true, true, true, true,
| |||
arcTL, arcTR, arcBL, arcBR);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Fast hover repaint that re-stamps only changed chrome regions |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderHoverFrame(bool sideDirty, bool headDirty, bool footDirty,
| |||
bool chatDirty, bool promptDirty)
| |||
{
| |||
if(!g_ai_canvMainExists)
| |||
return;
| |||
const int dW = Ai_DashboardW();
| |||
//--- Re-stamp only the chrome regions whose hover actually changed
| |||
if(sideDirty)
| |||
Ai_RenderSidebar();
| |||
if(headDirty)
| |||
Ai_RenderHeader();
| |||
if(footDirty)
| |||
Ai_RenderFooter();
| |||
//--- Redraw dividers cheaply, clipped to the sidebar, header and footer
| |||
g_ai_canvMain.LineHorizontal(0, dW - 1, AI_HEADER_H, ColorToARGB(g_ai_border, 255));
| |||
g_ai_canvMain.LineHorizontal(Ai_MainContentX(), dW - 1, Ai_FooterT() - 1,
| |||
ColorToARGB(g_ai_border, 255));
| |||
//--- Panes only when their own hover-relevant state changed
| |||
if(chatDirty)
| |||
Ai_RenderChatPane();
| |||
if(promptDirty)
| |||
Ai_RenderPromptPane();
| |||
//--- Re-stamp the panel edge, re-stroking corner arcs only for repainted regions
| |||
Ai_DrawPanelBorder(dW, Ai_DashboardH(), headDirty, headDirty, sideDirty, footDirty);
| |||
//--- Push the bitmap
| |||
g_ai_canvMain.Update();
| |||
//--- Keep the prompt overlay canvas in sync only if the prompt changed
| |||
if(promptDirty && g_ai_canvPromptExists)
| |||
Ai_RenderPromptPaneOverlay(false);
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Render the entire dashboard in one full pass |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderAll()
| |||
{
| |||
//--- Bail when main canvas does not exist
| |||
if(!g_ai_canvMainExists)
| |||
return;
| |||
//--- Get dashboard dimensions
| |||
const int dW = Ai_DashboardW();
| |||
const int dH = Ai_DashboardH();
| |||
//--- Erase canvas and fill rounded background
| |||
g_ai_canvMain.Erase(0x00000000);
| |||
g_ai_prim.FillRoundRectSharp(g_ai_canvMain, 0, 0, dW, dH, 10,
| |||
ColorToARGB(g_ai_bg, 255));
| |||
//--- Render sidebar and header
| |||
Ai_RenderSidebar();
| |||
Ai_RenderHeader();
| |||
//--- Draw header divider line
| |||
g_ai_canvMain.LineHorizontal(0, dW - 1, AI_HEADER_H,
| |||
ColorToARGB(g_ai_border, 255));
| |||
//--- Skip main panes when search popup covers them
| |||
if(!g_ai_showSearch)
| |||
{
| |||
Ai_RenderChatPane();
| |||
Ai_RenderPromptPane();
| |||
Ai_RenderFooter();
| |||
//--- Draw footer divider line
| |||
g_ai_canvMain.LineHorizontal(Ai_MainContentX(), dW - 1, Ai_FooterT() - 1,
| |||
ColorToARGB(g_ai_border, 255));
| |||
}
| |||
//--- Render search popup overlay
| |||
if(g_ai_showSearch)
| |||
Ai_RenderPopup(0);
| |||
//--- Clear outside-radius corners and stroke the outer rounded border
| |||
Ai_DrawPanelBorder(dW, dH);
| |||
//--- Render top-level overlay popups after snapshotting the popup-free backdrop
| |||
if(g_ai_showFooterDropdown || g_ai_showSmallHistory)
| |||
Ai_SnapshotBackdrop();
| |||
if(g_ai_showFooterDropdown)
| |||
Ai_RenderFooterDropdown();
| |||
if(g_ai_showSmallHistory)
| |||
Ai_RenderPopup(3);
| |||
//--- Render toast last so it overlays everything
| |||
Ai_RenderToast();
| |||
//--- Push main canvas bitmap to chart
| |||
g_ai_canvMain.Update();
| |||
//--- Sync prompt overlay - blank when popup obscures, full otherwise
| |||
if(g_ai_canvPromptExists)
| |||
{
| |||
const bool anyPopupUp =
| |||
g_ai_showSearch
| |||
|| g_ai_showFooterDropdown
| |||
|| g_ai_showSmallHistory
| |||
|| (StringLen(g_ai_toastText) > 0);
| |||
Ai_RenderPromptPaneOverlay(anyPopupUp);
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Pace interaction frames, capping rasterization near 60fps |
| |||
//+------------------------------------------------------------------+
| |||
#ifndef AI_UI_FRAME_MS
| |||
#define AI_UI_FRAME_MS 16 // ~60fps repaint cap
| |||
#endif
| |||
uint g_ai_lastFrameMs = 0; // Timestamp of last painted frame
| |||
bool g_ai_frameDirty = false; // A capped frame was skipped
| |||
bool g_ai_timerFast = false; // Millisecond timer is in fast mode
| |||
| |||
//--- Repaint only the scrolled pane since a scroll changes one pane
| |||
#define AI_RT_NONE 0
| |||
#define AI_RT_CHAT 1
| |||
#define AI_RT_SEARCH 2
| |||
#define AI_RT_PROMPT 3
| |||
int g_ai_partialTarget = AI_RT_NONE; // Pane to flush on the next timer tick
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Request a repaint, painting now or arming the fast flush timer |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RequestFrame()
| |||
{
| |||
//--- Paint immediately when outside the frame cap window
| |||
const uint nowMs = GetTickCount();
| |||
if(nowMs - g_ai_lastFrameMs >= AI_UI_FRAME_MS)
| |||
{
| |||
g_ai_lastFrameMs = nowMs;
| |||
g_ai_frameDirty = false;
| |||
g_ai_partialTarget = AI_RT_NONE; // full paint covers every pane
| |||
Ai_RenderAll();
| |||
ChartRedraw();
| |||
return;
| |||
}
| |||
//--- Inside the cap - defer to the timer flush
| |||
g_ai_frameDirty = true;
| |||
if(!g_ai_timerFast)
| |||
{
| |||
EventSetMillisecondTimer(AI_UI_FRAME_MS);
| |||
g_ai_timerFast = true;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Re-render a single scrolling pane over the persistent canvas |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderPartial(int target)
| |||
{
| |||
if(!g_ai_canvMainExists)
| |||
return;
| |||
if(target == AI_RT_CHAT)
| |||
Ai_RenderChatPane();
| |||
else
| |||
if(target == AI_RT_SEARCH)
| |||
Ai_RenderPopup(0);
| |||
else
| |||
if(target == AI_RT_PROMPT)
| |||
{
| |||
Ai_RenderPromptPane();
| |||
if(g_ai_canvPromptExists)
| |||
{
| |||
const bool anyPopupUp = g_ai_showSearch || g_ai_showFooterDropdown
| |||
|| g_ai_showSmallHistory || (StringLen(g_ai_toastText) > 0);
| |||
Ai_RenderPromptPaneOverlay(anyPopupUp);
| |||
}
| |||
}
| |||
//--- Re-stamp the panel edge since only the search popup reaches it
| |||
const bool brDirty = (target == AI_RT_SEARCH);
| |||
Ai_DrawPanelBorder(Ai_DashboardW(), Ai_DashboardH(), false, false, false, brDirty);
| |||
//--- Push the (mostly unchanged) bitmap to the chart
| |||
g_ai_canvMain.Update();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Throttled partial repaint for scroll bursts |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RequestPartial(int target)
| |||
{
| |||
//--- Paint immediately when outside the frame cap window
| |||
const uint nowMs = GetTickCount();
| |||
if(nowMs - g_ai_lastFrameMs >= AI_UI_FRAME_MS)
| |||
{
| |||
g_ai_lastFrameMs = nowMs;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
Ai_RenderPartial(target);
| |||
ChartRedraw();
| |||
return;
| |||
}
| |||
//--- Inside the cap - defer this pane to the timer flush
| |||
g_ai_partialTarget = target;
| |||
if(!g_ai_timerFast)
| |||
{
| |||
EventSetMillisecondTimer(AI_UI_FRAME_MS);
| |||
g_ai_timerFast = true;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Repaint only an open top-overlay over the frozen backdrop |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RenderOverlayPartial()
| |||
{
| |||
if(!g_ai_canvMainExists)
| |||
return;
| |||
//--- Force a full render when no popup-free backdrop was captured yet
| |||
if(!g_ai_canvBackdropExists)
| |||
{
| |||
Ai_RenderAll();
| |||
return;
| |||
}
| |||
const int m = 2; // small margin so any 1px AA fringe is covered
| |||
if(g_ai_showFooterDropdown)
| |||
{
| |||
g_ai_canvMain.CopyRectFromCanvas(g_ai_canvBackdrop,
| |||
g_ai_footerDdL - m, g_ai_footerDdT - m,
| |||
g_ai_footerDdR + m, g_ai_footerDdB + m);
| |||
Ai_RenderFooterDropdown();
| |||
}
| |||
if(g_ai_showSmallHistory)
| |||
{
| |||
g_ai_canvMain.CopyRectFromCanvas(g_ai_canvBackdrop,
| |||
g_ai_popupL - m, g_ai_popupT - m,
| |||
g_ai_popupR + m, g_ai_popupB + m);
| |||
Ai_RenderPopup(3);
| |||
}
| |||
g_ai_canvMain.Update();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Throttled overlay repaint with full-flush fallback |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RequestOverlayFrame()
| |||
{
| |||
const uint nowMs = GetTickCount();
| |||
if(nowMs - g_ai_lastFrameMs >= AI_UI_FRAME_MS)
| |||
{
| |||
g_ai_lastFrameMs = nowMs;
| |||
g_ai_frameDirty = false;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
Ai_RenderOverlayPartial();
| |||
ChartRedraw();
| |||
return;
| |||
}
| |||
//--- Skipped by the cap: defer to a full flush (guaranteed correct)
| |||
g_ai_frameDirty = true;
| |||
if(!g_ai_timerFast)
| |||
{
| |||
EventSetMillisecondTimer(AI_UI_FRAME_MS);
| |||
g_ai_timerFast = true;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Throttled fast-hover repaint with full-flush fallback |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_RequestHoverFrame(bool sideDirty, bool headDirty, bool footDirty,
| |||
bool chatDirty, bool promptDirty)
| |||
{
| |||
const uint nowMs = GetTickCount();
| |||
if(nowMs - g_ai_lastFrameMs >= AI_UI_FRAME_MS)
| |||
{
| |||
//--- Absorb any pending scroll pane so an in-flight scroll is not lost
| |||
if(g_ai_partialTarget == AI_RT_CHAT)
| |||
chatDirty = true;
| |||
if(g_ai_partialTarget == AI_RT_PROMPT)
| |||
promptDirty = true;
| |||
g_ai_lastFrameMs = nowMs;
| |||
g_ai_frameDirty = false;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
Ai_RenderHoverFrame(sideDirty, headDirty, footDirty, chatDirty, promptDirty);
| |||
ChartRedraw();
| |||
return;
| |||
}
| |||
//--- Skipped by the cap: defer to a full flush (guaranteed correct)
| |||
g_ai_frameDirty = true;
| |||
if(!g_ai_timerFast)
| |||
{
| |||
EventSetMillisecondTimer(AI_UI_FRAME_MS);
| |||
g_ai_timerFast = true;
| |||
}
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Flush a skipped frame and restore the slow timer after the burst |
| |||
//+------------------------------------------------------------------+
| |||
void Ai_FlushFrame()
| |||
{
| |||
//--- A full-frame request supersedes any pending partial pane
| |||
if(g_ai_frameDirty)
| |||
{
| |||
g_ai_frameDirty = false;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
g_ai_lastFrameMs = GetTickCount();
| |||
Ai_RenderAll();
| |||
ChartRedraw();
| |||
return;
| |||
}
| |||
//--- Otherwise flush the pending scroll pane, if any
| |||
if(g_ai_partialTarget != AI_RT_NONE)
| |||
{
| |||
const int t = g_ai_partialTarget;
| |||
g_ai_partialTarget = AI_RT_NONE;
| |||
g_ai_lastFrameMs = GetTickCount();
| |||
Ai_RenderPartial(t);
| |||
ChartRedraw();
| |||
return;
| |||
}
| |||
//--- Burst over - drop the timer back to its idle cadence
| |||
if(g_ai_timerFast)
| |||
{
| |||
EventSetMillisecondTimer(500);
| |||
g_ai_timerFast = false;
| |||
}
| |||
}
| |||
| |||
#endif // AI_CANVAS_RENDER_MQH
| |||
//+------------------------------------------------------------------+
|