916 lines
No EOL
33 KiB
MQL5
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 |