Article-22495-Dispatch-Driv.../AI Canvas Render.mqh

2599 lines
104 KiB
MQL5
Raw Permalink Normal View History

//+------------------------------------------------------------------+
//| 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 |
//+------------------------------------------------------------------+
CCanvas g_ai_canvMain; // Main dashboard canvas
CCanvas g_ai_canvSmallPopup; // Small history popup canvas
CCanvas g_ai_canvBigPopup; // Big popup canvas
CCanvas g_ai_canvSearchPopup; // Search popup canvas
CCanvas g_ai_canvPrompt; // Prompt-pane overlay canvas
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
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 |
//+------------------------------------------------------------------+
uint g_ai_pixHeader[]; // Header image pixels
uint g_ai_pixSidebarBig[]; // Expanded sidebar logo
uint g_ai_pixSidebarSmall[]; // Collapsed sidebar logo
uint g_ai_pixNewChat[]; // New chat icon pixels
uint g_ai_pixClear[]; // Clear icon pixels
uint g_ai_pixHistory[]; // History icon pixels
uint g_ai_pixSearch[]; // Search icon pixels
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
//--- 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 if already loaded
if(g_ai_imagesLoaded) return true;
//--- Load and scale header image
uint w, h;
if(ResourceReadImage("::AI MQL5.bmp", g_ai_pixHeader, w, h))
{
AiScaleImage(g_ai_pixHeader, (int)w, (int)h, 104, 40);
}
else
{
ArrayResize(g_ai_pixHeader, 0);
}
//--- Load and scale logo for both sidebar sizes
uint wL, hL;
uint pixLogoOrig[];
if(ResourceReadImage("::AI LOGO.bmp", pixLogoOrig, wL, hL))
{
ArrayResize(g_ai_pixSidebarBig, ArraySize(pixLogoOrig));
ArrayCopy(g_ai_pixSidebarBig, pixLogoOrig);
AiScaleImage(g_ai_pixSidebarBig, (int)wL, (int)hL, 81, 81);
ArrayResize(g_ai_pixSidebarSmall, ArraySize(pixLogoOrig));
ArrayCopy(g_ai_pixSidebarSmall, pixLogoOrig);
AiScaleImage(g_ai_pixSidebarSmall, (int)wL, (int)hL, 30, 30);
}
//--- Load and scale sidebar action icons
if(ResourceReadImage("::AI NEW CHAT.bmp", g_ai_pixNewChat, w, h))
AiScaleImage(g_ai_pixNewChat, (int)w, (int)h, 22, 22);
if(ResourceReadImage("::AI CLEAR.bmp", g_ai_pixClear, w, h))
AiScaleImage(g_ai_pixClear, (int)w, (int)h, 22, 22);
if(ResourceReadImage("::AI HISTORY.bmp", g_ai_pixHistory, w, h))
AiScaleImage(g_ai_pixHistory, (int)w, (int)h, 22, 22);
if(ResourceReadImage("::AI SEARCH.bmp", g_ai_pixSearch, w, h))
AiScaleImage(g_ai_pixSearch, (int)w, (int)h, 22, 22);
//--- Mark loaded and return
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;
}
//+------------------------------------------------------------------+
//| Pane Y-coordinate Helpers |
//+------------------------------------------------------------------+
int Ai_MainContentX() { return Ai_SidebarW(); } // Main pane left
int Ai_ChatPaneT() { return AI_HEADER_H + AI_PAD; } // Chat pane top
int Ai_ChatPaneB() { return Ai_ChatPaneT() + AI_DISPLAY_H; } // Chat pane bottom
int Ai_PromptPaneT() { return Ai_ChatPaneB() + AI_PAD; } // Prompt pane top
int Ai_PromptPaneB() { return Ai_PromptPaneT() + AI_PROMPT_H; } // Prompt pane bottom
int Ai_FooterT() { return Ai_PromptPaneB() + AI_PAD; } // Footer top
int Ai_FooterB() { return Ai_FooterT() + AI_FOOTER_H; } // Footer bottom
//+------------------------------------------------------------------+
//| 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));
//--- Stamp logo
if(ArraySize(g_ai_pixHeader) > 0)
{
AiStampPixels(g_ai_canvMain, AI_SIDE_PAD, (AI_HEADER_H - 40) / 2,
g_ai_pixHeader, 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);
//--- Stamp current server time centered in main area
string dateStr = TimeToString(TimeTradeServer(), TIME_MINUTES);
int dW2 = AiTextWidth(dateStr, "Arial", AI_FONT_HEADING);
int dateX = Ai_MainContentX() + (AI_MAIN_W - dW2) / 2;
int dateY = (AI_HEADER_H - AiTextHeight("Arial", AI_FONT_HEADING)) / 2;
AiStampTextAA(g_ai_canvMain, dateX, dateY, dateStr, "Arial", AI_FONT_HEADING, g_ai_subText);
//--- 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));
}
//--- Stamp theme glyph
const string themeGlyph = CharToString((uchar)91);
const int themeFs = 16;
int thIcW = AiTextWidth(themeGlyph, "Wingdings", themeFs);
int thIcH = AiTextHeight("Wingdings", themeFs);
AiStampTextAA(g_ai_canvMain, tL + (tR - tL - thIcW) / 2,
tT + (tB - tT - thIcH) / 2,
themeGlyph, "Wingdings", themeFs, g_ai_titleText);
//--- 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);
}
//--- Stamp close glyph
int iconW = AiTextWidth(AI_GLYPH_CLOSE, "Webdings", 14);
int iconH = AiTextHeight("Webdings", 14);
AiStampTextAA(g_ai_canvMain, cL + (cR - cL - iconW) / 2,
cT + (cB - cT - iconH) / 2,
AI_GLYPH_CLOSE, "Webdings", 14, closeFg);
}
//+------------------------------------------------------------------+
//| 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();
if(g_ai_sidebarExpanded)
{
if(ArraySize(g_ai_pixSidebarBig) > 0)
AiStampPixels(g_ai_canvMain, (sw - 81) / 2, AI_HEADER_H + 10,
g_ai_pixSidebarBig, 81, 81);
}
else
{
if(ArraySize(g_ai_pixSidebarSmall) > 0)
AiStampPixels(g_ai_canvMain, (sw - 30) / 2, AI_HEADER_H + 10,
g_ai_pixSidebarSmall, 30, 30);
}
}
//+------------------------------------------------------------------+
//| Render a single sidebar button |
//+------------------------------------------------------------------+
void Ai_RenderSidebarBtn(int idx, color bgIdle, color bgHover, int hovId,
uint &iconPix[], 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;
if(ArraySize(iconPix) > 0)
AiStampPixels(g_ai_canvMain, iconX, iconY, iconPix, 22, 22);
//--- 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 = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 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 = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
int xH = AiTextHeight("Wingdings 2", 14);
AiStampTextAA(g_ai_canvMain, bR - 6 - xW, bT + (bB - bT - xH) / 2,
AI_GLYPH_DELETE, "Wingdings 2", 14, xCol);
}
}
}
//+------------------------------------------------------------------+
//| 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));
//--- Stamp directional glyph
const string glyph = g_ai_sidebarExpanded ? AI_GLYPH_TOGGLE_LEFT : AI_GLYPH_TOGGLE_RIGHT;
int gW = AiTextWidth(glyph, "Webdings", 16);
int gH = AiTextHeight("Webdings", 16);
AiStampTextAA(g_ai_canvMain, bL + (bR - bL - gW) / 2,
bT + (bB - bT - gH) / 2, glyph, "Webdings", 16, g_ai_titleText);
}
//+------------------------------------------------------------------+
//| 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, g_ai_pixSearch, "Search");
Ai_RenderSidebarBtn(1, g_ai_buttonNewChatBg, g_ai_buttonNewChatBgHover,
AI_HOV_NEW_CHAT, g_ai_pixNewChat, "New Chat");
Ai_RenderSidebarBtn(2, g_ai_buttonClearBg, g_ai_buttonClearBgHover,
AI_HOV_CLEAR, g_ai_pixClear, "Clear");
Ai_RenderSidebarBtn(3, g_ai_buttonHistoryBg, g_ai_buttonHistoryBgHover,
AI_HOV_HISTORY, g_ai_pixHistory, "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;
//--- 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);
//--- 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;
}
//--- 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);
//--- 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 editor onto overlay
g_ai_editor.Render(g_ai_canvPrompt, 4, 4, paneW - 4, paneH - 4, g_ai_prim);
//--- 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 "";
//--- 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 and return
StringTrimLeft(out);
StringTrimRight(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 = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 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 = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
int xH = AiTextHeight("Wingdings 2", 14);
AiStampTextAA(g_ai_canvSearchTmp, rR - 8 - xW, rT + ((rB - rT) - xH) / 2,
AI_GLYPH_DELETE, "Wingdings 2", 14, xCol);
}
}
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 = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 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 = AiTextWidth(AI_GLYPH_DELETE, "Wingdings 2", 14);
int xH = AiTextHeight("Wingdings 2", 14);
AiStampTextAA(g_ai_canvMain, rR - 8 - xW, rT + ((rB - rT) - xH) / 2,
AI_GLYPH_DELETE, "Wingdings 2", 14, xCol);
}
}
}
//--- 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 |
//+------------------------------------------------------------------+
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);
//--- Corner-clear pixel mask cache
const int crClear = 10;
static int s_cornerClearX[];
static int s_cornerClearY[];
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);
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);
s_cornerClearX[n] = xx;
s_cornerClearY[n] = yy;
n++;
}
}
}
}
s_cornerClearW = dW;
s_cornerClearH = dH;
}
//--- Apply cached corner mask to clear outside-radius pixels
const int ccN = ArraySize(s_cornerClearX);
for(int cci = 0; cci < ccN; cci++)
g_ai_canvMain.PixelSet(s_cornerClearX[cci], s_cornerClearY[cci], 0x00000000);
//--- Draw outer rounded border
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,
true, true, true, true);
//--- Render top-level overlay popups
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);
}
}
#endif // AI_CANVAS_RENDER_MQH