//+------------------------------------------------------------------+ //| 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 #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