| //+------------------------------------------------------------------+
|
| //| AI Canvas Interact.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_INTERACT_MQH
|
| #define AI_CANVAS_INTERACT_MQH
|
|
|
| //--- Include required modules
|
| #include "AI Canvas State.mqh"
|
| #include "AI Canvas Render.mqh"
|
| #include "AI Canvas Editor.mqh"
|
| #include "AI Logic.mqh"
|
|
|
| //+------------------------------------------------------------------+
|
| //| Global Variables |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_chatScrollDragging = false; // Chat-pane scrollbar drag flag
|
| bool g_ai_editorScrollDragging = false; // Editor scrollbar drag flag
|
| bool g_ai_searchScrollDragging = false; // Search popup scrollbar drag flag
|
|
|
| //--- Full-image viewer: drag-to-pan directly on the image
|
| bool g_ai_fullGrabPan = false;
|
| int g_ai_fullGrabAnchorX = 0, g_ai_fullGrabAnchorY = 0;
|
| int g_ai_fullGrabScroll0X = 0, g_ai_fullGrabScroll0Y = 0;
|
|
|
| //+------------------------------------------------------------------+
|
| //| Handle modal interaction for the full-image viewer in one place |
|
| //+------------------------------------------------------------------+
|
| void Ai_ViewerInteract(int lx, int ly, int mstate, bool freshPress)
|
| {
|
| const bool btnDown = ((mstate & 1) == 1);
|
| //--- Active vertical scrollbar drag
|
| if(g_ai_fullVScroll.dragging)
|
| {
|
| AiScrollUpdateDrag(g_ai_fullVScroll, ly);
|
| if(!btnDown)
|
| AiScrollEndDrag(g_ai_fullVScroll);
|
| Ai_RenderViewerScroll();
|
| return;
|
| }
|
| //--- Active horizontal scrollbar drag
|
| if(g_ai_fullHScroll.dragging)
|
| {
|
| AiScrollUpdateDragH(g_ai_fullHScroll, lx);
|
| if(!btnDown)
|
| AiScrollEndDrag(g_ai_fullHScroll);
|
| Ai_RenderViewerScroll();
|
| return;
|
| }
|
| //--- Active grab-pan on the image
|
| if(g_ai_fullGrabPan)
|
| {
|
| g_ai_fullHScroll.scrollPx = g_ai_fullGrabScroll0X - (lx - g_ai_fullGrabAnchorX);
|
| g_ai_fullVScroll.scrollPx = g_ai_fullGrabScroll0Y - (ly - g_ai_fullGrabAnchorY);
|
| AiScrollClamp(g_ai_fullHScroll);
|
| AiScrollClamp(g_ai_fullVScroll);
|
| if(!btnDown)
|
| g_ai_fullGrabPan = false;
|
| Ai_RenderViewerScroll();
|
| return;
|
| }
|
| //--- Fresh press
|
| if(freshPress)
|
| {
|
| if(AiPointInRect(lx, ly, g_ai_fullCloseL, g_ai_fullCloseT,
|
| g_ai_fullCloseR - g_ai_fullCloseL, g_ai_fullCloseB - g_ai_fullCloseT))
|
| { Ai_CloseImageFull(); return; }
|
| if(AiScrollVisible(g_ai_fullVScroll) && AiScrollHitTestThumb(g_ai_fullVScroll, lx, ly))
|
| { AiScrollBeginDrag(g_ai_fullVScroll, ly); return; }
|
| if(AiScrollVisible(g_ai_fullHScroll) && AiScrollHitTestThumbH(g_ai_fullHScroll, lx, ly))
|
| { AiScrollBeginDragH(g_ai_fullHScroll, lx); return; }
|
| if(AiPointInRect(lx, ly, g_ai_fullVpL, g_ai_fullVpT,
|
| g_ai_fullVpR - g_ai_fullVpL, g_ai_fullVpB - g_ai_fullVpT))
|
| {
|
| g_ai_fullGrabPan = true;
|
| g_ai_fullGrabAnchorX = lx;
|
| g_ai_fullGrabAnchorY = ly;
|
| g_ai_fullGrabScroll0X = g_ai_fullHScroll.scrollPx;
|
| g_ai_fullGrabScroll0Y = g_ai_fullVScroll.scrollPx;
|
| return;
|
| }
|
| Ai_CloseImageFull(); // click on the backdrop closes
|
| return;
|
| }
|
| //--- Pure hover: close button + scrollbar pills
|
| const bool overClose = AiPointInRect(lx, ly, g_ai_fullCloseL, g_ai_fullCloseT,
|
| g_ai_fullCloseR - g_ai_fullCloseL, g_ai_fullCloseB - g_ai_fullCloseT);
|
| const int newHovV = overClose ? AI_HOV_VIEWER_CLOSE : AI_HOV_NONE;
|
| const bool vArea = AiScrollHitTestArea(g_ai_fullVScroll, lx, ly);
|
| const bool vThumb = AiScrollHitTestThumb(g_ai_fullVScroll, lx, ly);
|
| const bool hArea = AiScrollHitTestAreaH(g_ai_fullHScroll, lx, ly);
|
| const bool hThumb = AiScrollHitTestThumbH(g_ai_fullHScroll, lx, ly);
|
| const bool changed = (newHovV != g_ai_hover)
|
| || (vArea != g_ai_fullVScroll.hoveredArea)
|
| || (vThumb != g_ai_fullVScroll.hoveredThumb)
|
| || (hArea != g_ai_fullHScroll.hoveredArea)
|
| || (hThumb != g_ai_fullHScroll.hoveredThumb);
|
| g_ai_hover = newHovV;
|
| g_ai_fullVScroll.hoveredArea = vArea;
|
| g_ai_fullVScroll.hoveredThumb = vThumb;
|
| g_ai_fullHScroll.hoveredArea = hArea;
|
| g_ai_fullHScroll.hoveredThumb = hThumb;
|
| ObjectSetString(0, AI_CANVAS_NAME_MAIN, OBJPROP_TOOLTIP, overClose ? "Close" : "\n");
|
| ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
|
| if(changed)
|
| Ai_RenderViewerScroll();
|
| }
|
|
|
| //--- Forward declarations
|
| void Ai_HideDashboard();
|
| void Ai_RecomputeLayout();
|
|
|
| //+------------------------------------------------------------------+
|
| //| Keyboard Override State |
|
| //+------------------------------------------------------------------+
|
| bool g_ai_kbOverrideActive = false; // Override active flag
|
| bool g_ai_savedKbControl = true; // Saved keyboard control state
|
| bool g_ai_savedQuickNav = true; // Saved quick navigation state
|
|
|
| //+------------------------------------------------------------------+
|
| //| Begin keyboard override to capture chart input |
|
| //+------------------------------------------------------------------+
|
| void Ai_BeginKeyboardOverride()
|
| {
|
| //--- Bail if already active
|
| if(g_ai_kbOverrideActive)
|
| return;
|
| //--- Save current chart settings
|
| g_ai_savedKbControl = (bool)ChartGetInteger(0, CHART_KEYBOARD_CONTROL);
|
| g_ai_savedQuickNav = (bool)ChartGetInteger(0, CHART_QUICK_NAVIGATION);
|
| //--- Disable chart keyboard handling
|
| ChartSetInteger(0, CHART_KEYBOARD_CONTROL, false);
|
| ChartSetInteger(0, CHART_QUICK_NAVIGATION, false);
|
| g_ai_kbOverrideActive = true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| End keyboard override and restore chart input |
|
| //+------------------------------------------------------------------+
|
| void Ai_EndKeyboardOverride()
|
| {
|
| //--- Bail if not active
|
| if(!g_ai_kbOverrideActive)
|
| return;
|
| //--- Restore saved settings
|
| ChartSetInteger(0, CHART_KEYBOARD_CONTROL, g_ai_savedKbControl);
|
| ChartSetInteger(0, CHART_QUICK_NAVIGATION, g_ai_savedQuickNav);
|
| g_ai_kbOverrideActive = false;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Convert chart coordinates to canvas-local coordinates |
|
| //+------------------------------------------------------------------+
|
| void Ai_ChartToCanvas(int chartX, int chartY, int &localX, int &localY)
|
| {
|
| //--- Subtract dashboard origin
|
| localX = chartX - AI_DASHBOARD_X;
|
| localY = chartY - AI_DASHBOARD_Y;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Decide between fast prompt overlay and full render after edit |
|
| //+------------------------------------------------------------------+
|
| void Ai_FastRenderAfterEditorChange()
|
| {
|
| //--- Check if send button visual needs refresh
|
| const bool nowDisabled = Ai_PromptIsEmpty();
|
| const bool sendStateChanged = (nowDisabled != g_ai_lastRenderedSendDisabled);
|
| //--- Check if any overlay is currently showing
|
| const bool hasOverlay =
|
| g_ai_showSearch
|
| || g_ai_showFooterDropdown
|
| || g_ai_showSmallHistory
|
| || (StringLen(g_ai_toastText) > 0);
|
| //--- Choose render path based on what changed
|
| if(sendStateChanged || hasOverlay || !g_ai_canvPromptExists)
|
| {
|
| //--- Slow path: full dashboard render
|
| Ai_RenderAll();
|
| }
|
| else
|
| {
|
| //--- Fast path: prompt overlay only
|
| Ai_RenderPromptPaneOverlay();
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Hit-test canvas coordinates and return hover code |
|
| //+------------------------------------------------------------------+
|
| int Ai_HitTest(int lx, int ly)
|
| {
|
| //--- Test footer dropdown items first
|
| if(g_ai_showFooterDropdown)
|
| {
|
| for(int i = 0; i < ArraySize(g_ai_footerDdItemL); i++)
|
| {
|
| if(AiPointInRect(lx, ly, g_ai_footerDdItemL[i], g_ai_footerDdItemT[i],
|
| g_ai_footerDdItemR[i] - g_ai_footerDdItemL[i],
|
| g_ai_footerDdItemB[i] - g_ai_footerDdItemT[i]))
|
| {
|
| return AI_HOV_FOOTER_DD_ITEM_BASE + i;
|
| }
|
| }
|
| //--- Inside dropdown body but not on item - block fall-through
|
| if(AiPointInRect(lx, ly, g_ai_footerDdL, g_ai_footerDdT,
|
| g_ai_footerDdR - g_ai_footerDdL,
|
| g_ai_footerDdB - g_ai_footerDdT))
|
| return AI_HOV_NONE;
|
| }
|
| //--- Compose attachment chip (remove badge takes priority over thumbnail)
|
| if(g_ai_attachPending)
|
| {
|
| int xl, xt, xr, xb;
|
| if(Ai_GetComposeRemoveRect(xl, xt, xr, xb)
|
| && AiPointInRect(lx, ly, xl, xt, xr - xl, xb - xt))
|
| return AI_HOV_COMPOSE_REMOVE;
|
| int ctl, ctt, ctr, ctb;
|
| if(Ai_GetComposeThumbRect(ctl, ctt, ctr, ctb)
|
| && AiPointInRect(lx, ly, ctl, ctt, ctr - ctl, ctb - ctt))
|
| return AI_HOV_COMPOSE_THUMB;
|
| }
|
| //--- Test search/history popup rows
|
| if(g_ai_showSearch || g_ai_showSmallHistory)
|
| {
|
| const int nRows = ArraySize(g_ai_popupRowL);
|
| for(int r = 0; r < nRows; r++)
|
| {
|
| if(AiPointInRect(lx, ly, g_ai_popupRowL[r], g_ai_popupRowT[r],
|
| g_ai_popupRowR[r] - g_ai_popupRowL[r],
|
| g_ai_popupRowB[r] - g_ai_popupRowT[r]))
|
| {
|
| //--- Right edge of row is the delete zone
|
| const int delZoneL = g_ai_popupRowR[r] - 24;
|
| if(lx >= delZoneL)
|
| {
|
| g_ai_popupHovRow = r;
|
| g_ai_popupHovDel = true;
|
| return AI_HOV_SMALL_DEL_BASE + r;
|
| }
|
| g_ai_popupHovRow = r;
|
| g_ai_popupHovDel = false;
|
| return AI_HOV_SMALL_CHAT_BASE + r;
|
| }
|
| }
|
| //--- Inside popup body but not on row - block fall-through
|
| if(AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
|
| g_ai_popupR - g_ai_popupL, g_ai_popupB - g_ai_popupT))
|
| {
|
| g_ai_popupHovRow = -1;
|
| g_ai_popupHovDel = false;
|
| return AI_HOV_NONE;
|
| }
|
| }
|
| //--- Reset popup hover state when no popup hit
|
| g_ai_popupHovRow = -1;
|
| g_ai_popupHovDel = false;
|
| //--- Test theme button
|
| int tL, tT, tR, tB;
|
| Ai_GetThemeBtnRect(tL, tT, tR, tB);
|
| if(AiPointInRect(lx, ly, tL, tT, tR - tL, tB - tT))
|
| return AI_HOV_THEME;
|
| //--- Test close button
|
| int cL, cT, cR, cB;
|
| Ai_GetCloseBtnRect(cL, cT, cR, cB);
|
| if(AiPointInRect(lx, ly, cL, cT, cR - cL, cB - cT))
|
| return AI_HOV_CLOSE;
|
| //--- Test sidebar buttons
|
| int bL, bT, bR, bB;
|
| for(int i = 0; i < 4; i++)
|
| {
|
| Ai_GetSidebarBtnRect(i, bL, bT, bR, bB);
|
| if(AiPointInRect(lx, ly, bL, bT, bR - bL, bB - bT))
|
| {
|
| switch(i)
|
| {
|
| case 0:
|
| return AI_HOV_SEARCH;
|
| case 1:
|
| return AI_HOV_NEW_CHAT;
|
| case 2:
|
| return AI_HOV_CLEAR;
|
| case 3:
|
| return AI_HOV_HISTORY;
|
| }
|
| }
|
| }
|
| //--- Test sidebar chat rows when expanded
|
| if(g_ai_sidebarExpanded)
|
| {
|
| int n = MathMin(7, ArraySize(g_ai_chats));
|
| for(int r = 0; r < n; r++)
|
| {
|
| int rL, rT, rR, rB;
|
| Ai_GetSidebarChatRowRect(r, rL, rT, rR, rB);
|
| if(AiPointInRect(lx, ly, rL, rT, rR - rL, rB - rT))
|
| {
|
| //--- Right edge is delete zone
|
| if(lx >= rR - 22)
|
| return AI_HOV_SIDE_DEL_BASE + r;
|
| return AI_HOV_SIDE_CHAT_BASE + r;
|
| }
|
| }
|
| }
|
| //--- Test sidebar toggle button
|
| int tgL, tgT, tgR, tgB;
|
| Ai_GetToggleBtnRect(tgL, tgT, tgR, tgB);
|
| if(AiPointInRect(lx, ly, tgL, tgT, tgR - tgL, tgB - tgT))
|
| return AI_HOV_TOGGLE;
|
| //--- Test signal button (split into action half and chevron half)
|
| int sgL, sgT, sgR, sgB;
|
| Ai_GetSignalBtnRect(sgL, sgT, sgR, sgB);
|
| if(AiPointInRect(lx, ly, sgL, sgT, sgR - sgL, sgB - sgT))
|
| {
|
| const int sepX = Ai_SignalSeparatorX();
|
| if(lx >= sepX)
|
| return AI_HOV_SIGNAL_DD;
|
| return AI_HOV_SIGNAL;
|
| }
|
| //--- Test send button
|
| int snL, snT, snR, snB;
|
| Ai_GetSendBtnRect(snL, snT, snR, snB);
|
| if(AiPointInRect(lx, ly, snL, snT, snR - snL, snB - snT))
|
| return AI_HOV_SEND;
|
| //--- Test regenerate button
|
| if(g_ai_regenR > g_ai_regenL
|
| && AiPointInRect(lx, ly, g_ai_regenL, g_ai_regenT,
|
| g_ai_regenR - g_ai_regenL, g_ai_regenB - g_ai_regenT))
|
| return AI_HOV_REGEN;
|
| //--- Test export button
|
| if(g_ai_exportR > g_ai_exportL
|
| && AiPointInRect(lx, ly, g_ai_exportL, g_ai_exportT,
|
| g_ai_exportR - g_ai_exportL, g_ai_exportB - g_ai_exportT))
|
| return AI_HOV_EXPORT;
|
| //--- Test approval approve button
|
| if(g_ai_apprR > g_ai_apprL
|
| && AiPointInRect(lx, ly, g_ai_apprL, g_ai_apprT,
|
| g_ai_apprR - g_ai_apprL, g_ai_apprB - g_ai_apprT))
|
| return AI_HOV_ACTION_APPROVE;
|
| //--- Test approval reject button
|
| if(g_ai_rejR > g_ai_rejL
|
| && AiPointInRect(lx, ly, g_ai_rejL, g_ai_rejT,
|
| g_ai_rejR - g_ai_rejL, g_ai_rejB - g_ai_rejT))
|
| return AI_HOV_ACTION_REJECT;
|
| //--- Test scroll-to-bottom FAB
|
| if(g_ai_scrollFabVisible
|
| && AiPointInRect(lx, ly, g_ai_scrollFabL, g_ai_scrollFabT,
|
| g_ai_scrollFabR - g_ai_scrollFabL,
|
| g_ai_scrollFabB - g_ai_scrollFabT))
|
| {
|
| return AI_HOV_SCROLL_FAB;
|
| }
|
| //--- Test inline chat image thumbnails first so they take priority over the edit pencil
|
| if(!g_ai_showSearch && !g_ai_showSmallHistory && !g_ai_showFooterDropdown)
|
| {
|
| const int chatImgN = ArraySize(g_ai_imgClickBase);
|
| for(int ciI = 0; ciI < chatImgN; ciI++)
|
| {
|
| if(AiPointInRect(lx, ly, g_ai_imgClickL[ciI], g_ai_imgClickT[ciI],
|
| g_ai_imgClickR[ciI] - g_ai_imgClickL[ciI],
|
| g_ai_imgClickB[ciI] - g_ai_imgClickT[ciI]))
|
| {
|
| return AI_HOV_CHAT_IMAGE_BASE + ciI;
|
| }
|
| }
|
| }
|
| //--- Test per-user-message edit pencils
|
| const int userEditN = ArraySize(g_ai_userEditRectL);
|
| for(int ueI = 0; ueI < userEditN; ueI++)
|
| {
|
| if(g_ai_userEditRectR[ueI] > g_ai_userEditRectL[ueI]
|
| && AiPointInRect(lx, ly,
|
| g_ai_userEditRectL[ueI], g_ai_userEditRectT[ueI],
|
| g_ai_userEditRectR[ueI] - g_ai_userEditRectL[ueI],
|
| g_ai_userEditRectB[ueI] - g_ai_userEditRectT[ueI]))
|
| {
|
| return AI_HOV_USER_EDIT_BASE + ueI;
|
| }
|
| }
|
| //--- Test header drag region
|
| if(ly >= 0 && ly < AI_HEADER_H)
|
| return AI_HOV_DRAG;
|
| //--- No hit
|
| return AI_HOV_NONE;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Test if point is inside prompt pane |
|
| //+------------------------------------------------------------------+
|
| bool Ai_PointInPromptPane(int lx, int ly)
|
| {
|
| //--- Get prompt pane rect and test
|
| int pL, pT, pR, pB;
|
| Ai_GetPromptPaneRect(pL, pT, pR, pB);
|
| return AiPointInRect(lx, ly, pL, pT, pR - pL, pB - pT);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Test if point is inside chat pane |
|
| //+------------------------------------------------------------------+
|
| bool Ai_PointInChatPane(int lx, int ly)
|
| {
|
| //--- Get chat pane rect and test
|
| int cL, cT, cR, cB;
|
| Ai_GetChatPaneRect(cL, cT, cR, cB);
|
| return AiPointInRect(lx, ly, cL, cT, cR - cL, cB - cT);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Dispatch footer dropdown action by id |
|
| //+------------------------------------------------------------------+
|
| void Ai_DispatchFooterAction(int actionId)
|
| {
|
| //--- Route to handler based on action id
|
| switch(actionId)
|
| {
|
| case 0:
|
| AiGetAndAppendChartData();
|
| break; // Get Chart Data
|
| case 7:
|
| Ai_AttachChartScreenshot();
|
| break; // Attach Chart Screenshot
|
| case 1:
|
| AiTwinBars();
|
| break; // Twin Bars
|
| case 2:
|
| AiGetTradeSignal(false);
|
| break; // Quick Scalp
|
| case 3:
|
| AiDailySignal();
|
| break; // Daily Signal
|
| case 4:
|
| AiTrendRead();
|
| break; // Trend Read
|
| case 5:
|
| AiKeyLevel();
|
| break; // Key Level
|
| case 6:
|
| AiClearSignalDrawings();
|
| break; // Clear Drawings
|
| default:
|
| //--- Unknown action - log and ignore
|
| Print("Ai_DispatchFooterAction: unknown actionId=", actionId);
|
| break;
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Handle action triggered by hover code |
|
| //+------------------------------------------------------------------+
|
| void Ai_HandleAction(int hov)
|
| {
|
| //--- Close dashboard
|
| if(hov == AI_HOV_CLOSE)
|
| {
|
| Ai_HideDashboard();
|
| return;
|
| }
|
| //--- Toggle theme
|
| if(hov == AI_HOV_THEME)
|
| {
|
| Ai_ApplyTheme(!g_ai_darkTheme);
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Toggle sidebar expansion
|
| if(hov == AI_HOV_TOGGLE)
|
| {
|
| g_ai_sidebarExpanded = !g_ai_sidebarExpanded;
|
| Ai_RecomputeLayout();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Create new chat
|
| if(hov == AI_HOV_NEW_CHAT)
|
| {
|
| g_ai_showSearch = false;
|
| g_ai_showSmallHistory = false;
|
| AiCreateNewChat();
|
| return;
|
| }
|
| //--- Clear current chat
|
| if(hov == AI_HOV_CLEAR)
|
| {
|
| //--- Skip if already empty
|
| if(StringLen(g_ai_conversationHistory) == 0
|
| && StringLen(g_ai_editor.GetText()) == 0)
|
| {
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Snapshot title for toast before clearing
|
| string titleSnap = g_ai_currentTitle;
|
| if(StringLen(titleSnap) > 30)
|
| titleSnap = StringSubstr(titleSnap, 0, 27) + "...";
|
| //--- Delete this chat's screenshots from disk, then clear history/prompt
|
| Ai_DeleteChatImages(g_ai_conversationHistory);
|
| g_ai_conversationHistory = "";
|
| g_ai_currentPrompt = "";
|
| g_ai_editor.SetText("");
|
| //--- Persist and notify
|
| const bool savedOk = AiUpdateCurrentHistory();
|
| if(savedOk)
|
| Ai_ShowToast("Successfully cleared chat '" + titleSnap + "'", false);
|
| else
|
| Ai_ShowToast("Failed to clear chat", true);
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Toggle history popup
|
| if(hov == AI_HOV_HISTORY)
|
| {
|
| g_ai_showSmallHistory = !g_ai_showSmallHistory;
|
| g_ai_showSearch = false;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Toggle search popup
|
| if(hov == AI_HOV_SEARCH)
|
| {
|
| g_ai_showSearch = !g_ai_showSearch;
|
| g_ai_showSmallHistory = false;
|
| if(g_ai_showSearch)
|
| {
|
| //--- Focus search editor and reset state
|
| if(g_ai_editor.focused)
|
| g_ai_editor.focused = false;
|
| g_ai_searchEditor.focused = true;
|
| g_ai_searchEditor.SetText("");
|
| g_ai_searchQuery = "";
|
| g_ai_searchScroll.scrollPx = 0;
|
| Ai_BeginKeyboardOverride();
|
| }
|
| else
|
| {
|
| //--- Unfocus search editor and end override if no other focus
|
| g_ai_searchEditor.focused = false;
|
| if(!g_ai_editor.focused)
|
| Ai_EndKeyboardOverride();
|
| }
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Signal button action half - fire selected dropdown action
|
| if(hov == AI_HOV_SIGNAL)
|
| {
|
| g_ai_showFooterDropdown = false;
|
| g_ai_showSearch = false;
|
| g_ai_showSmallHistory = false;
|
| const int ddi = MathMax(0, MathMin(g_ai_footerDropdownSelectedIdx, AI_FOOTER_DD_COUNT - 1));
|
| Ai_DispatchFooterAction(AI_FOOTER_DD_ACTION_IDS[ddi]);
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Signal button chevron half - toggle dropdown
|
| if(hov == AI_HOV_SIGNAL_DD)
|
| {
|
| g_ai_showFooterDropdown = !g_ai_showFooterDropdown;
|
| g_ai_showSearch = false;
|
| g_ai_showSmallHistory = false;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Footer dropdown item selection
|
| if(hov >= AI_HOV_FOOTER_DD_ITEM_BASE && hov < AI_HOV_FOOTER_DD_ITEM_BASE + 100)
|
| {
|
| int item = hov - AI_HOV_FOOTER_DD_ITEM_BASE;
|
| g_ai_footerDropdownSelectedIdx = item;
|
| g_ai_showFooterDropdown = false;
|
| if(item >= 0 && item < AI_FOOTER_DD_COUNT)
|
| {
|
| Ai_DispatchFooterAction(AI_FOOTER_DD_ACTION_IDS[item]);
|
| }
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Send button - submit prompt
|
| if(hov == AI_HOV_SEND)
|
| {
|
| //--- Suppress empty submissions
|
| if(Ai_PromptIsEmpty())
|
| return;
|
| string txt = g_ai_editor.GetText();
|
| g_ai_editor.SetText("");
|
| g_ai_currentPrompt = "";
|
| AiSubmitMessage(txt);
|
| return;
|
| }
|
| //--- Approve button - execute the pending AI action
|
| if(hov == AI_HOV_ACTION_APPROVE)
|
| {
|
| AiResolvePendingAction(true);
|
| return;
|
| }
|
| //--- Reject button - refuse the pending AI action
|
| if(hov == AI_HOV_ACTION_REJECT)
|
| {
|
| AiResolvePendingAction(false);
|
| return;
|
| }
|
| //--- Regenerate button - resubmit last prompt
|
| if(hov == AI_HOV_REGEN)
|
| {
|
| //--- Capture last prompt then strip the turn pair and resubmit
|
| string lastPrompt = AiGetLastUserPrompt();
|
| if(StringLen(lastPrompt) > 0)
|
| {
|
| AiRemoveLastConversationTurn();
|
| AiSubmitMessage(lastPrompt);
|
| }
|
| return;
|
| }
|
| //--- Export button - write chat to file
|
| if(hov == AI_HOV_EXPORT)
|
| {
|
| string fname = AI_EXPORT_DIR + "ChatGPT_Export_Chat" + IntegerToString(g_ai_currentChatId) + ".txt";
|
| int h = FileOpen(fname, FILE_WRITE | FILE_TXT | FILE_ANSI);
|
| if(h != INVALID_HANDLE)
|
| {
|
| FileWriteString(h, "Title: " + g_ai_currentTitle + "\r\n\r\n");
|
| FileWriteString(h, g_ai_conversationHistory);
|
| FileClose(h);
|
| Print("Exported chat to ", fname);
|
| Ai_ShowToast("Chat exported to " + fname, false);
|
| }
|
| else
|
| {
|
| const int err = GetLastError();
|
| Print("Export failed: ", err);
|
| Ai_ShowToast("Export failed (error " + IntegerToString(err) + ")", true);
|
| }
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Scroll-to-bottom FAB - jump chat scroll to max
|
| if(hov == AI_HOV_SCROLL_FAB)
|
| {
|
| g_ai_chatScroll.scrollPx = AiScrollMax(g_ai_chatScroll);
|
| AiScrollClamp(g_ai_chatScroll);
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- User-message edit pencil - load prompt into editor
|
| if(hov >= AI_HOV_USER_EDIT_BASE
|
| && hov < AI_HOV_USER_EDIT_BASE + 100)
|
| {
|
| const int peIdx = hov - AI_HOV_USER_EDIT_BASE;
|
| if(peIdx >= 0 && peIdx < ArraySize(g_ai_userEditPrompt))
|
| {
|
| //--- Validate click landed in narrow pencil rect not just bubble
|
| const bool inClickRect =
|
| (peIdx < ArraySize(g_ai_userEditClickL))
|
| && (g_ai_mouseLx >= g_ai_userEditClickL[peIdx])
|
| && (g_ai_mouseLx < g_ai_userEditClickR[peIdx])
|
| && (g_ai_mouseLy >= g_ai_userEditClickT[peIdx])
|
| && (g_ai_mouseLy < g_ai_userEditClickB[peIdx]);
|
| if(!inClickRect)
|
| return;
|
| //--- Load prompt into editor
|
| const string prompt = g_ai_userEditPrompt[peIdx];
|
| g_ai_editor.SetText(prompt);
|
| //--- Focus editor and start keyboard override
|
| if(g_ai_searchEditor.focused)
|
| g_ai_searchEditor.focused = false;
|
| if(!g_ai_editor.focused)
|
| {
|
| g_ai_editor.focused = true;
|
| Ai_BeginKeyboardOverride();
|
| }
|
| //--- Sync currentPrompt for chat-store consistency
|
| g_ai_currentPrompt = prompt;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| return;
|
| }
|
| //--- Sidebar chat row click - switch active chat
|
| if(hov >= AI_HOV_SIDE_CHAT_BASE && hov < AI_HOV_SIDE_DEL_BASE)
|
| {
|
| int row = hov - AI_HOV_SIDE_CHAT_BASE;
|
| int total = ArraySize(g_ai_chats);
|
| int chatIdx = total - 1 - row;
|
| if(chatIdx >= 0 && chatIdx < total && g_ai_chats[chatIdx].id != g_ai_currentChatId)
|
| {
|
| AiUpdateCurrentHistory();
|
| g_ai_currentChatId = g_ai_chats[chatIdx].id;
|
| g_ai_currentTitle = g_ai_chats[chatIdx].title;
|
| g_ai_conversationHistory = g_ai_chats[chatIdx].history;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| return;
|
| }
|
| //--- Sidebar delete button - remove chat
|
| if(hov >= AI_HOV_SIDE_DEL_BASE && hov < AI_HOV_SIDE_DEL_BASE + 100)
|
| {
|
| int row = hov - AI_HOV_SIDE_DEL_BASE;
|
| int total = ArraySize(g_ai_chats);
|
| int chatIdx = total - 1 - row;
|
| if(chatIdx >= 0 && chatIdx < total)
|
| {
|
| //--- Snapshot title before delete for toast
|
| string titleSnap = g_ai_chats[chatIdx].title;
|
| if(StringLen(titleSnap) > 30)
|
| titleSnap = StringSubstr(titleSnap, 0, 27) + "...";
|
| const bool deletedOk = AiDeleteChat(g_ai_chats[chatIdx].id);
|
| //--- Notify user via toast
|
| if(deletedOk)
|
| Ai_ShowToast("Successfully deleted chat '" + titleSnap + "'", false);
|
| else
|
| Ai_ShowToast("Failed to delete chat", true);
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| return;
|
| }
|
| //--- Popup chat row click - switch active chat
|
| if(hov >= AI_HOV_SMALL_CHAT_BASE && hov < AI_HOV_SMALL_DEL_BASE)
|
| {
|
| int row = hov - AI_HOV_SMALL_CHAT_BASE;
|
| if(row >= 0 && row < ArraySize(g_ai_popupRowChatIdx))
|
| {
|
| int chatIdx = g_ai_popupRowChatIdx[row];
|
| if(chatIdx >= 0 && chatIdx < ArraySize(g_ai_chats)
|
| && g_ai_chats[chatIdx].id != g_ai_currentChatId)
|
| {
|
| AiUpdateCurrentHistory();
|
| g_ai_currentChatId = g_ai_chats[chatIdx].id;
|
| g_ai_currentTitle = g_ai_chats[chatIdx].title;
|
| g_ai_conversationHistory = g_ai_chats[chatIdx].history;
|
| }
|
| }
|
| g_ai_showSearch = false;
|
| g_ai_showSmallHistory = false;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Popup delete button - remove chat from popup
|
| if(hov >= AI_HOV_SMALL_DEL_BASE && hov < AI_HOV_SMALL_DEL_BASE + 100)
|
| {
|
| int row = hov - AI_HOV_SMALL_DEL_BASE;
|
| if(row >= 0 && row < ArraySize(g_ai_popupRowChatIdx))
|
| {
|
| int chatIdx = g_ai_popupRowChatIdx[row];
|
| if(chatIdx >= 0 && chatIdx < ArraySize(g_ai_chats))
|
| {
|
| //--- Snapshot title before delete for toast
|
| string titleSnap = g_ai_chats[chatIdx].title;
|
| if(StringLen(titleSnap) > 30)
|
| titleSnap = StringSubstr(titleSnap, 0, 27) + "...";
|
| const bool deletedOk = AiDeleteChat(g_ai_chats[chatIdx].id);
|
| if(deletedOk)
|
| Ai_ShowToast("Successfully deleted chat '" + titleSnap + "'", false);
|
| else
|
| Ai_ShowToast("Failed to delete chat", true);
|
| }
|
| }
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| }
|
| //+------------------------------------------------------------------+
|
| //| Main chart event handler |
|
| //+------------------------------------------------------------------+
|
| void Ai_OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
|
| {
|
| //--- Bail when dashboard is hidden
|
| if(!g_ai_dashboardVisible)
|
| return;
|
| //--- Handle keyboard input events
|
| if(id == CHARTEVENT_KEYDOWN)
|
| {
|
| //--- Read modifier states
|
| int vk = (int)lparam;
|
| const int shiftRaw = (int)TerminalInfoInteger(TERMINAL_KEYSTATE_SHIFT);
|
| const bool shiftDown = (shiftRaw < 0) || ((shiftRaw & 0x8000) != 0);
|
| const int ctrlRaw = (int)TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL);
|
| const bool ctrlDown = (ctrlRaw < 0) || ((ctrlRaw & 0x8000) != 0);
|
| //--- Search editor focus path
|
| if(g_ai_searchEditor.focused)
|
| {
|
| //--- Escape closes search
|
| if(vk == 27)
|
| {
|
| g_ai_searchEditor.focused = false;
|
| g_ai_showSearch = false;
|
| Ai_EndKeyboardOverride();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Enter ignored in search
|
| if(vk == 13)
|
| {
|
| return;
|
| }
|
| //--- Shift-modified navigation extends selection
|
| if(shiftDown)
|
| {
|
| if(vk == 37)
|
| {
|
| g_ai_searchEditor.ShiftExtendLeft();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 39)
|
| {
|
| g_ai_searchEditor.ShiftExtendRight();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 36)
|
| {
|
| g_ai_searchEditor.ShiftExtendHome();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 35)
|
| {
|
| g_ai_searchEditor.ShiftExtendEnd();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| }
|
| //--- Plain navigation
|
| if(vk == 37)
|
| {
|
| g_ai_searchEditor.MoveCaretLeft();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 39)
|
| {
|
| g_ai_searchEditor.MoveCaretRight();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 36)
|
| {
|
| g_ai_searchEditor.MoveCaretHome();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 35)
|
| {
|
| g_ai_searchEditor.MoveCaretEnd();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Backspace and delete - update query and reset scroll
|
| if(vk == 8)
|
| {
|
| g_ai_searchEditor.Backspace();
|
| g_ai_searchQuery = g_ai_searchEditor.GetText();
|
| g_ai_searchScroll.scrollPx = 0;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 46)
|
| {
|
| g_ai_searchEditor.DeleteChar();
|
| g_ai_searchQuery = g_ai_searchEditor.GetText();
|
| g_ai_searchScroll.scrollPx = 0;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Filter out non-typing keys
|
| bool isModifier2 = (vk == 16 || vk == 17 || vk == 18
|
| || vk == 20 || vk == 144 || vk == 145
|
| || vk == 91 || vk == 92 || vk == 93);
|
| bool isNavigation2 = (vk >= 33 && vk <= 40) || (vk == 45);
|
| bool isFunctionKey2 = (vk >= 112 && vk <= 123);
|
| bool isTab2 = (vk == 9);
|
| if(isModifier2 || isNavigation2 || isFunctionKey2 || isTab2)
|
| return;
|
| //--- Filter to printable virtual keys
|
| bool isPrintableVk2 = (vk == 32) ||
|
| (vk >= 48 && vk <= 57) ||
|
| (vk >= 65 && vk <= 90) ||
|
| (vk >= 96 && vk <= 111) ||
|
| (vk >= 186 && vk <= 223);
|
| if(!isPrintableVk2)
|
| return;
|
| //--- Translate and insert character
|
| short uch2 = TranslateKey(vk);
|
| if(uch2 > 0)
|
| {
|
| ushort code2 = (ushort)uch2;
|
| string ch2 = ShortToString(code2);
|
| g_ai_searchEditor.InsertChar(ch2);
|
| g_ai_searchQuery = g_ai_searchEditor.GetText();
|
| g_ai_searchScroll.scrollPx = 0;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| return;
|
| }
|
| //--- Main editor focus path
|
| if(g_ai_editor.focused)
|
| {
|
| //--- Enter handling - submit on Shift+Enter, newline otherwise
|
| if(vk == 13)
|
| {
|
| if(shiftDown)
|
| {
|
| //--- Suppress empty submissions
|
| if(Ai_PromptIsEmpty())
|
| return;
|
| string txt = g_ai_editor.GetText();
|
| g_ai_editor.SetText("");
|
| g_ai_currentPrompt = "";
|
| AiSubmitMessage(txt);
|
| }
|
| else
|
| {
|
| g_ai_editor.InsertNewline();
|
| g_ai_currentPrompt = g_ai_editor.GetText();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| return;
|
| }
|
| //--- Escape blurs editor
|
| if(vk == 27)
|
| {
|
| g_ai_editor.focused = false;
|
| Ai_EndKeyboardOverride();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Shift-modified navigation extends selection
|
| if(shiftDown)
|
| {
|
| if(vk == 37)
|
| {
|
| g_ai_editor.ShiftExtendLeft();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 39)
|
| {
|
| g_ai_editor.ShiftExtendRight();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 38)
|
| {
|
| g_ai_editor.ShiftExtendUp();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 40)
|
| {
|
| g_ai_editor.ShiftExtendDown();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 36)
|
| {
|
| g_ai_editor.ShiftExtendHome();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 35)
|
| {
|
| g_ai_editor.ShiftExtendEnd();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| }
|
| //--- Plain navigation - use fast render path
|
| if(vk == 37)
|
| {
|
| g_ai_editor.MoveCaretLeft();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 39)
|
| {
|
| g_ai_editor.MoveCaretRight();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 38)
|
| {
|
| g_ai_editor.MoveCaretUp();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 40)
|
| {
|
| g_ai_editor.MoveCaretDown();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 36)
|
| {
|
| g_ai_editor.MoveCaretHome();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 35)
|
| {
|
| g_ai_editor.MoveCaretEnd();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Backspace and delete with fast render
|
| if(vk == 8)
|
| {
|
| g_ai_editor.Backspace();
|
| g_ai_currentPrompt = g_ai_editor.GetText();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| return;
|
| }
|
| if(vk == 46)
|
| {
|
| g_ai_editor.DeleteChar();
|
| g_ai_currentPrompt = g_ai_editor.GetText();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Filter out non-typing keys
|
| bool isModifier = (vk == 16 || vk == 17 || vk == 18
|
| || vk == 20 || vk == 144 || vk == 145
|
| || vk == 91 || vk == 92 || vk == 93);
|
| bool isNavigation = (vk >= 33 && vk <= 40) || (vk == 45);
|
| bool isFunctionKey = (vk >= 112 && vk <= 123);
|
| bool isTab = (vk == 9);
|
| if(isModifier || isNavigation || isFunctionKey || isTab)
|
| return;
|
| //--- Filter to printable virtual keys
|
| bool isPrintableVk = (vk == 32) ||
|
| (vk >= 48 && vk <= 57) ||
|
| (vk >= 65 && vk <= 90) ||
|
| (vk >= 96 && vk <= 111) ||
|
| (vk >= 186 && vk <= 223);
|
| if(!isPrintableVk)
|
| return;
|
| //--- Translate and insert character with fast render
|
| short uch = TranslateKey(vk);
|
| if(uch > 0)
|
| {
|
| ushort code = (ushort)uch;
|
| string ch = ShortToString(code);
|
| g_ai_editor.InsertChar(ch);
|
| g_ai_currentPrompt = g_ai_editor.GetText();
|
| Ai_FastRenderAfterEditorChange();
|
| ChartRedraw();
|
| }
|
| return;
|
| }
|
| //--- Escape with no editor focused closes dashboard
|
| if(vk == 27)
|
| {
|
| Ai_HideDashboard();
|
| return;
|
| }
|
| return;
|
| }
|
| //--- Handle mouse move events
|
| if(id == CHARTEVENT_MOUSE_MOVE)
|
| {
|
| //--- Snapshot popup open-state so an opening or closing popup forces a full render
|
| const bool _wasSearch0 = g_ai_showSearch;
|
| const bool _wasFootDd0 = g_ai_showFooterDropdown;
|
| const bool _wasHist0 = g_ai_showSmallHistory;
|
| //--- Read mouse coords and button state
|
| int mx = (int)lparam;
|
| int my = (int)dparam;
|
| int mstate = (int)sparam;
|
| int lx, ly;
|
| Ai_ChartToCanvas(mx, my, lx, ly);
|
| //--- Cache canvas-local mouse coords for render-time hover gates
|
| g_ai_mouseLx = lx;
|
| g_ai_mouseLy = ly;
|
| //--- Detect press/release up front so the modal image viewer handles its own clicks first
|
| static int prevMouseState = 0;
|
| bool freshPress = (prevMouseState == 0 && mstate == 1);
|
| bool freshRelease = (prevMouseState == 1 && mstate == 0);
|
| prevMouseState = mstate;
|
| //--- Full-image viewer is modal - it owns all interaction while open
|
| if(g_ai_imgFullView)
|
| {
|
| Ai_ViewerInteract(lx, ly, mstate, freshPress);
|
| return;
|
| }
|
| //--- Handle dashboard drag in progress
|
| if(g_ai_dragging)
|
| {
|
| //--- End drag on button release
|
| if((mstate & 1) == 0)
|
| {
|
| g_ai_dragging = false;
|
| }
|
| else
|
| {
|
| //--- Compute new dashboard position with bounds clamp
|
| int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
|
| int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
|
| int newX = MathMax(0, MathMin(chartW - Ai_DashboardW(),
|
| mx - g_ai_dragOffsetX));
|
| int newY = MathMax(0, MathMin(chartH - Ai_DashboardH(),
|
| my - g_ai_dragOffsetY));
|
| //--- Apply new position
|
| AI_DASHBOARD_X = newX;
|
| AI_DASHBOARD_Y = newY;
|
| ObjectSetInteger(0, AI_CANVAS_NAME_MAIN, OBJPROP_XDISTANCE, newX);
|
| ObjectSetInteger(0, AI_CANVAS_NAME_MAIN, OBJPROP_YDISTANCE, newY);
|
| //--- Move prompt overlay in lockstep
|
| if(g_ai_canvPromptExists)
|
| {
|
| int dpL, dpT, dpR, dpB;
|
| Ai_GetPromptPaneRect(dpL, dpT, dpR, dpB);
|
| ObjectSetInteger(0, AI_CANVAS_NAME_PROMPT, OBJPROP_XDISTANCE, newX + dpL);
|
| ObjectSetInteger(0, AI_CANVAS_NAME_PROMPT, OBJPROP_YDISTANCE, newY + dpT);
|
| }
|
| ChartRedraw();
|
| return;
|
| }
|
| }
|
| //--- Handle chat scrollbar drag
|
| if(g_ai_chatScrollDragging)
|
| {
|
| //--- Position is written every event (always exact); the repaint is capped at ~60fps
|
| AiScrollUpdateDrag(g_ai_chatScroll, ly);
|
| if((mstate & 1) == 0)
|
| {
|
| //--- Drop: paint immediately so the final frame lands on the true position
|
| AiScrollEndDrag(g_ai_chatScroll);
|
| g_ai_chatScrollDragging = false;
|
| g_ai_frameDirty = false;
|
| g_ai_partialTarget = AI_RT_NONE;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| else
|
| Ai_RequestPartial(AI_RT_CHAT);
|
| return;
|
| }
|
| //--- Handle editor scrollbar drag
|
| if(g_ai_editorScrollDragging)
|
| {
|
| //--- Position is written every event (always exact); the repaint is capped at ~60fps
|
| AiScrollUpdateDrag(g_ai_editor.scroll, ly);
|
| if((mstate & 1) == 0)
|
| {
|
| //--- Drop: paint immediately so the final frame lands on the true position
|
| AiScrollEndDrag(g_ai_editor.scroll);
|
| g_ai_editorScrollDragging = false;
|
| g_ai_frameDirty = false;
|
| g_ai_partialTarget = AI_RT_NONE;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| else
|
| Ai_RequestPartial(AI_RT_PROMPT);
|
| return;
|
| }
|
| //--- Handle search scrollbar drag
|
| if(g_ai_searchScrollDragging)
|
| {
|
| //--- Position is written every event (always exact); the repaint is capped at ~60fps
|
| AiScrollUpdateDrag(g_ai_searchScroll, ly);
|
| if((mstate & 1) == 0)
|
| {
|
| //--- Drop: paint immediately so the final frame lands on the true position
|
| AiScrollEndDrag(g_ai_searchScroll);
|
| g_ai_searchScrollDragging = false;
|
| g_ai_frameDirty = false;
|
| g_ai_partialTarget = AI_RT_NONE;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| else
|
| Ai_RequestPartial(AI_RT_SEARCH);
|
| return;
|
| }
|
| //--- Compute current hover code
|
| int newHov = Ai_HitTest(lx, ly);
|
| //--- Auto-show small history popup on hover when sidebar collapsed
|
| if(!g_ai_sidebarExpanded)
|
| {
|
| if(newHov == AI_HOV_HISTORY && !g_ai_showSmallHistory)
|
| {
|
| g_ai_showSmallHistory = true;
|
| g_ai_showSearch = false;
|
| }
|
| else
|
| if(g_ai_showSmallHistory)
|
| {
|
| //--- Hide popup when leaving anchor and popup body
|
| int hbL, hbT, hbR, hbB;
|
| Ai_GetSidebarBtnRect(3, hbL, hbT, hbR, hbB);
|
| bool inAnchor = AiPointInRect(lx, ly, hbL, hbT, hbR - hbL, hbB - hbT);
|
| bool inPopup = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
|
| g_ai_popupR - g_ai_popupL,
|
| g_ai_popupB - g_ai_popupT);
|
| if(!inAnchor && !inPopup)
|
| g_ai_showSmallHistory = false;
|
| }
|
| }
|
| //--- Update chat scrollbar hover state
|
| bool chatHovArea = false, chatHovThumb = false;
|
| if(AiScrollVisible(g_ai_chatScroll))
|
| {
|
| chatHovArea = Ai_PointInChatPane(lx, ly);
|
| chatHovThumb = AiScrollHitTestThumb(g_ai_chatScroll, lx, ly);
|
| }
|
| //--- Update editor scrollbar hover state
|
| bool editorHovArea = false, editorHovThumb = false;
|
| if(AiScrollVisible(g_ai_editor.scroll))
|
| {
|
| editorHovArea = Ai_PointInPromptPane(lx, ly);
|
| editorHovThumb = AiScrollHitTestThumb(g_ai_editor.scroll, lx, ly);
|
| }
|
| //--- Update search scrollbar hover state
|
| bool searchHovArea = false, searchHovThumb = false;
|
| if(g_ai_showSearch && AiScrollVisible(g_ai_searchScroll))
|
| {
|
| searchHovArea = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
|
| g_ai_popupR - g_ai_popupL,
|
| g_ai_popupB - g_ai_popupT);
|
| searchHovThumb = AiScrollHitTestThumb(g_ai_searchScroll, lx, ly);
|
| }
|
| //--- Track if cursor is over a pencil icon's narrow click rect
|
| bool overPencilIcon = false;
|
| const int peClickN = ArraySize(g_ai_userEditClickL);
|
| for(int piI = 0; piI < peClickN; piI++)
|
| {
|
| if(lx >= g_ai_userEditClickL[piI] && lx < g_ai_userEditClickR[piI]
|
| && ly >= g_ai_userEditClickT[piI] && ly < g_ai_userEditClickB[piI])
|
| {
|
| overPencilIcon = true;
|
| break;
|
| }
|
| }
|
| //--- Compute whether a redraw is needed for hover state changes
|
| bool needRedraw = (newHov != g_ai_hover)
|
| || (chatHovArea != g_ai_chatScroll.hoveredArea)
|
| || (chatHovThumb != g_ai_chatScroll.hoveredThumb)
|
| || (editorHovArea != g_ai_editor.scroll.hoveredArea)
|
| || (editorHovThumb != g_ai_editor.scroll.hoveredThumb)
|
| || (searchHovArea != g_ai_searchScroll.hoveredArea)
|
| || (searchHovThumb != g_ai_searchScroll.hoveredThumb)
|
| || (overPencilIcon != g_ai_overPencilIcon);
|
| //--- Compute which pane changes appearance this event to skip re-rendering the rest
|
| const bool oldChatCode = (g_ai_hover == AI_HOV_SCROLL_FAB)
|
| || (g_ai_hover >= AI_HOV_USER_EDIT_BASE
|
| && g_ai_hover < AI_HOV_USER_EDIT_BASE + 100)
|
| || (g_ai_hover >= AI_HOV_CHAT_IMAGE_BASE
|
| && g_ai_hover < AI_HOV_CHAT_IMAGE_BASE + 100);
|
| const bool newChatCode = (newHov == AI_HOV_SCROLL_FAB)
|
| || (newHov >= AI_HOV_USER_EDIT_BASE
|
| && newHov < AI_HOV_USER_EDIT_BASE + 100)
|
| || (newHov >= AI_HOV_CHAT_IMAGE_BASE
|
| && newHov < AI_HOV_CHAT_IMAGE_BASE + 100);
|
| const bool hoverChatDirty = oldChatCode || newChatCode
|
| || (chatHovArea != g_ai_chatScroll.hoveredArea)
|
| || (chatHovThumb != g_ai_chatScroll.hoveredThumb)
|
| || (overPencilIcon != g_ai_overPencilIcon);
|
| const bool hoverPromptDirty = (editorHovArea != g_ai_editor.scroll.hoveredArea)
|
| || (editorHovThumb != g_ai_editor.scroll.hoveredThumb);
|
| //--- Repaint the chrome regions the hover left and entered; unknown codes force all
|
| const int hovRgMask = Ai_HovRegionMask(g_ai_hover) | Ai_HovRegionMask(newHov);
|
| const bool hovRgUnknown = (hovRgMask & AI_RG_UNKNOWN) != 0;
|
| const bool hoverSideDirty = hovRgUnknown || ((hovRgMask & AI_RG_SIDEBAR) != 0);
|
| const bool hoverHeadDirty = hovRgUnknown || ((hovRgMask & AI_RG_HEADER) != 0);
|
| const bool hoverFootDirty = hovRgUnknown || ((hovRgMask & AI_RG_FOOTER) != 0);
|
| //--- Update tooltip when hover code changes
|
| if(newHov != g_ai_hover)
|
| {
|
| string tip = "";
|
| if(newHov == AI_HOV_CLOSE)
|
| tip = "Close";
|
| else
|
| if(newHov == AI_HOV_THEME)
|
| tip = g_ai_darkTheme ? "Switch to light theme" : "Switch to dark theme";
|
| else
|
| if(newHov == AI_HOV_TOGGLE)
|
| tip = g_ai_sidebarExpanded ? "Collapse sidebar" : "Expand sidebar";
|
| else
|
| if(newHov == AI_HOV_NEW_CHAT)
|
| tip = "New chat";
|
| else
|
| if(newHov == AI_HOV_CLEAR)
|
| tip = "Clear current chat";
|
| else
|
| if(newHov == AI_HOV_HISTORY)
|
| tip = "Recent chats";
|
| else
|
| if(newHov == AI_HOV_SEARCH)
|
| tip = "Search chats";
|
| else
|
| if(newHov == AI_HOV_SIGNAL)
|
| tip = "Run selected action";
|
| else
|
| if(newHov == AI_HOV_SIGNAL_DD)
|
| tip = "Choose action";
|
| else
|
| if(newHov == AI_HOV_SEND)
|
| tip = Ai_PromptIsEmpty() ? "Type a message to send" : "Send (Shift+Enter)";
|
| else
|
| if(newHov == AI_HOV_REGEN)
|
| tip = "Regenerate response";
|
| else
|
| if(newHov == AI_HOV_ACTION_APPROVE)
|
| tip = "Approve AI action";
|
| else
|
| if(newHov == AI_HOV_ACTION_REJECT)
|
| tip = "Reject AI action";
|
| else
|
| if(newHov == AI_HOV_EXPORT)
|
| tip = "Export chat to file";
|
| else
|
| if(newHov == AI_HOV_COMPOSE_REMOVE)
|
| tip = "Remove attachment";
|
| else
|
| if(newHov == AI_HOV_COMPOSE_THUMB)
|
| tip = "Click to view full";
|
| else
|
| if(newHov == AI_HOV_SCROLL_FAB)
|
| tip = "Scroll to latest";
|
| else
|
| if(newHov >= AI_HOV_USER_EDIT_BASE
|
| && newHov < AI_HOV_USER_EDIT_BASE + 100)
|
| tip = "Edit this prompt";
|
| else
|
| if(newHov >= AI_HOV_CHAT_IMAGE_BASE
|
| && newHov < AI_HOV_CHAT_IMAGE_BASE + 100)
|
| tip = "Click to view full";
|
| ObjectSetString(0, AI_CANVAS_NAME_MAIN, OBJPROP_TOOLTIP,
|
| (StringLen(tip) > 0) ? tip : "\n");
|
| }
|
| //--- Commit hover state to globals
|
| g_ai_hover = newHov;
|
| g_ai_chatScroll.hoveredArea = chatHovArea;
|
| g_ai_chatScroll.hoveredThumb = chatHovThumb;
|
| g_ai_chatScroll.hover = chatHovArea || chatHovThumb;
|
| g_ai_editor.scroll.hoveredArea = editorHovArea;
|
| g_ai_editor.scroll.hoveredThumb = editorHovThumb;
|
| g_ai_editor.scroll.hover = editorHovArea || editorHovThumb;
|
| g_ai_searchScroll.hoveredArea = searchHovArea;
|
| g_ai_searchScroll.hoveredThumb = searchHovThumb;
|
| g_ai_searchScroll.hover = searchHovArea || searchHovThumb;
|
| g_ai_overPencilIcon = overPencilIcon;
|
| //--- Toggle chart mouse scroll based on cursor inside dashboard
|
| const bool overDashboard = (lx >= 0 && lx < Ai_DashboardW()
|
| && ly >= 0 && ly < Ai_DashboardH());
|
| if(!g_ai_chatScrollDragging && !g_ai_editorScrollDragging && !g_ai_searchScrollDragging)
|
| ChartSetInteger(0, CHART_MOUSE_SCROLL, !overDashboard);
|
| //--- Handle fresh mouse button press
|
| if(freshPress)
|
| {
|
| //--- Make chat and compose clicks inert while a popup is open so it does not leak beneath
|
| const bool popupOpen = g_ai_showSearch || g_ai_showSmallHistory
|
| || g_ai_showFooterDropdown;
|
| if(!popupOpen)
|
| {
|
| //--- Click an inline chart thumbnail to open the full view
|
| const int imgClkN = ArraySize(g_ai_imgClickBase);
|
| for(int ici = 0; ici < imgClkN; ici++)
|
| {
|
| if(lx >= g_ai_imgClickL[ici] && lx < g_ai_imgClickR[ici]
|
| && ly >= g_ai_imgClickT[ici] && ly < g_ai_imgClickB[ici])
|
| {
|
| Ai_OpenImageFull(g_ai_imgClickBase[ici]);
|
| return;
|
| }
|
| }
|
| //--- Handle the compose attachment chip: badge removes it, thumbnail opens the viewer
|
| if(g_ai_attachPending)
|
| {
|
| int cxl, cxt, cxr, cxb;
|
| if(Ai_GetComposeRemoveRect(cxl, cxt, cxr, cxb)
|
| && lx >= cxl && lx < cxr && ly >= cxt && ly < cxb)
|
| {
|
| Ai_RemoveAttachment();
|
| return;
|
| }
|
| int ctl, ctt, ctr, ctb;
|
| if(Ai_GetComposeThumbRect(ctl, ctt, ctr, ctb)
|
| && lx >= ctl && lx < ctr && ly >= ctt && ly < ctb)
|
| {
|
| Ai_OpenImageFull(g_ai_attachBase);
|
| return;
|
| }
|
| }
|
| }
|
| //--- Footer dropdown handling
|
| if(g_ai_showFooterDropdown)
|
| {
|
| bool inDdBody = AiPointInRect(lx, ly, g_ai_footerDdL, g_ai_footerDdT,
|
| g_ai_footerDdR - g_ai_footerDdL,
|
| g_ai_footerDdB - g_ai_footerDdT);
|
| bool onAnchor = (newHov == AI_HOV_SIGNAL || newHov == AI_HOV_SIGNAL_DD);
|
| bool onItem = (newHov >= AI_HOV_FOOTER_DD_ITEM_BASE
|
| && newHov < AI_HOV_FOOTER_DD_ITEM_BASE + 100);
|
| //--- Click on item dispatches action
|
| if(onItem)
|
| {
|
| Ai_HandleAction(newHov);
|
| return;
|
| }
|
| //--- Click outside body and anchor closes dropdown
|
| if(!inDdBody && !onAnchor)
|
| {
|
| g_ai_showFooterDropdown = false;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Click inside body but not on item - swallow
|
| if(inDdBody)
|
| {
|
| return;
|
| }
|
| }
|
| //--- Search/history popup handling
|
| if(g_ai_showSearch || g_ai_showSmallHistory)
|
| {
|
| bool inPopupBody = AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
|
| g_ai_popupR - g_ai_popupL,
|
| g_ai_popupB - g_ai_popupT);
|
| bool onAnchorS = (newHov == AI_HOV_SEARCH);
|
| bool onAnchorH = (newHov == AI_HOV_HISTORY);
|
| bool onPopupRow = (newHov >= AI_HOV_SMALL_CHAT_BASE
|
| && newHov < AI_HOV_SMALL_DEL_BASE + 100);
|
| //--- Search popup scrollbar thumb takes priority
|
| if(g_ai_showSearch
|
| && AiScrollVisible(g_ai_searchScroll)
|
| && AiScrollHitTestThumb(g_ai_searchScroll, lx, ly))
|
| {
|
| AiScrollBeginDrag(g_ai_searchScroll, ly);
|
| g_ai_searchScrollDragging = true;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Click on popup row dispatches action
|
| if(onPopupRow)
|
| {
|
| Ai_HandleAction(newHov);
|
| return;
|
| }
|
| //--- Click on search input box focuses search editor
|
| if(g_ai_showSearch
|
| && g_ai_popupSearchR > g_ai_popupSearchL
|
| && AiPointInRect(lx, ly, g_ai_popupSearchL, g_ai_popupSearchT,
|
| g_ai_popupSearchR - g_ai_popupSearchL,
|
| g_ai_popupSearchB - g_ai_popupSearchT))
|
| {
|
| if(g_ai_editor.focused)
|
| g_ai_editor.focused = false;
|
| bool wasSF = g_ai_searchEditor.focused;
|
| g_ai_searchEditor.focused = true;
|
| g_ai_searchEditor.SetCaretFromMouse(
|
| lx - g_ai_popupSearchL - 4 - g_ai_searchEditor.padX,
|
| ly - g_ai_popupSearchT - 4 - g_ai_searchEditor.padY);
|
| if(!wasSF)
|
| Ai_BeginKeyboardOverride();
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Click outside body and anchors closes popup
|
| if(!inPopupBody && !onAnchorS && !onAnchorH)
|
| {
|
| g_ai_showSearch = false;
|
| g_ai_showSmallHistory = false;
|
| if(g_ai_searchEditor.focused)
|
| {
|
| g_ai_searchEditor.focused = false;
|
| if(!g_ai_editor.focused)
|
| Ai_EndKeyboardOverride();
|
| }
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| return;
|
| }
|
| //--- Click inside body but unhandled - swallow
|
| if(inPopupBody)
|
| {
|
| return;
|
| }
|
| }
|
| //--- Test scrollbar thumbs
|
| bool editorThumbDown = AiScrollVisible(g_ai_editor.scroll)
|
| && AiScrollHitTestThumb(g_ai_editor.scroll, lx, ly);
|
| bool chatThumbDown = AiScrollVisible(g_ai_chatScroll)
|
| && AiScrollHitTestThumb(g_ai_chatScroll, lx, ly);
|
| //--- Begin editor scrollbar drag
|
| if(editorThumbDown)
|
| {
|
| AiScrollBeginDrag(g_ai_editor.scroll, ly);
|
| g_ai_editorScrollDragging = true;
|
| needRedraw = true;
|
| }
|
| //--- Begin chat scrollbar drag
|
| else
|
| if(chatThumbDown)
|
| {
|
| AiScrollBeginDrag(g_ai_chatScroll, ly);
|
| g_ai_chatScrollDragging = true;
|
| needRedraw = true;
|
| }
|
| //--- Begin dashboard drag from header
|
| else
|
| if(newHov == AI_HOV_DRAG)
|
| {
|
| g_ai_dragging = true;
|
| g_ai_dragOffsetX = lx;
|
| g_ai_dragOffsetY = ly;
|
| if(g_ai_editor.focused)
|
| {
|
| g_ai_editor.focused = false;
|
| Ai_EndKeyboardOverride();
|
| needRedraw = true;
|
| }
|
| }
|
| //--- Generic click handling
|
| else
|
| {
|
| //--- Focus editor on prompt pane click
|
| bool wasFocused = g_ai_editor.focused;
|
| if(Ai_PointInPromptPane(lx, ly))
|
| {
|
| if(g_ai_searchEditor.focused)
|
| g_ai_searchEditor.focused = false;
|
| g_ai_editor.focused = true;
|
| int pL, pT, pR, pB;
|
| Ai_GetPromptPaneRect(pL, pT, pR, pB);
|
| g_ai_editor.SetCaretFromMouse(lx - pL - 4 - g_ai_editor.padX,
|
| ly - pT - 4 - Ai_ComposeChipH() - g_ai_editor.padY);
|
| if(!wasFocused)
|
| Ai_BeginKeyboardOverride();
|
| needRedraw = true;
|
| }
|
| else
|
| {
|
| //--- Click outside prompt blurs editor
|
| if(wasFocused)
|
| {
|
| g_ai_editor.focused = false;
|
| if(!g_ai_searchEditor.focused)
|
| Ai_EndKeyboardOverride();
|
| needRedraw = true;
|
| }
|
| }
|
| //--- Dispatch action for non-prompt hits
|
| if(newHov != AI_HOV_NONE && newHov != AI_HOV_DRAG
|
| && !Ai_PointInPromptPane(lx, ly))
|
| {
|
| Ai_HandleAction(newHov);
|
| return;
|
| }
|
| }
|
| }
|
| //--- Handle fresh mouse button release
|
| if(freshRelease)
|
| {
|
| g_ai_dragging = false;
|
| if(g_ai_chatScrollDragging)
|
| {
|
| AiScrollEndDrag(g_ai_chatScroll);
|
| g_ai_chatScrollDragging = false;
|
| needRedraw = true;
|
| }
|
| if(g_ai_editorScrollDragging)
|
| {
|
| AiScrollEndDrag(g_ai_editor.scroll);
|
| g_ai_editorScrollDragging = false;
|
| needRedraw = true;
|
| }
|
| }
|
| //--- Repaint on state change: instant on press or release, hover motion capped near 60fps
|
| if(needRedraw)
|
| {
|
| if(freshPress || freshRelease)
|
| {
|
| g_ai_frameDirty = false;
|
| g_ai_partialTarget = AI_RT_NONE;
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| else
|
| {
|
| //--- Pure hover motion: full render if a popup just toggled, else repaint only that overlay
|
| const bool popupChanged = (g_ai_showSearch != _wasSearch0)
|
| || (g_ai_showFooterDropdown != _wasFootDd0)
|
| || (g_ai_showSmallHistory != _wasHist0);
|
| const bool anyPopupUp = g_ai_showSearch || g_ai_showFooterDropdown
|
| || g_ai_showSmallHistory
|
| || (StringLen(g_ai_toastText) > 0);
|
| if(popupChanged
|
| || (anyPopupUp && (g_ai_showSearch || StringLen(g_ai_toastText) > 0)))
|
| Ai_RequestFrame();
|
| else
|
| if(anyPopupUp && (g_ai_showFooterDropdown || g_ai_showSmallHistory))
|
| Ai_RequestOverlayFrame();
|
| else
|
| Ai_RequestHoverFrame(hoverSideDirty, hoverHeadDirty, hoverFootDirty,
|
| hoverChatDirty || hovRgUnknown,
|
| hoverPromptDirty || hovRgUnknown);
|
| }
|
| }
|
| return;
|
| }
|
| //--- Handle mouse wheel events
|
| if(id == CHARTEVENT_MOUSE_WHEEL)
|
| {
|
| //--- Decode wheel coords and delta
|
| const int mx = (int)(short)lparam;
|
| const int my = (int)(short)(lparam >> 16);
|
| const int delta = (int)dparam;
|
| int lx, ly;
|
| Ai_ChartToCanvas(mx, my, lx, ly);
|
| //--- Full-image viewer wheel pans vertically, or horizontally when only overflowing that way
|
| if(g_ai_imgFullView)
|
| {
|
| ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
|
| if(AiScrollVisible(g_ai_fullVScroll))
|
| AiScrollByWheel(g_ai_fullVScroll, delta, 60);
|
| else
|
| if(AiScrollVisible(g_ai_fullHScroll))
|
| AiScrollByWheel(g_ai_fullHScroll, delta, 60);
|
| Ai_RenderViewerScroll();
|
| return;
|
| }
|
| //--- Search popup takes priority when open and hovered
|
| if(g_ai_showSearch
|
| && AiPointInRect(lx, ly, g_ai_popupL, g_ai_popupT,
|
| g_ai_popupR - g_ai_popupL,
|
| g_ai_popupB - g_ai_popupT)
|
| && AiScrollVisible(g_ai_searchScroll))
|
| {
|
| ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
|
| //--- Skip repaint when already clamped at an extreme so the ends do not hang
|
| const int beforeS = g_ai_searchScroll.scrollPx;
|
| AiScrollByWheel(g_ai_searchScroll, delta, 30);
|
| if(g_ai_searchScroll.scrollPx == beforeS)
|
| return;
|
| //--- Only the search popup moved - repaint just that pane, capped ~60fps
|
| Ai_RequestPartial(AI_RT_SEARCH);
|
| return;
|
| }
|
| //--- Editor scroll when over prompt pane
|
| if(Ai_PointInPromptPane(lx, ly) && AiScrollVisible(g_ai_editor.scroll))
|
| {
|
| ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
|
| //--- Clamp guard - no repaint when already at an extreme
|
| const int beforeP = g_ai_editor.scroll.scrollPx;
|
| AiScrollByWheel(g_ai_editor.scroll, delta, 30);
|
| if(g_ai_editor.scroll.scrollPx == beforeP)
|
| return;
|
| //--- Only the prompt pane moved
|
| Ai_RequestPartial(AI_RT_PROMPT);
|
| return;
|
| }
|
| //--- Chat scroll when over chat pane
|
| if(Ai_PointInChatPane(lx, ly) && AiScrollVisible(g_ai_chatScroll))
|
| {
|
| ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
|
| //--- Clamp guard - no repaint when already at an extreme
|
| const int beforeC = g_ai_chatScroll.scrollPx;
|
| AiScrollByWheel(g_ai_chatScroll, delta, 30);
|
| if(g_ai_chatScroll.scrollPx == beforeC)
|
| return;
|
| //--- Only the chat pane moved - repaint just that pane, capped ~60fps
|
| Ai_RequestPartial(AI_RT_CHAT);
|
| return;
|
| }
|
| //--- Restore chart mouse scroll outside dashboard
|
| ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
|
| return;
|
| }
|
| //--- Re-render on chart resize or scroll with repaint capped and final state guaranteed
|
| if(id == CHARTEVENT_CHART_CHANGE)
|
| {
|
| Ai_RequestFrame();
|
| return;
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Timer event handler for blink and toast updates |
|
| //+------------------------------------------------------------------+
|
| void Ai_OnTimer()
|
| {
|
| //--- Bail when dashboard is hidden
|
| if(!g_ai_dashboardVisible)
|
| {
|
| //--- Keep the timer from idling in fast mode while hidden
|
| if(g_ai_timerFast)
|
| {
|
| EventSetMillisecondTimer(500);
|
| g_ai_timerFast = false;
|
| g_ai_frameDirty = false;
|
| }
|
| return;
|
| }
|
| //--- Flush any skipped frame and restore the idle timer cadence after the burst
|
| Ai_FlushFrame();
|
| //--- Keep blink and toast logic on the original 500ms cadence during fast flush
|
| static uint s_lastSlowTickMs = 0;
|
| const uint slowNowMs = GetTickCount();
|
| if(g_ai_timerFast && slowNowMs - s_lastSlowTickMs < 500)
|
| return;
|
| s_lastSlowTickMs = slowNowMs;
|
| //--- Track what changed for minimum redraw decision
|
| bool editorBlinkChanged = false;
|
| bool searchBlinkChanged = false;
|
| bool toastChanged = false;
|
| //--- Update editor blink state
|
| if(g_ai_editor.focused)
|
| {
|
| bool prev = g_ai_editor.blinkOn;
|
| g_ai_editor.UpdateBlink();
|
| if(g_ai_editor.blinkOn != prev)
|
| editorBlinkChanged = true;
|
| }
|
| //--- Update search editor blink state
|
| if(g_ai_searchEditor.focused)
|
| {
|
| bool prev2 = g_ai_searchEditor.blinkOn;
|
| g_ai_searchEditor.UpdateBlink();
|
| if(g_ai_searchEditor.blinkOn != prev2)
|
| searchBlinkChanged = true;
|
| }
|
| //--- Toast auto-dismiss after expiry
|
| bool toastAlive = false;
|
| if(StringLen(g_ai_toastText) > 0)
|
| {
|
| if(GetTickCount64() > g_ai_toastExpiryMs)
|
| {
|
| g_ai_toastText = "";
|
| g_ai_toastExpiryMs = 0;
|
| toastChanged = true;
|
| }
|
| else
|
| {
|
| //--- Toast still alive - flag for progress bar redraw
|
| toastAlive = true;
|
| }
|
| }
|
| //--- Choose minimum redraw path based on what changed
|
| if(toastChanged || toastAlive || searchBlinkChanged)
|
| {
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| else
|
| if(editorBlinkChanged)
|
| {
|
| //--- Fall back to full render when overlays are present
|
| const bool hasOverlay =
|
| g_ai_showSearch
|
| || g_ai_showFooterDropdown
|
| || g_ai_showSmallHistory
|
| || (StringLen(g_ai_toastText) > 0);
|
| if(hasOverlay)
|
| {
|
| Ai_RenderAll();
|
| ChartRedraw();
|
| }
|
| else
|
| if(g_ai_canvPromptExists)
|
| {
|
| //--- Fast path: render only prompt overlay
|
| Ai_RenderPromptPaneOverlay();
|
| ChartRedraw();
|
| }
|
| else
|
| {
|
| //--- Overlay not available - fall back to legacy path
|
| Ai_RenderPromptPane();
|
| g_ai_canvMain.Update();
|
| ChartRedraw();
|
| }
|
| }
|
| }
|
|
|
| #endif // AI_CANVAS_INTERACT_MQH
|
| //+------------------------------------------------------------------+
|