| //+------------------------------------------------------------------+
|
| //| 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(CAiCanvasFast &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(CAiCanvasFast &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
|
| //+------------------------------------------------------------------+
|