//+------------------------------------------------------------------+ //| AI Canvas Editor.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_EDITOR_MQH #define AI_CANVAS_EDITOR_MQH //--- Include required libraries #include #include "AI Canvas Theme.mqh" #include "AI Canvas Primitives.mqh" #include "AI Canvas Scrollbar.mqh" //+------------------------------------------------------------------+ //| Multi-line scrollable text editor with caret and selection | //+------------------------------------------------------------------+ class CAiEditor { public: string buffer; // Full text content int caret; // Caret character position int anchor; // Selection anchor position (-1 = none) bool focused; // Focus state flag string fontName; // Font name for rendering int fontSize; // Font size in points int padX; // Horizontal padding int padY; // Vertical padding int wrapWidth; // Word-wrap width in pixels int lineH; // Line height in pixels AiScrollState scroll; // Vertical scroll state string visLines[]; // Visual line strings (post-wrap) int bufOffsets[]; // Buffer offsets per visual line bool blinkOn; // Caret blink toggle ulong lastBlinkMs; // Last blink toggle timestamp string placeholder; // Placeholder text when empty //--- Persistent off-screen canvas for clip and blend operations CAiCanvasFast tmpCanvas; // Reusable scratch canvas bool tmpCanvasReady; // Scratch canvas init flag int tmpCanvasW; // Scratch canvas width int tmpCanvasH; // Scratch canvas height string tmpCanvasName; // Unique scratch canvas object name CAiEditor(); void Init(const string fnt, int fsz, int padInX, int padInY); void SetWrapWidth(int wrapW); void SetPlaceholder(const string s) { placeholder = s; } void SetText(const string s); string GetText() const { return buffer; } bool IsEmpty() const { return StringLen(buffer) == 0; } void Rebuild(); bool HasSelection(); bool GetSelectionRange(int &s, int &e); bool DeleteSelection(); void ClearSelection() { anchor = -1; } void SelectAll(); void InsertChar(const string ch); void InsertNewline() { InsertChar("\n"); } void Backspace(); void DeleteChar(); void MoveCaretLeft(); void MoveCaretRight(); void MoveCaretUp(); void MoveCaretDown(); void MoveCaretHome(); void MoveCaretEnd(); void ShiftExtendLeft(); void ShiftExtendRight(); void ShiftExtendUp(); void ShiftExtendDown(); void ShiftExtendHome(); void ShiftExtendEnd(); void SetCaretFromMouse(int localX, int localY); bool HandleKeydown(int vk, bool shift, bool ctrl); void UpdateBlink(); void EnsureCaretVisible(); void Render(CCanvas &canvas, int rectL, int rectT, int rectR, int rectB, CAiCanvasPrimitives &prim); private: void FindLineCol(int caretPos, int &outLine, int &outCol); int ColPxToCharIndex(const string line, int xPx); int CharIndexToColPx(const string line, int charIdx); }; //+------------------------------------------------------------------+ //| Construct editor with default state | //+------------------------------------------------------------------+ CAiEditor::CAiEditor() { //--- Initialize text and selection state buffer = ""; caret = 0; anchor = -1; focused = false; //--- Initialize font and layout defaults fontName = "Arial"; fontSize = AI_FONT_BODY; padX = 8; padY = 6; wrapWidth = 200; lineH = 16; //--- Initialize scroll, blink, and placeholder AiScrollInit(scroll); blinkOn = true; lastBlinkMs = 0; placeholder = ""; //--- Initialize scratch canvas state tmpCanvasReady = false; tmpCanvasW = 0; tmpCanvasH = 0; //--- Generate unique scratch canvas name per instance static int s_editorInstanceCounter = 0; s_editorInstanceCounter++; tmpCanvasName = "AiEditorTmpPersistent_" + IntegerToString(s_editorInstanceCounter); } //+------------------------------------------------------------------+ //| Initialize font and padding | //+------------------------------------------------------------------+ void CAiEditor::Init(const string fnt, int fsz, int padInX, int padInY) { //--- Apply font and padding settings fontName = fnt; fontSize = fsz; padX = padInX; padY = padInY; //--- Compute line height from font metrics lineH = AiTextHeight(fnt, fsz) + 2; } //+------------------------------------------------------------------+ //| Set wrap width and rebuild visual lines | //+------------------------------------------------------------------+ void CAiEditor::SetWrapWidth(int w) { //--- Bail if unchanged if(w == wrapWidth) return; wrapWidth = w; Rebuild(); } //+------------------------------------------------------------------+ //| Replace buffer content and reset caret | //+------------------------------------------------------------------+ void CAiEditor::SetText(const string s) { //--- Replace buffer and reset selection buffer = s; caret = StringLen(buffer); anchor = -1; Rebuild(); } //+------------------------------------------------------------------+ //| Rebuild visual lines array from buffer | //+------------------------------------------------------------------+ void CAiEditor::Rebuild() { //--- Reset visual line arrays ArrayResize(visLines, 0); ArrayResize(bufOffsets, 0); if(wrapWidth <= 0) wrapWidth = 200; //--- Walk buffer one logical line at a time const int textLen = StringLen(buffer); int bufLineStart = 0; for(int i = 0; i <= textLen; i++) { bool atEnd = (i == textLen); bool atNL = (!atEnd && StringGetCharacter(buffer, i) == '\n'); if(!atEnd && !atNL) continue; //--- Extract logical line content int bufLineLen = i - bufLineStart; string bufLine = StringSubstr(buffer, bufLineStart, bufLineLen); int localStart = 0; //--- Wrap logical line into visual segments while(localStart <= bufLineLen) { int remainLen = bufLineLen - localStart; string remain = StringSubstr(bufLine, localStart, remainLen); //--- Empty line case - emit empty visual line if(wrapWidth <= 0 || remainLen == 0) { int sz = ArraySize(visLines); ArrayResize(visLines, sz + 1); ArrayResize(bufOffsets, sz + 1); visLines[sz] = remain; bufOffsets[sz] = bufLineStart + localStart; break; } //--- Whole remainder fits - emit as one visual line int wholeW = AiTextWidth(remain, fontName, fontSize); if(wholeW <= wrapWidth) { int sz = ArraySize(visLines); ArrayResize(visLines, sz + 1); ArrayResize(bufOffsets, sz + 1); visLines[sz] = remain; bufOffsets[sz] = bufLineStart + localStart; break; } //--- Binary search for max prefix that fits int lo = 1, hi = remainLen, fitK = 1; while(lo <= hi) { int mid = (lo + hi) / 2; string prefix = StringSubstr(remain, 0, mid); int pw = AiTextWidth(prefix, fontName, fontSize); if(pw <= wrapWidth) { fitK = mid; lo = mid + 1; } else { hi = mid - 1; } } //--- Prefer wrapping at last space before fit point int breakAt = fitK; bool foundSpace = false; for(int k = fitK; k >= 1; k--) { ushort ch = StringGetCharacter(remain, k - 1); if(ch == ' ') { breakAt = k - 1; foundSpace = true; break; } } //--- Emit visual line and advance int consume; string visualLine; if(foundSpace) { visualLine = StringSubstr(remain, 0, breakAt); consume = breakAt + 1; } else { visualLine = StringSubstr(remain, 0, fitK); consume = fitK; } int sz = ArraySize(visLines); ArrayResize(visLines, sz + 1); ArrayResize(bufOffsets, sz + 1); visLines[sz] = visualLine; bufOffsets[sz] = bufLineStart + localStart; localStart += consume; } bufLineStart = i + 1; } //--- Ensure at least one empty line exists if(ArraySize(visLines) == 0) { ArrayResize(visLines, 1); ArrayResize(bufOffsets, 1); visLines[0] = ""; bufOffsets[0] = 0; } //--- Update scroll total height and clamp scroll.totalH = lineH * ArraySize(visLines); AiScrollClamp(scroll); } //+------------------------------------------------------------------+ //| Check if a non-empty selection exists | //+------------------------------------------------------------------+ bool CAiEditor::HasSelection() { //--- No anchor or zero-width range means no selection if(anchor < 0) return false; if(anchor == caret) return false; return true; } //+------------------------------------------------------------------+ //| Get normalized selection range start/end | //+------------------------------------------------------------------+ bool CAiEditor::GetSelectionRange(int &s, int &e) { //--- Bail when no selection if(!HasSelection()) { s = e = 0; return false; } //--- Normalize ordering s = (anchor < caret) ? anchor : caret; e = (anchor > caret) ? anchor : caret; //--- Clamp to buffer bounds const int len = StringLen(buffer); if(s < 0) s = 0; if(e > len) e = len; if(s > e) s = e; return e > s; } //+------------------------------------------------------------------+ //| Delete the current selection from buffer | //+------------------------------------------------------------------+ bool CAiEditor::DeleteSelection() { //--- Bail if no selection int s, e; if(!GetSelectionRange(s, e)) return false; //--- Splice buffer around selection const int len = StringLen(buffer); const string before = (s > 0) ? StringSubstr(buffer, 0, s) : ""; const string after = (e < len) ? StringSubstr(buffer, e, len - e) : ""; buffer = before + after; //--- Reset caret to splice point caret = s; anchor = -1; Rebuild(); return true; } //+------------------------------------------------------------------+ //| Select the entire buffer content | //+------------------------------------------------------------------+ void CAiEditor::SelectAll() { //--- Anchor to start, caret at end const int len = StringLen(buffer); if(len <= 0) { anchor = -1; caret = 0; return; } anchor = 0; caret = len; } //+------------------------------------------------------------------+ //| Insert character/string at caret | //+------------------------------------------------------------------+ void CAiEditor::InsertChar(const string ch) { //--- Bail on empty input if(StringLen(ch) == 0) return; //--- Replace selection if present DeleteSelection(); //--- Clamp caret to valid range int len = StringLen(buffer); if(caret < 0) caret = 0; if(caret > len) caret = len; //--- Splice in the new content string before = StringSubstr(buffer, 0, caret); string after = StringSubstr(buffer, caret, len - caret); buffer = before + ch + after; caret += StringLen(ch); Rebuild(); EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Delete character before caret | //+------------------------------------------------------------------+ void CAiEditor::Backspace() { //--- Delete selection first if present if(DeleteSelection()) { EnsureCaretVisible(); return; } if(caret <= 0) return; //--- Splice out character before caret int len = StringLen(buffer); string before = StringSubstr(buffer, 0, caret - 1); string after = (caret < len) ? StringSubstr(buffer, caret, len - caret) : ""; buffer = before + after; caret--; Rebuild(); EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Delete character at caret | //+------------------------------------------------------------------+ void CAiEditor::DeleteChar() { //--- Delete selection first if present if(DeleteSelection()) { EnsureCaretVisible(); return; } int len = StringLen(buffer); if(caret >= len) return; //--- Splice out character at caret string before = StringSubstr(buffer, 0, caret); string after = StringSubstr(buffer, caret + 1, len - caret - 1); buffer = before + after; Rebuild(); EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Translate buffer position to visual line and column | //+------------------------------------------------------------------+ void CAiEditor::FindLineCol(int caretPos, int &outLine, int &outCol) { //--- Find the visual line containing the position const int n = ArraySize(visLines); int line = 0; for(int i = 0; i < n; i++) { if(bufOffsets[i] <= caretPos) line = i; else break; } //--- Compute column within line outLine = line; outCol = caretPos - bufOffsets[line]; //--- Clamp column to line length int lineLen = StringLen(visLines[line]); if(outCol > lineLen) outCol = lineLen; if(outCol < 0) outCol = 0; } //+------------------------------------------------------------------+ //| Move caret one position left | //+------------------------------------------------------------------+ void CAiEditor::MoveCaretLeft() { //--- Collapse selection to start int s, e; if(GetSelectionRange(s, e)) { caret = s; anchor = -1; EnsureCaretVisible(); return; } //--- Step back one character if(caret > 0) caret--; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Move caret one position right | //+------------------------------------------------------------------+ void CAiEditor::MoveCaretRight() { //--- Collapse selection to end int s, e; if(GetSelectionRange(s, e)) { caret = e; anchor = -1; EnsureCaretVisible(); return; } //--- Step forward one character int len = StringLen(buffer); if(caret < len) caret++; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Move caret up one visual line | //+------------------------------------------------------------------+ void CAiEditor::MoveCaretUp() { //--- Clear selection and locate caret ClearSelection(); int line, col; FindLineCol(caret, line, col); if(line <= 0) return; //--- Map column to previous line int prevLineLen = StringLen(visLines[line - 1]); int newCol = (col <= prevLineLen) ? col : prevLineLen; caret = bufOffsets[line - 1] + newCol; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Move caret down one visual line | //+------------------------------------------------------------------+ void CAiEditor::MoveCaretDown() { //--- Clear selection and locate caret ClearSelection(); int line, col; FindLineCol(caret, line, col); int n = ArraySize(visLines); if(line >= n - 1) return; //--- Map column to next line int nextLineLen = StringLen(visLines[line + 1]); int newCol = (col <= nextLineLen) ? col : nextLineLen; caret = bufOffsets[line + 1] + newCol; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Move caret to start of line | //+------------------------------------------------------------------+ void CAiEditor::MoveCaretHome() { //--- Snap caret to line start ClearSelection(); int line, col; FindLineCol(caret, line, col); caret = bufOffsets[line]; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Move caret to end of line | //+------------------------------------------------------------------+ void CAiEditor::MoveCaretEnd() { //--- Snap caret to line end ClearSelection(); int line, col; FindLineCol(caret, line, col); caret = bufOffsets[line] + StringLen(visLines[line]); EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Extend selection one position left | //+------------------------------------------------------------------+ void CAiEditor::ShiftExtendLeft() { //--- Set anchor and step back if(anchor < 0) anchor = caret; if(caret > 0) caret--; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Extend selection one position right | //+------------------------------------------------------------------+ void CAiEditor::ShiftExtendRight() { //--- Set anchor and step forward if(anchor < 0) anchor = caret; const int len = StringLen(buffer); if(caret < len) caret++; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Extend selection up one visual line | //+------------------------------------------------------------------+ void CAiEditor::ShiftExtendUp() { //--- Set anchor and move to previous line if(anchor < 0) anchor = caret; int line, col; FindLineCol(caret, line, col); if(line <= 0) return; int prevLineLen = StringLen(visLines[line - 1]); int newCol = (col <= prevLineLen) ? col : prevLineLen; caret = bufOffsets[line - 1] + newCol; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Extend selection down one visual line | //+------------------------------------------------------------------+ void CAiEditor::ShiftExtendDown() { //--- Set anchor and move to next line if(anchor < 0) anchor = caret; int line, col; FindLineCol(caret, line, col); int n = ArraySize(visLines); if(line >= n - 1) return; int nextLineLen = StringLen(visLines[line + 1]); int newCol = (col <= nextLineLen) ? col : nextLineLen; caret = bufOffsets[line + 1] + newCol; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Extend selection to start of line | //+------------------------------------------------------------------+ void CAiEditor::ShiftExtendHome() { //--- Set anchor and snap to line start if(anchor < 0) anchor = caret; int line, col; FindLineCol(caret, line, col); caret = bufOffsets[line]; EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Extend selection to end of line | //+------------------------------------------------------------------+ void CAiEditor::ShiftExtendEnd() { //--- Set anchor and snap to line end if(anchor < 0) anchor = caret; int line, col; FindLineCol(caret, line, col); caret = bufOffsets[line] + StringLen(visLines[line]); EnsureCaretVisible(); } //+------------------------------------------------------------------+ //| Convert pixel x within a line to character index | //+------------------------------------------------------------------+ int CAiEditor::ColPxToCharIndex(const string line, int xPx) { //--- Handle empty line and left edge const int len = StringLen(line); if(len == 0 || xPx <= 0) return 0; //--- Past line end - clamp to length int totalW = AiTextWidth(line, fontName, fontSize); if(xPx >= totalW) return len; //--- Walk prefixes finding nearest character boundary int prevW = 0; for(int i = 1; i <= len; i++) { string prefix = StringSubstr(line, 0, i); int w = AiTextWidth(prefix, fontName, fontSize); int mid = (prevW + w) / 2; if(xPx < mid) return i - 1; prevW = w; } return len; } //+------------------------------------------------------------------+ //| Convert character index in line to pixel x | //+------------------------------------------------------------------+ int CAiEditor::CharIndexToColPx(const string line, int charIdx) { //--- Zero index is at left edge if(charIdx <= 0) return 0; //--- Measure prefix width string prefix = StringSubstr(line, 0, charIdx); return AiTextWidth(prefix, fontName, fontSize); } //+------------------------------------------------------------------+ //| Set caret position from mouse click coordinates | //+------------------------------------------------------------------+ void CAiEditor::SetCaretFromMouse(int localX, int localY) { //--- Convert mouse y to content line int contentY = localY - padY + scroll.scrollPx; if(contentY < 0) contentY = 0; int line = contentY / lineH; int n = ArraySize(visLines); if(line < 0) line = 0; if(line >= n) line = n - 1; //--- Convert mouse x to column int xInLine = localX - padX; if(xInLine < 0) xInLine = 0; int col = ColPxToCharIndex(visLines[line], xInLine); //--- Apply caret position and clear selection caret = bufOffsets[line] + col; ClearSelection(); } //+------------------------------------------------------------------+ //| Process keyboard input event | //+------------------------------------------------------------------+ bool CAiEditor::HandleKeydown(int vk, bool shift, bool ctrl) { //--- Ignore when unfocused if(!focused) return false; //--- Ignore modifier-only keys if(vk == 16 || vk == 17 || vk == 18 || vk == 20 || vk == 144 || vk == 145 || vk == 91 || vk == 92 || vk == 93) return false; //--- Handle shift-modified navigation (selection extension) if(shift) { if(vk == 37) { ShiftExtendLeft(); return true; } if(vk == 39) { ShiftExtendRight(); return true; } if(vk == 38) { ShiftExtendUp(); return true; } if(vk == 40) { ShiftExtendDown(); return true; } if(vk == 36) { ShiftExtendHome(); return true; } if(vk == 35) { ShiftExtendEnd(); return true; } } //--- Handle plain navigation keys if(vk == 37) { MoveCaretLeft(); return true; } if(vk == 39) { MoveCaretRight(); return true; } if(vk == 38) { MoveCaretUp(); return true; } if(vk == 40) { MoveCaretDown(); return true; } if(vk == 36) { MoveCaretHome(); return true; } if(vk == 35) { MoveCaretEnd(); return true; } //--- Handle editing keys if(vk == 8) { Backspace(); return true; } if(vk == 46) { DeleteChar(); return true; } if(vk == 13 && shift) { InsertNewline(); return true; } //--- 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 false; //--- Translate to character and insert short uch = TranslateKey(vk); if(uch <= 0) return false; ushort code = (ushort)uch; string ch = ShortToString(code); InsertChar(ch); return true; } //+------------------------------------------------------------------+ //| Update caret blink state based on elapsed time | //+------------------------------------------------------------------+ void CAiEditor::UpdateBlink() { //--- Always on when unfocused if(!focused) { blinkOn = true; return; } //--- Toggle every 500ms ulong now = GetTickCount64(); if(now - lastBlinkMs >= 500) { blinkOn = !blinkOn; lastBlinkMs = now; } } //+------------------------------------------------------------------+ //| Auto-scroll so caret is within viewport | //+------------------------------------------------------------------+ void CAiEditor::EnsureCaretVisible() { //--- Compute caret line bounds int line, col; FindLineCol(caret, line, col); int caretLineY = line * lineH; int caretLineBottom = caretLineY + lineH; //--- Scroll up or down to keep caret visible if(caretLineY < scroll.scrollPx) { scroll.scrollPx = caretLineY; } else if(caretLineBottom > scroll.scrollPx + scroll.viewportH) { scroll.scrollPx = caretLineBottom - scroll.viewportH; } AiScrollClamp(scroll); } //+------------------------------------------------------------------+ //| Render editor text, selection, and caret | //+------------------------------------------------------------------+ void CAiEditor::Render(CCanvas &canvas, int rectL, int rectT, int rectR, int rectB, CAiCanvasPrimitives &prim) { //--- Compute content rect from outer rect and padding const int contentL = rectL + padX; const int contentT = rectT + padY; const int contentR = rectR - padX; const int contentB = rectB - padY; const int viewportW = contentR - contentL; const int viewportH = contentB - contentT; if(viewportW <= 0 || viewportH <= 0) return; //--- Update scroll viewport and clamp scroll.viewportH = viewportH; AiScrollClamp(scroll); //--- Recompute wrap if viewport width changed const int effectiveWrapW = viewportW - 10; if(wrapWidth != effectiveWrapW) { wrapWidth = effectiveWrapW; Rebuild(); } //--- Determine if placeholder should display const bool showPlaceholder = (StringLen(buffer) == 0) && StringLen(placeholder) > 0; //--- Read host canvas dimensions const int cw = canvas.Width(); const int ch = canvas.Height(); //--- Ensure scratch canvas matches host size if(!tmpCanvasReady || tmpCanvasW < cw || tmpCanvasH < ch) { if(tmpCanvasReady) tmpCanvas.Destroy(); const int newW = MathMax(cw, tmpCanvasW); const int newH = MathMax(ch, tmpCanvasH); if(!tmpCanvas.CreateBitmap(tmpCanvasName, 0, 0, newW, newH, COLOR_FORMAT_ARGB_NORMALIZE)) return; tmpCanvasW = newW; tmpCanvasH = newH; tmpCanvasReady = true; } //--- Clear content region and seed from host canvas tmpCanvas.FillRectFast(MathMax(contentL, 0), MathMax(contentT, 0), MathMin(contentR, cw), MathMin(contentB, ch), 0); tmpCanvas.CopyRectFromCanvas(canvas, contentL, contentT, contentR, contentB); //--- Alias scratch canvas as tmp for the render block #define tmp tmpCanvas tmp.FontSet(fontName, -(fontSize * 10)); const int n = ArraySize(visLines); //--- Draw selection highlights when focused with text if(focused && !showPlaceholder) { int selS, selE; if(GetSelectionRange(selS, selE)) { const uint selArgb = ColorToARGB(g_ai_selectionBg, 140); //--- Iterate visual lines and intersect with selection for(int liS = 0; liS < n; liS++) { const int lineStart = bufOffsets[liS]; const int lineLen = StringLen(visLines[liS]); const int lineEnd = lineStart + lineLen; const int subS = (selS > lineStart) ? selS : lineStart; const int subE = (selE < lineEnd) ? selE : lineEnd; if(subE <= subS) continue; //--- Measure pre-selection and selection widths const int preStart = lineStart; const int preCount = subS - preStart; const int selCount = subE - subS; const string preSel = (preCount > 0) ? StringSubstr(buffer, preStart, preCount) : ""; const string inSel = (selCount > 0) ? StringSubstr(buffer, subS, selCount) : ""; const int preW = (StringLen(preSel) > 0) ? tmp.TextWidth(preSel) : 0; const int selW = (StringLen(inSel) > 0) ? tmp.TextWidth(inSel) : 0; //--- Add small sliver when selection spans newline const bool inclTrailNL = (selE > lineEnd && lineEnd < StringLen(buffer) && StringGetCharacter(buffer, lineEnd) == '\n'); const int sliverW = inclTrailNL ? 4 : 0; //--- Compute screen rect for highlight const int rL = contentL + preW; const int rR = rL + selW + sliverW; const int rT = contentT + liS * lineH - scroll.scrollPx; const int rB = rT + lineH; if(rB <= contentT || rT >= contentB) continue; //--- Blend selection color over pixels for(int yy = rT; yy < rB; yy++) for(int xx = rL; xx < rR; xx++) WidgetAiBlendPixel(tmp, xx, yy, selArgb); } } } //--- Draw placeholder or visual lines if(showPlaceholder) { AiStampTextAA(tmp, contentL, contentT, placeholder, fontName, fontSize, g_ai_subText); } else { for(int li = 0; li < n; li++) { const int lineY = contentT + li * lineH - scroll.scrollPx; if(lineY + lineH < contentT) continue; if(lineY > contentB) break; if(StringLen(visLines[li]) > 0) AiStampTextAA(tmp, contentL, lineY, visLines[li], fontName, fontSize, g_ai_bodyText); } } //--- Draw blinking caret when focused if(focused && blinkOn) { int caretX, caretY1, caretY2; if(showPlaceholder) { caretX = contentL; caretY1 = contentT + 1; caretY2 = caretY1 + lineH - 3; } else { int caretLine, caretCol; FindLineCol(caret, caretLine, caretCol); caretX = contentL + CharIndexToColPx(visLines[caretLine], caretCol); caretY1 = contentT + caretLine * lineH - scroll.scrollPx + 1; caretY2 = caretY1 + lineH - 3; } const uint caretArgb = ColorToARGB(g_ai_caretColor, 255); WidgetAiThickLineAA(tmp, caretX, caretY1, caretX, caretY2, 1, caretArgb); } //--- Copy rendered region back to host canvas tmp.CopyRectToCanvas(canvas, contentL, contentT, contentR, contentB); //--- Undefine scratch alias #undef tmp //--- Draw scrollbar when content exceeds viewport if(AiScrollVisible(scroll)) { const int sbW = 4; const int sbRightPad = 2; scroll.trackL = contentR - sbW - sbRightPad; scroll.trackT = contentT; scroll.trackR = scroll.trackL + sbW; scroll.trackB = contentB; AiScrollDraw(canvas, scroll, prim); } } #endif // AI_CANVAS_EDITOR_MQH