1013 lines
34 KiB
MQL5
1013 lines
34 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(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
| |||
//+------------------------------------------------------------------+
|