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

916 lines
No EOL
33 KiB
MQL5

//+------------------------------------------------------------------+
//| 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 <Canvas/Canvas.mqh>
#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