GMS 2 Multiline Textbox: Improved, still needs optimization ideas

Tsa05

Member
I think we've all dodged around this a bit for decades, so I'm just going to post a multiline textbox script now and have done with it.

GMS has long gotten away with having no built-in way to allow reasonable multi-line text input because
A) Why would your player want to write an essay in your game?
b) Wouldn't you prefer to code your own to match the aesthetic of your game?

The answers have always been
A) Because <infinitely varied specific uses of text input>
B) No, and how would I pop up the system keyboard on mobile with that?

Ok, so here's half, at least. A multiline textbox. I don't know how to make an os keyboard pop up for mobile, but I suppose you can make one with the virtual keys thing?
Edit: I was linked to this, which would be an effective way to connect the textbox script with mobile keyboards: https://marketplace.yoyogames.com/assets/245/keyboard

Objective: A single script that handles textbox input.
There's lots more things we can do by ballooning everything into UI objects, and that's great, but I want to cut right to the heart of the thing. What should such a box have, how should it be implemented, what are the quirks and how to solve them?

And it's really hard to have this discussion on the Reviews section of the Marketplace (most multiline textbox script requests are met with "find an extension"). So, I'm going to put on a helmet and toss this out there, so we at least have something to discuss.

EDIT: New, more optimized solution, and additionally optimized solution using surfaces combined into this post. The remainder of the OP remains as a more backwards compatible, simpler version; the new set of scripts in post #19 runs 25x faster on my computer (literally 25x!)

Yes, it yanks info from keyboard_string. Yes, it checks some specific vk presses. Yes, I bet there's os hooks and dlls and egos abound. But, here's something made in GML--where should we go from here?

EDIT: Re-wrote with entirely new method. Have a look. Now, Enter key fully and properly works and all the rest. Original tucked in a spoiler in a later post.
Code:
/// @description    draw_input(x,y,width,height,caption,text, limit)
/// @arg    x        The x-position of the box
/// @arg    y        The y-position of the box
/// @arg    width    The width of the box
/// @arg    height    The height of the box
/// @arg    caption    Caption to display in the box
/// @arg    text    Text to work on
/// @arg    limit    Max # of characters allowed in the box
/**************************************************************/
/*        Information
*    Developed by Tsa05 on the GameMaker forum for the SLUDGE engine
*    Use of the script is subject to the following conditions:
*        1) Leave the information section intact
*        2) Give this script freely
*
*    This script should be called from a Draw event.
*    The return value should be continuously passed back in.
*    This script erases keyboard_string.
*    Some instance variables are used to track persistent info;
*    instance variables all start with underscore (_)
*    Text *will* overflow the box if your limit permits it.
*
*    Undo (Ctrl+Z)/Redo (Ctrl+Shift+Z) is available, but REQUIRES additional steps:
*        1) Set useUndo to 1 in the settings area below. Now you have a memory leak.
*        2) Somewhere in your game, when you decide to stop showing
*           this box, you've got to clear the histroy to solve the leak:
*        if(ds_exists(_textUndoList,ds_type_list)) ds_map_destroy(_textUndoList);
*
*    This script was designed for GMS2, and uses variable_instance_exists.
*    To use with GMS 1.4, you'd need to remove that section and define
*    those variables in a calling instance.
*/
var bx = argument[0];
var by = argument[1];
var bw = argument[2];
var bh = argument[3];
var bc = argument[4];
var bt = argument[5]; // Whole input string
var bl = argument[6];

/******* Settings ********/
var c_border = make_color_rgb(44,44,44);
var c_fill   = make_color_rgb(70,70,70);
var c_text   = c_white;
var c_select = c_white;
var c_cursor = c_white;

var repeatDelay = room_speed/20;
var firstDelayFactor = 10;
var doubleClick = room_speed/3;
var blinkSpeed = 5; // Higher number == slower cursor blink
var hintOpacity = .15; //Opacuty of the hint cursor. Zero for no hint
var useUndo = 0; // Should undo/redo exist?
var nl = "\n";    // Linebreak character
var space = " "; // Space character
/*************************/
/*    Keys                **/
var isLeft        = keyboard_check(vk_left);
var isRight        = keyboard_check(vk_right);
var isUp        = keyboard_check(vk_up);
var isDown        = keyboard_check(vk_down);
var isShift        = keyboard_check(vk_shift);
var isDel        = keyboard_check(vk_delete);
var isBack        = keyboard_check(vk_backspace);
var isCtrl        = keyboard_check(vk_control);
var isHome        = keyboard_check(vk_home);
var isEnd        = keyboard_check(vk_end);
var isAny        = keyboard_check(vk_anykey);
var isEnter        = keyboard_check(vk_enter);
var isC            = keyboard_check(ord("C"));
var isV            = keyboard_check(ord("V"));
var isX            = keyboard_check(ord("X"));
var isA            = keyboard_check(ord("A"));
var isZ            = keyboard_check(ord("Z"));
/*************************/
/*    Mouse                **/
var isPressed  = mouse_check_button_pressed(mb_left);
var isHeld     = mouse_check_button(mb_left);
var isReleased = mouse_check_button_released(mb_left);
/*************************/

// Set up things this script needs
var pad = 3;
var indent = 2;
var okbs = keyboard_string;
var wrapped = bt;
keyboard_string = "";
var tbw = bw-2*indent*pad;
var op = 0; // Current opacity for mouse hint cursor
var obt = bt;
var undoLock = 0;
var offset = 0;    // Offset used to track how many visual linebreaks added before cursor

/******* Define Locals ********/
if(!variable_instance_exists(id,"_txtBoxDelay")){
    _txtBoxDelay = -4;
}
if(!variable_instance_exists(id,"_cursorAfterNewline")){
    _cursorAfterNewline = 0;
}
if(!variable_instance_exists(id,"_selectAfterNewline")){
    _selectAfterNewline = 0;
}
if(!variable_instance_exists(id,"_cursorPos")){
    _cursorPos = string_length(bt);
}
if(!variable_instance_exists(id,"_selectCursorPos")){
    _selectCursorPos = -1;
}
if(!variable_instance_exists(id,"_dragCursor")){
    _dragCursor = -1;
}
if(!variable_instance_exists(id,"_doubleClickTimer")){
    _doubleClickTimer = -1;
}
if(!variable_instance_exists(id,"_doubleClickLock")){
    _doubleClickLock = -1;
}
if(!variable_instance_exists(id,"_textBlinkCounter")){
    // I hate to track something this mundane, but
    // there's a subtle need for blink to reset on keypress
    _textBlinkCounter = 0;
}

if(useUndo){
    if(!variable_instance_exists(id,"_textUndoList")){
        _textUndoList = ds_list_create();
        ds_list_add(_textUndoList,bt);
    }
    if(!variable_instance_exists(id,"_textUndoPos")){
        _textUndoPos = 0;
    }
}
/***************************/
_textBlinkCounter+=1;
if(isAny||isHeld){
    _textBlinkCounter=0;
}

/////////////////////////
// Draw the box
draw_set_color(c_fill);
draw_roundrect_ext(bx,by,bx+bw,by+bh, 2*pad, 2*pad, 0);
draw_set_color(c_border);
for(var z=0;z<pad;z+=1){
    draw_roundrect_ext(bx+z,by+z,bx+bw-z,by+bh-z, 2*pad, 2*pad, 1);
}
draw_set_color(c_text);
if(bc!=""){ // Draw a caption
    draw_text(bx+indent*pad, by+3*pad, bc);
    by+=3*pad+string_height(bc)+pad;
}

/////////////////////////
// Insert text
if(_selectCursorPos==_cursorPos) _selectCursorPos = -1; // Do not permit length zero (GMS bug with string_copy)

if(_selectCursorPos>=0 && okbs!=""){
    if(_cursorPos<_selectCursorPos){
        var c1 = _cursorPos+1;
        var c2 = _selectCursorPos+1;
        var af = 1;
    }else{
        var c2 = _cursorPos+1;
        var c1 = _selectCursorPos+1;
        var af = 0;
    }
    var len = c2-c1;
 
    bt = string_delete(bt,c1,len);
    if(!af){
        _cursorPos-=len;
    }
    _selectCursorPos = -1;
    _selectAfterNewline = 0;
    _cursorAfterNewline = 0;
}
/////////////////////////
// Handle text add/remove
if(okbs!=""){
    if(string_length(bt+okbs)<bl){
        bt = string_insert(okbs,bt,_cursorPos+1);
        _cursorPos+=string_length(okbs);
        _cursorAfterNewline = 0;
    }else{
        // Add character by character
        var tkbs = okbs;
        while(string_length(bt)<bl){
            bt = string_insert(string_char_at(tkbs,1),bt,_cursorPos+1);
            tkbs=string_delete(tkbs,1,1);
            _cursorPos+=1;
            _cursorAfterNewline = 0;
        }
    }
}
if(string_length(bt)>bl){
    bt = string_copy(bt,1,bl);
    if(_cursorPos>string_length(bt)){
        _cursorPos = string_length(bt);
    }
}
if(_txtBoxDelay<=0){ // See if text is inserted via paste
    if(_selectCursorPos!=-1){
        // put cursor in order
        if(_selectCursorPos<_cursorPos){
            var low = _selectCursorPos+1;
            var high = _cursorPos+1;
        }else{
            var low = _cursorPos+1;
            var high = _selectCursorPos+1;
        }
    }else{
        low = _cursorPos; high = _cursorPos;
    }
    /////////////////////////
    // Remove text
    if(isDel){
        if(_selectCursorPos!=-1){
            bt = string_delete(bt,low,high-low);
            _selectCursorPos = -1;
            _cursorPos = low-1;
        }else{
            bt = string_delete(bt,_cursorPos+1,1);
        }
    }else if(isBack){
        if(_selectCursorPos!=-1){
            // put things in order
            bt = string_delete(bt,low,high-low);
            _selectCursorPos = -1;
            _cursorPos = low-1;
        }else{
            if(_cursorPos>0){
                bt = string_delete(bt,_cursorPos,1);
                _cursorPos-=1;
            }
        }
        _cursorAfterNewline = 0;
    }else
    /////////////////////////
    // Add newline
    if(isEnter){
        if(_selectCursorPos!=-1){
            bt = string_delete(bt,low,high-low);
            _selectCursorPos = -1;
            _cursorPos = low-1;
        }
        bt = string_insert(nl,bt,_cursorPos+1);
        _cursorPos+=string_length(nl);
        _cursorAfterNewline = 1;
    }
    else
    /////////////////////////
    // CutCopyPaste
    if(isCtrl){
        // Crtl + X is a combo of cut and copy
        if(isC || isX){
            // Copy
            if(_selectCursorPos>=0){
                if(_cursorPos<_selectCursorPos){
                    var c1 = _cursorPos+1;
                    var c2 = _selectCursorPos+1;
                    var af = 1;
                }else{
                    var c2 = _cursorPos+1;
                    var c1 = _selectCursorPos+1;
                    var af = 0;
                }
                var len = c2-c1;
                var newStr = string_copy(bt,c1,len);
                clipboard_set_text(newStr);
            }
        }
    
        if(isV || isX){
            // Paste and Cut
            if(clipboard_has_text()){
                if(_selectCursorPos>=0){ // Trim out selected text, if any
                    if(_cursorPos<_selectCursorPos){
                        var c1 = _cursorPos+1;
                        var c2 = _selectCursorPos+1;
                        var af = 1;
                    }else{
                        var c2 = _cursorPos+1;
                        var c1 = _selectCursorPos+1;
                        var af = 0;
                    }
                    var len = c2-c1;
 
                    bt = string_delete(bt,c1,len);
                    if(!af){
                        _cursorPos-=len;
                    }
                    _selectCursorPos = -1;
                }
                if(isV){        
                    // Add new text
                    var newStr = clipboard_get_text();
                    if(string_length(bt+newStr)<bl){
                        bt = string_insert(newStr,bt,_cursorPos+1);
                        _cursorPos+=string_length(newStr);
                    }else{
                        // Add character by character
                        var ts = newStr;
                        while(string_length(bt)<bl){
                            bt = string_insert(string_char_at(ts,1),bt,_cursorPos+1);
                            ts=string_delete(ts,1,1);
                            _cursorPos+=1;
                        }
                    }
                }
            }
        }
    }
}
/////////////////////////
// Convert to text matrix
var p = 1;
var row = 0;
var col = 0;
var textArray = 0;
var ts = bt;
while(p<=string_length(ts)){
    if(string_pos(nl, ts)==p){
        // There's a newline!
        p+=string_length(nl)-1; // Minus one because we're already +1
        textArray[row, col] = nl;
        ts = string_delete(ts,1,p);
        col = 0; row+=1; p = 1;
    }else{
        var currentW = string_width(string_copy(ts,1,p));
        if(currentW>tbw){
            row+=1;
            col = 0;
            ts = string_delete(ts,1,p-1);    
            p = 1;
        }
        var c = string_char_at(ts,p);
        textArray[row, col] = c;
        col+=1;
        p+=1;
    }
}
/////////////////////////
// Check where the mouse
// is among the text
var tbx = bx+indent*pad;
var tx = tbx;
var ty = by;
var mPos = -1;
var mCoords = -1;
var cCoords = -1;
var sCoords = -1;
draw_set_color(c_white);
var ah = array_height_2d(textArray);
for(var row = 0; row<ah; row+=1){
    var rowLength = array_length_2d(textArray, row);
    for(var col = 0; col<rowLength; col+=1){
        var c = textArray[row, col];
        var cw = string_width(c); var ch = string_height(c);
        var leftEdge = tx;
        if(col==0) leftEdge-=cw/2; // Swag~ Gives extra room to click in the margin.
        var isTrail = point_in_rectangle(mouse_x, mouse_y, leftEdge, ty, tx+cw/2, ty+ch);
        var isLead = point_in_rectangle(mouse_x, mouse_y, tx+cw/2,ty, tbx+tbw, ty+ch);
        if(isTrail){ // Trailing Edge
            mPos = [row, col-1];
            mCoords = [tx, ty, tx, ty+ch];
        }else if(isLead){ // Leading edge
            mPos = [row, col];
            mCoords = [tx+cw, ty, tx+cw, ty+ch];
        }
    tx+=string_width(c);
    }
    ty+=string_height(c);
    tx = tbx;
}

/////////////////////////
// Handle the mouse
var dragging = 0;
if(is_array(mCoords)){
    if(isHeld){
        if(!_dragCursor){
            // Set the cursor
            if(isShift){
                // Fresh click in a new place, but moving selection
                // Find the lower cursor position, move it.
                var scp = 1;
                for(var z=0;z<mPos[0];z+=1){
                    // Increment scp by a whole row
                    scp+=array_length_2d(textArray, z);
                }
                scp+=mPos[1];
                if(mPos[1]<0){
                    _selectAfterNewline = 1;
                }else{
                    _selectAfterNewline = 0;
                }
                _selectCursorPos = scp;
            }else{
                // If it's just an innocent little click, clear selections
                // and start a new drag
                var cp = 1;
                for(var z=0;z<mPos[0];z+=1){
                    // Increment scp by a whole row
                    cp+=array_length_2d(textArray, z);
                }
                cp+=mPos[1];
                if(mPos[1]<0){
                    _cursorAfterNewline = 1;
                }else{
                    _cursorAfterNewline = 0;
                }
                _cursorPos = cp;
                _dragCursor = 1;
                _selectCursorPos = -1; _selectAfterNewline = 0;
            }
        }else{
            // Move the selection cursor
            if(_doubleClickTimer>0){
                // This click was within doubleClick time and place.
                _doubleClickTimer = -1;
                // Find space before
                var pc = string_copy(bt,1,_cursorPos);
                var p1 = 0;
                while(string_pos(" ",pc)){
                    var p1 = string_pos(" ",pc);
                    pc = string_replace(pc," ","_");
                }
                // Find space after
                var pc = string_delete(bt,1,_cursorPos);
                var p2 = string_pos(" ",pc)-1;
                if(p2<0){
                    _selectCursorPos = string_length(bt);
                }else{
                    _selectCursorPos = _cursorPos + p2;
                
                }
                _cursorPos = p1;
                _doubleClickLock = 1;
            }else if(!_doubleClickLock){
                var scp = 1;
                for(var z=0;z<mPos[0];z+=1){
                    // Increment scp by a whole row
                    scp+=array_length_2d(textArray, z);
                }
                scp+=mPos[1];
                _selectCursorPos = scp;
            }
        }
    }
}


/////////////////////////
// Find the cursor grid
var cr = 0; var cc = 0;
var tracker = 1;
var arrayHeight = array_height_2d(textArray);
while(tracker<_cursorPos){
    tracker+=1;
    var rowLength = array_length_2d(textArray, cr);
    if(cc>=rowLength-1){
        cc = 0;
        cr+=1;
        // If the last character is a newline,
        // it might be multiple characters
        var a = string_length(textArray[cr, cc])-1;
        tracker+=a;
    }else{
        cc+=1;
    }
}
if(_cursorPos==0){
    cc=-1;
}else if(_cursorAfterNewline && cr<arrayHeight-1){
    cc=-1; cr +=1;
}
/////////////////////////
// Find the selection
var scr = 0; var scc = 0;
var tracker = 1;
var arrayHeight = array_height_2d(textArray);
while(tracker<_selectCursorPos){
    tracker+=1;
    var rowLength = array_length_2d(textArray, scr);
    if(scc>=rowLength-1){
        scc = 0;
        scr+=1;
        // If the last character is a newline,
        // it might be multiple characters
        var a = string_length(textArray[scr, scc])-1;
        tracker+=a;
    }else{
        scc+=1;
    }
}
if(_selectCursorPos==0){
    scc=-1;
}else if(_selectAfterNewline && scr<arrayHeight-1){
    scc=-1; scr +=1;
}


/////////////////////////
// Draw the text
/////////////////////////
// Find the cursor
/////////////////////////
// Find the selection
var tbx = bx+indent*pad;
var tx = tbx;
var ty = by;
draw_set_color(c_cursor);
cCoords = [tx, ty, tx, ty+string_height("|")];
var ah = array_height_2d(textArray);
for(var row = 0; row<ah; row+=1){
    var rowLength = array_length_2d(textArray, row);
    for(var col = 0; col<rowLength; col+=1){
        var c = textArray[row, col];
        cw = string_width(c);
        draw_text(tx, ty, c);
        ////////////////////////////////////
        // Check cursor                   //
        var modX = 0;
        if( (row == cr) && ((col == cc) or (col==0&&cc=-1)) ){
            cCoords = [tx+cw+modX, ty, tx+cw+modX, ty+ch];
        }
        ////////////////////////////////////
        // Check selection                //
        if(_selectCursorPos!=-1){
            if( (row == scr) && ((col == scc) or (col==0&&scc=-1)) ){
                sCoords = [tx+cw+modX, ty, tx+cw+modX, ty+ch];
            }
        }
    
        ////////////////////////////////////
        tx+=string_width(c);
    }
    ty+=string_height(c);
    tx = tbx;
}


if(!isHeld){
    if(_selectCursorPos!=_cursorPos&&_selectCursorPos!=-1){
        // If there's a selection, don't enable double-clicking on cancel
        dragging = 1;
    }
    _dragCursor=-1;
    _doubleClickLock = -1;
}
if(_doubleClickTimer>0){
    _doubleClickTimer-=1;
}
if(isReleased && !dragging){
    _doubleClickTimer = doubleClick;
}
//////////////////////////////////////////////////
// Now that the current state of decorations
// is computed, draw them
//////////////////////////////////////////////////

/////////////////////////
// Draw the mouse
if(is_array(mCoords)){
    //show_debug_message("mrow:"+string(mPos[0])+" mcol:"+string(mPos[1]));
    draw_set_alpha(hintOpacity);
    draw_line(mCoords[0], mCoords[1], mCoords[2], mCoords[3]);
    draw_set_alpha(1);
}
/////////////////////////
// Draw the cursor
if(is_array(cCoords)){
    var a = round(.5*cos(_textBlinkCounter*(room_speed/blinkSpeed))+.5);
    draw_set_alpha(a);
    draw_set_color(c_select);
    if(_cursorPos==0||_cursorAfterNewline){
        if(is_array(textArray)){
            var cw = string_width(textArray[cr,0]);
        }
        cCoords[0]-=cw; cCoords[2]-=cw;
    }
    draw_line(cCoords[0], cCoords[1], cCoords[2], cCoords[3]);
    draw_set_alpha(1);
}
/////////////////////////
// Draw the selection
draw_set_color(c_select);
if(is_array(sCoords) && is_array(cCoords)){
    if(_selectCursorPos==0||_selectAfterNewline){
        var cw = string_width(textArray[0,0]);
        sCoords[0]-=cw; sCoords[2]-=cw;
    }
    var lower = 0; var upper = 0;
    if(_selectCursorPos<_cursorPos){
        lower = sCoords; upper = cCoords;
    }else{
        lower = cCoords; upper = sCoords;
    }
    // Reverse cursor coords if needed
    // to always draw low to high
    var x1 = lower[0]; var tx1 = upper[0];
    var y1 = lower[1]; var ty1 = upper[1];
    var x2 = lower[2]; var tx2 = upper[2];
    var y2 = lower[3]; var ty2 = upper[3];
    var h = y2-y1;
    var outline = 1;
    while(y1<ty1){
        // Draw to the end of the lines
        // until we have the right line
        draw_rectangle(x1, y1,tbx+tbw, y2, outline);
        x1 = tbx;
        y1+=h;y2+=h;
    }
    if(y1!=ty2) // Eliminate 1 px selection at start of line
        draw_rectangle(x1, y1, tx2, ty2, outline);
}
//////////////////////////////////////////////////
// Now that the current state is computed,
// Compute input movement for next cycle
//////////////////////////////////////////////////

if(isAny && _txtBoxDelay<=0){
    /////////////////////////
    // Prepare cursor info
    /////////////////////////
    if(isShift && _selectCursorPos==-1){
        _selectCursorPos = _cursorPos;
        scc = cc; scr = cr;
    }else if(!isShift && !isCtrl && _selectCursorPos!=-1){
        // Just useability
        // Arrow keys canceling a selection
        // Jumps to last selection point
        cc = scc; cr = scr;
    }
    // Left Pressed
    if(isLeft){
        var amt = 0;
        if(isShift){
            if(isCtrl){
                // Find the nearest previous space or line
                amt =1;
                while(scc>=amt && textArray[scr, scc-amt]!=space){
                    amt+=1;
                }
            }else{amt = 1; }
            if(scc>=0){
                scc-=amt;
            }
            if(scc<0){
                if(scr>0){
                    scr-=1;
                    scc = array_length_2d(textArray, scr)-1;
                    if(!_selectAfterNewline){
                        _selectAfterNewline = 1;
                    }else{
                        _selectAfterNewline = 0;
                        scc-=1;
                    }
                }else{
                    scc = -1;
                }
            }else{
                _selectAfterNewline = 0;
            }
        }else{
            if(isCtrl){
                // Find the nearest previous space or line
                amt =1;
                while(cc>=amt && textArray[cr, cc-amt]!=space){
                    amt+=1;
                }
            }else{amt = 1; }
            if(cc>=0){
                cc-=amt;
            }
            if(cc<0){
                if(cr>0){
                    cr-=1;
                    cc = array_length_2d(textArray, cr)-1;
                    if(!_cursorAfterNewline){
                        _cursorAfterNewline = 1;
                    }else{
                        _cursorAfterNewline = 0;
                    }
                }else{
                    cc = -1;
                }
            }else{
                _cursorAfterNewline = 0;
            }
            _selectCursorPos = -1;
        }
    
    } else
    /////////////////////////
    // Right Pressed
    if(isRight){
        var amt = 0;
        if(isShift){
            var rowLength = array_length_2d(textArray, scr);
            var arrayHeight = array_height_2d(textArray);
            if(isCtrl){
                // Find the nearest previous space or line
                amt =1;
                while(scc+amt<rowLength && textArray[scr, scc+amt]!=space){
                    amt+=1;
                }
            }else{amt = 1; }
            scc+=amt;
            if(scc>=rowLength){
                if(scr<arrayHeight-1){
                    scr+=1;
                    scc = -1;
                    if(!_selectAfterNewline){
                        _selectAfterNewline = 1;
                    }else{
                        _selectAfterNewline = 0;
                        scc+=string_length(nl);
                    }
                }else{
                    scc = rowLength-1;
                    _selectAfterNewline = 0;
                }
            }else{ _selectAfterNewline = 0; }
        }else{
            var rowLength = array_length_2d(textArray, cr);
            var arrayHeight = array_height_2d(textArray);
            if(isCtrl){
                // Find the nearest previous space or line
                amt =1;
                while(cc+amt<rowLength && textArray[cr, cc+amt]!=space){
                    amt+=1;
                }
            }else{amt = 1; }
            cc+=amt;
            if(cc>=rowLength){
                if(cr<arrayHeight-1){
                    cr+=1;
                    cc = -1;
                    if(!_cursorAfterNewline){
                        _cursorAfterNewline = 1;
                    }else{
                        _cursorAfterNewline = 0;
                    }
                }else{
                    cc = rowLength-1;
                    _cursorAfterNewline = 0;
                }
            }else{ _cursorAfterNewline = 0; }
            _selectCursorPos = -1;
        }
    }
    else
    /////////////////////////
    // Up Pressed
    if(isUp){
        if(isShift){
            var amt = 1;
            if(scr>0){
                scr-=1;
                var rowLength = array_length_2d(textArray, scr);
                if(scc>rowLength-1){
                    scc = rowLength-1;
                }
            }else{
                scc = -1;
            }
            if(scc<0){
                if(scr>0){
                    scr-=1;
                    scc = array_length_2d(textArray, scr)-1;
                }else{
                }
            }
        }else{
            var amt = 1;
            if(cr>0){
                cr-=1;
                var rowLength = array_length_2d(textArray, cr);
                if(cc>rowLength-1){
                    cc = rowLength-1;
                }
            }else{
                cc = -1;
            }
            if(cc<0){
                if(cr>0){
                    cr-=1;
                    cc = array_length_2d(textArray, cr)-1;
                }else{
                }
            }
            _selectCursorPos = -1;
        }
    }
    else
    /////////////////////////
    // Down Pressed
    if(isDown){
        var arrayHeight = array_height_2d(textArray);
        if(isShift){
            if(scr<=arrayHeight-2){
                scr+=1;
                var rowLength = array_length_2d(textArray, scr);
                if(scc>rowLength-1){
                    scc = rowLength-1;
                }
            }else{
                scc = array_length_2d(textArray, scr)-1;
                _selectAfterNewline = 0;
            }
        }else{
            if(cr<=arrayHeight-2){
                cr+=1;
                var rowLength = array_length_2d(textArray, cr);
                if(cc>rowLength-1){
                    cc = rowLength-1;
                }
            }else{
                cc = array_length_2d(textArray, cr)-1;
                _cursorAfterNewline = 0;
            }
            _selectCursorPos = -1;
        }
    } else
    /////////////////////////
    // Home Pressed
    if(isHome){
        if(isShift){
            _selectAfterNewline = 1;
            if(isCtrl){
                scc = -1;
                scr = 0;
            }else{
                scc = -1;
            }
        }else{
            _cursorAfterNewline = 1;
            if(isCtrl){
                cc = -1;
                cr = 0;
            }else{
                cc = -1;
            }
            _selectCursorPos = -1;
        }
    
    }else
    /////////////////////////
    // End Pressed
    if(isEnd){
        var arrayHeight = array_height_2d(textArray);
        if(isShift){
            _selectAfterNewline = 0;
            var rowLength = array_length_2d(textArray, scr);
            if(isCtrl){
                scr = arrayHeight-1;
                scc = array_length_2d(textArray, scr)-1;
            }else{
                scc = rowLength-1;
            }
        }else{
            _cursorAfterNewline = 0;
            var rowLength = array_length_2d(textArray, cr);
            if(isCtrl){
                cr = arrayHeight-1;
                cc = array_length_2d(textArray, cr)-1;
            }else{
                cc = rowLength-1;
            }
            _selectCursorPos = -1;
        }
    }else
    /////////////////////////
    // Ctrl+A Pressed
    if(isCtrl and isA){
        cc = -1; cr = 0;
        _cursorPos = 0; _selectCursorPos = string_length(bt);
        scr = array_height_2d(textArray)-1;
        scc = array_length_2d(textArray, scr)-1;

    }
    /////////////////////////
    // Undo/Redo
    if(isCtrl && isZ && useUndo){
        undoLock = 1;
        if(isShift){
            // Step foward
            if(_textUndoPos<ds_list_size(_textUndoList)-1){
                _textUndoPos+=1;
                if(_textUndoPos>=ds_list_size(_textUndoList)){
                    _textUndoPos = ds_list_size(_textUndoList)-1;
                }
            }
            bt=ds_list_find_value(_textUndoList,_textUndoPos);
        }else{
            _textUndoPos-=1;
            var s = ds_list_find_value(_textUndoList,_textUndoPos);
            if(!is_undefined(s)){
                bt=s;
            }
            if(_textUndoPos<0) _textUndoPos = 0;
        }
        if(_cursorPos>string_length(bt)){
            _cursorPos = string_length(bt);
        }
        if(_selectCursorPos>string_length(bt)){
            _selectCursorPos = -1;
        }
    }

//////////////////////////////////////////////////
//////////////////////////////////////////////////
    /////////////////////////
    // Selection Position
    if(scc<0) _selectAfterNewline = 1;
    var tracker = 0;
    var trR = 0;
    while(trR<scr){
        var rowLength = array_length_2d(textArray, trR);
        trR+=1;    
        tracker+=rowLength;
    }
    tracker+=scc+1;
    if(isShift)
        _selectCursorPos = tracker;
 
    /////////////////////////
    // Cursor Position
    if(cc<0){ _cursorAfterNewline = 1; }
    var tracker = 0;
    var trR = 0;
    while(trR<cr){
        var rowLength = array_length_2d(textArray, trR);
        trR+=1;    
        tracker+=rowLength;
    }
    tracker+=cc+1;
    _cursorPos = tracker;
    
    /////////////////////////
    // Add Delay
    if(_txtBoxDelay<-1){
        _txtBoxDelay = firstDelayFactor*repeatDelay;
        if( (isCtrl || isShift) &&
        !(isLeft || isRight
        || isUp || isDown
        || isHome || isEnd
        || isX || isC || isV || isA || isZ)
        ){
            _txtBoxDelay = -4;
        }
    }else{
        _txtBoxDelay = repeatDelay;
    }
}else{
    /////////////////////////
    // Remove delay
    if(keyboard_check_released(vk_anykey)){
        _txtBoxDelay=-4;
    }else{
        if(_txtBoxDelay>0 && isAny){
            _txtBoxDelay -= 1;
        }else{
            if(!isAny){
                _txtBoxDelay=-4;
            }else{
                _txtBoxDelay=0;
            }
        }
 
    }
}
// Add an undo state
if(obt!=bt && !undoLock && useUndo){
    while(_textUndoPos<ds_list_size(_textUndoList)-1){
        ds_list_delete(_textUndoList,_textUndoPos+1);
    }
    ds_list_add(_textUndoList,bt)
    _textUndoPos = ds_list_size(_textUndoList)-1;
}
return bt; // Is it safe to come out now?
//////////////////////////////////////////////////
Useage: Set an instance variable as a string--maybe like... text= "";
Call the script in the draw event of an object:
draw_input(x,y,width,height,caption,text,limit)

A thing to possibly do~~~
Something that I do in "persistent" scripts like this--kinda odd, I guess, but try it....
You'll notice that this script pollutes your global scope. It sets like...11? local variables and uses them to track things from one call to the next. Also means that you can't do multiple boxes onscreen at once. Easy solution: put the return value into index zero of an array, and pop the remaining locals into the return array as well instead of being local. Pass the whole array in (with optional parameters and defaults in the script), and when you want to get the result text it's just the zero index of the return. This'll let you run multiple boxes and not have any locals to clean up. Yay.
 
Last edited:

Tsa05

Member
Wow I can see you did a lot of work!
How many days did you spend on this?
Mmm, good question... I made this back in October, and worked on it over the course of 2 weeks, but I'm working on a much, much, much, much, much larger project (alluded to in sig) and this is a tiny fractional part of it. Definitely didn't spend 2 weeks of full-time work on it, but a few hours here and there over that time span.

Now that the bigger project is nearly ready, figured I'd toss this in here to see if anyone finds it useful, has suggestions, etc. (Mother of all textbox script is another component). Basically, the components that had me weighing lots of different implementation strategies seem like ripe candidates for discussion.
 

Lonewolff

Member
This is an awesome start!

Couple of probs I have noticed;

- pressing delete a bunch of times followed by typing causes the text to be displayed in reverse.
- pressing enter more than once will throw out the cursor position.
 

Tsa05

Member
Grrarg lol. Not sure why I neglected to add a clamp around backspace to prevent _cursorPos from becoming -1... Fixed that, easy-easy.

Enter key, hmmm. I'd removed Enter key at some point during testing; the box text-wraps, but doesn't do manually Entered enter keys. Adding Enter key back in was easy enough, but it throws off cursor position significantly because the script is counting upon all newlines being aesthetic only. I will take a look through when I've got time--since the script supports keyboard navigation, mouse-clicking, and selecting, each computation will have to be tweaked a bit. Suggestions welcome, of course!
 

Tsa05

Member
... said:
Definitely agree--way easier to track the position from line to line using a matrix--but still doesn't solve the directionality problem with word wrapping...
... said:
That's an interesting point; I probably should be separating the mouse position and cursor position computations to avoid the order-of-operations issue
... said:
Yea, I wasn't commenting as I went, so it got a little sloppy there. Will try to address

Ok, good discussion everyone, lots of great suggestions about how to implement. As we know, I didn't design this initially to do manual new line insertion, so all the cursor position stuff goes wonky when it's added. I've re-written the script basically from scratch using an entirely new method (breaking the text into an array, performing row/column movement, and then figuring out positions again). There's a fun little problem that comes up, and you'll see _selectAfterNewline and _cursorAfterNewline littered throughout. I don't love it, but it's there to solve a problem that you'll likely run into if making this sort of thing yourself... New version is now featured, old version is in this spoiler tag. (Too long to include both in one post)

Have a look at the OP--how else should I have solved the "after newline" issue?
Any bugs? I'm planning to actually include this as a tiny part of a future asset ;D

Code:
/// @description    draw_input(x,y,width,height,caption,text, limit)
/// @arg    x        The x-position of the box
/// @arg    y        The y-position of the box
/// @arg    width    The width of the box
/// @arg    height    The height of the box
/// @arg    caption    Caption to display in the box
/// @arg    text    Text to work on
/// @arg    limit    Max # of characters allowed in the box
/**************************************************************/
/*        Information
*    Developed by Tsa05 on the GameMaker forum
*    Use of the script is subject to the following conditions:
*        1) Leave the information section intact
*        2) Give this script as freely as it was received
*
*    This script should be called from a Draw event.
*    The return value should be continuously passed back in.
*    This script erases keyboard_string.
*    Some instance variables are used to track persistent info;
*    instance variables all start with underscore (_)
*    Text *will* overflow the box if your limit permits it.
*
*    Undo (Ctrl+Z)/Redo (Ctrl+Shift+Z) is available, but REQUIRES additional steps:
*        1) Set useUndo to 1 in the settings area below. Now you have a memory leak.
*        2) Somewhere in your game, when you decide to stop showing
*           this box, you've got to clear the histroy to solve the leak:
*        if(ds_exists(_textUndoList,ds_type_list)) ds_map_destroy(_textUndoList);
*
*    This script was designed for GMS2, and uses variable_instance_exists.
*    To use with GMS 1.4, you'd need to remove that section and define
*    those variables in a calling instance.
*/
var bx = argument[0];
var by = argument[1];
var bw = argument[2];
var bh = argument[3];
var bc = argument[4];
var bt = argument[5]; // Whole input string
var bl = argument[6];

/******* Settings ********/
var c_border = make_color_rgb(44,44,44);
var c_fill   = make_color_rgb(70,70,70);
var c_text   = c_white;
var c_select = c_white;

var repeatDelay = room_speed/20;
var firstDelayFactor = 10;
var doubleClick = room_speed/3;
var blinkSpeed = 5; // Higher number == slower cursor blink
var hintOpacity = .15; //Opacuty of the hint cursor. Zero for no hint
var useUndo = 0; // Should undo/redo exist?
var nl = "\n";    // Linebreak character
/*************************/
/*    Keys                **/
var isLeft        = keyboard_check(vk_left);
var isRight        = keyboard_check(vk_right);
var isUp        = keyboard_check(vk_up);
var isDown        = keyboard_check(vk_down);
var isShift        = keyboard_check(vk_shift);
var isDel        = keyboard_check(vk_delete);
var isBack        = keyboard_check(vk_backspace);
var isCtrl        = keyboard_check(vk_control);
var isHome        = keyboard_check(vk_home);
var isEnd        = keyboard_check(vk_end);
var isAny        = keyboard_check(vk_anykey);
var isEnter        = keyboard_check(vk_enter);
var isC            = keyboard_check(ord("C"));
var isV            = keyboard_check(ord("V"));
var isX            = keyboard_check(ord("X"));
var isA            = keyboard_check(ord("A"));
var isZ            = keyboard_check(ord("Z"));
/*************************/
/*    Mouse                **/
var isPressed  = mouse_check_button_pressed(mb_left);
var isHeld     = mouse_check_button(mb_left);
var isReleased = mouse_check_button_released(mb_left);
/*************************/

// Set up things this script needs
var pad = 3;
var okbs = keyboard_string;
var wrapped = bt;
keyboard_string = "";
var tbw = bw-2*pad - 6*pad;
var op = 0; // Current opacity for mouse hint cursor
var obt = bt;
var undoLock = 0;
var offset = 0;    // Offset used to track how many visual linebreaks added before cursor

/******* Define Locals ********/
if(!variable_instance_exists(id,"_txtBoxDelay")){
    _txtBoxDelay = -4;
}
if(!variable_instance_exists(id,"_afterTextBoxNewline")){
    _afterTextBoxNewline = 0;
}
if(!variable_instance_exists(id,"_cursorPos")){
    _cursorPos = string_length(bt);
}
if(!variable_instance_exists(id,"_selectCursorPos")){
    _selectCursorPos = -1;
}
if(!variable_instance_exists(id,"_dragCursor")){
    _dragCursor = -1;
}
if(!variable_instance_exists(id,"_doubleClickTimer")){
    _doubleClickTimer = -1;
}
if(!variable_instance_exists(id,"_doubleClickLock")){
    _doubleClickLock = -1;
}
if(!variable_instance_exists(id,"_textBlinkCounter")){
    // I hate to track something this mundane, but
    // there's a subtle need for blink to reset on keypress
    _textBlinkCounter = 0;
}

if(useUndo){
    if(!variable_instance_exists(id,"_textUndoList")){
        _textUndoList = ds_list_create();
        ds_list_add(_textUndoList,bt);
    }
    if(!variable_instance_exists(id,"_textUndoPos")){
        _textUndoPos = 0;
    }
}
/***************************/
_textBlinkCounter+=1;
if(isAny||isHeld){
    _textBlinkCounter=0;
}
// Due to a GM bug, deleting zero characters from a string wrecks everything
// For now...
if(_selectCursorPos==_cursorPos) _selectCursorPos = -1;

if(_selectCursorPos>=0 && okbs!=""){
    if(_cursorPos<_selectCursorPos){
        var c1 = _cursorPos+1;
        var c2 = _selectCursorPos+1;
        var af = 1;
    }else{
        var c2 = _cursorPos+1;
        var c1 = _selectCursorPos+1;
        var af = 0;
    }
    var len = c2-c1;
 
    bt = string_delete(bt,c1,len);
    if(!af){
        _cursorPos-=len;
    }
    _selectCursorPos = -1;
}
// Handle text addition/insertion
if(string_length(bt+okbs)<bl){
    bt = string_insert(okbs,bt,_cursorPos+1);
    _cursorPos+=string_length(okbs);
}else{
    // Add character by character
    var tkbs = okbs;
    while(string_length(bt)<bl){
        bt = string_insert(string_char_at(tkbs,1),bt,_cursorPos+1);
        tkbs=string_delete(tkbs,1,1);
        _cursorPos+=1;
    }
}
if(string_length(bt)>bl){
    bt = string_copy(bt,1,bl);
    if(_cursorPos>string_length(bt)){
        _cursorPos = string_length(bt);
    }
}

var cur = _cursorPos; // Modify for line breaks
var altCur = _cursorPos;  // Show the mouse position
var sCurPos = _selectCursorPos;

// Insert line breaks as needed into bt
var breakPos = 1;
var miniPos = 1;
// Format wrapping
while(string_width(wrapped)>tbw && breakPos<string_length(wrapped)){
    var mini = string_copy(wrapped,miniPos,(breakPos-miniPos));
    if(string_width(mini)>tbw){
        wrapped = string_insert(nl, wrapped, breakPos);
        if(breakPos<=cur){
            cur+=1;
            offset+=1;
        }
        if(_selectCursorPos>=0 && breakPos<=sCurPos){
            sCurPos+=1;
        }
     
        miniPos = breakPos;
    }else{
        breakPos+=1;
    }
 
}


draw_set_color(c_fill);
draw_roundrect_ext(bx,by,bx+bw,by+bh, 2*pad, 2*pad, 0);
draw_set_color(c_border);
for(var z=0;z<pad;z+=1){
    draw_roundrect_ext(bx+z,by+z,bx+bw-z,by+bh-z, 2*pad, 2*pad, 1);
}
draw_set_color(c_text);

bx+=3*pad;

if(bc!=""){ // Draw a caption
    draw_text(bx, by+3*pad, bc);
    by+=3*pad+string_height(bc)+pad;
}
var cx = bx;
var cy = by;


draw_text(bx, by, wrapped);

/* Solve mouse */
var mx = mouse_x; var my = mouse_y;
if(mx>bx-string_width("m")&&mx<bx+string_width(wrapped)+string_width("m") &&
my>by&&my<by+string_height(wrapped) ){
    op=hintOpacity;
    var tempCur = 0;
    var tempCurX = bx;
    var tempCurY = by;
    var tempStr = "";
 
    while((tempCurX<mx || tempCurY<my) && tempCur<string_length(wrapped) ){
            tempCur+=1;
            tempStr = string_copy(wrapped,1,tempCur);
            var ts = tempStr;
            while(string_pos(nl,ts)){
                ts = string_delete(ts,1,string_pos(nl,ts));
            }
            var adjust = .3*string_width(string_char_at(ts,string_length(ts)));
            tempCurX=bx+string_width(ts) + adjust;
            tempCurY=by+string_height(tempStr);
    }
    if(mx<bx){ // Adjust for line starters
        tempCur-=1;
    }
    altCur = tempCur;

    if(isHeld){
        if(!_dragCursor){
            // Set the cursor
         
            if(isShift){
                // Fresh click in a new place, but moving selection
                _selectCursorPos = tempCur-string_count(nl,tempStr);
            }else{
                // If it's just an innocent little click, clear selections
                // and start a new drag        
                _cursorPos = tempCur-string_count(nl,tempStr);
                cur = tempCur;
                if(mx<bx){ // Adjust for line starters
                    _afterTextBoxNewline = 1;
                }else{
                    _afterTextBoxNewline = 0;
                }

                op=0;
                _dragCursor = 1;
                _selectCursorPos = -1;
            }
        }else{
            // Move the selection cursor
            if(_doubleClickTimer>0){
                // This click was within doubleClick time and place.
                _doubleClickTimer = -1;
                // Find space before
                var pc = string_copy(bt,1,_cursorPos);
                var p1 = 0;
                while(string_pos(" ",pc)){
                    var p1 = string_pos(" ",pc);
                    pc = string_replace(pc," ","_");
                }
                // Find space after
                var pc = string_delete(bt,1,_cursorPos);
                var p2 = string_pos(" ",pc)-1;
                if(p2<0){
                    _selectCursorPos = string_length(bt);
                }else{
                    _selectCursorPos = _cursorPos + p2;
                 
                }
                _cursorPos = p1;
                _doubleClickLock = 1;
            }else if(!_doubleClickLock){
                _selectCursorPos = tempCur-string_count(nl,tempStr);
            }
        }
    }
     
}
if(!isHeld){
    _dragCursor=-1;
    _doubleClickLock = -1;
}
if(_doubleClickTimer>0){
    _doubleClickTimer-=1;
}
if(isReleased){
    _doubleClickTimer = doubleClick;
}
/* Draw cursor */
if(string_char_at(wrapped, cur+1)==nl && _afterTextBoxNewline){
    if(_selectCursorPos<0){
        cur+=1;
    }
    altCur+=1;
}
var curString = string_copy(wrapped,1,cur);
while(string_pos(nl, curString)){
    var temp = string_copy(curString,1,string_pos(nl, curString)-1);
    var lh = string_height(temp);
    if(lh==0) lh = string_height("|");
    cy+=lh;
    curString = string_delete(curString,1,string_pos(nl, curString));
}
cx+=string_width(curString);
var h = string_height(curString);
if(h==0) h=string_height("|");
a = round(.5*cos(_textBlinkCounter*(room_speed/blinkSpeed))+.5);
draw_set_alpha(a);
draw_set_color(c_select);
draw_line(cx,cy,cx,cy+h);
draw_set_alpha(1);

/* Draw Mouse Hint cursor */
if(op>0){
    var hx = bx;
    var hy = by;
    var curString = string_copy(wrapped,1,altCur);
    while(string_pos(nl, curString)){
        var temp = string_copy(curString,1,string_pos(nl, curString)-1);
        hy+=string_height(temp);
        curString = string_delete(curString,1,string_pos(nl, curString));
    }
    hx+=string_width(curString);
    var h = string_height(curString);
    if(h==0) h=string_height("|");
    draw_set_alpha(op);
    draw_line(hx,hy,hx,hy+h);
    draw_set_alpha(1);
}

/* Draw Selection */
sCurPos = 0;
var s = 0;
while(s<_selectCursorPos && _selectCursorPos>=0){ // Find the selection position with wrapping
    sCurPos+=1;
    if(string_char_at(wrapped,sCurPos)!=nl){
        s+=1;
    }
}
if(_selectCursorPos>=0){
    var sx = bx;
    var sy = by;
 
    draw_set_color(c_select);
 
    if(string_char_at(wrapped, sCurPos-1==nl)) sCurPos+=1;
    var curString = string_copy(wrapped,1,sCurPos);
 
    while(string_pos(nl, curString)){ // Find coords
        var temp = string_copy(curString,1,string_pos(nl, curString)-1);
        sy+=string_height(temp);
        curString = string_delete(curString,1,string_pos(nl, curString));
    }
    sx+=string_width(curString);
    var h = string_height(curString);
    if(h==0) h=string_height("|");
 
    if(isHeld) draw_circle(sx,sy,2,0);
 
    if(sCurPos<=cur){
        var x1 = sx; var y1 = sy;
        var x2 = cx; var y2 = cy+h;
        var c1 = sCurPos;
        var c2 = cur;
    }else{
        var x1 = cx; var y1 = cy;
        var x2 = sx; var y2 = sy+h;
        var c1 = cur;
        var c2 = sCurPos;
    }
    var temp = string_copy(wrapped,c1+1,c2-c1);
    while(string_pos(nl,temp)){
        var subStr = string_copy(temp,1,string_pos(nl,temp));
        var h = string_height(subStr);
        if(string_width(subStr)!=0){
            // Condition set to fix the fact that the
            // selection technically starts at the end
            // of the previous line
            draw_rectangle(x1,y1,x1+string_width(subStr),y1+h,1);
        }
        temp = string_delete(temp,1,string_pos(nl,temp));
        y1+=h; x1=bx;
    }
    draw_rectangle(x1,y1,x2,y2,1);
}

/* Deal with keyboard */
if(isAny && _txtBoxDelay<=0){

    if(isCtrl ){
 
        if(isLeft){
            // Move left 1 word
         
            if(isShift){
                if(_selectCursorPos>=0){
                    var curToUse = _selectCursorPos;
                }else{
                    var curToUse = _cursorPos;
                }
            }else{
                var curToUse = _cursorPos;
            }
            curToUse-=1;
            var newCur = curToUse;
         
            var curLet = string_char_at(bt,newCur);
            while(!(curLet==" " || curLet==nl) && newCur>0){
                newCur-=1;
                curLet = string_char_at(bt,newCur);
            }
            if(isShift){
                _selectCursorPos = newCur;
                if(_selectCursorPos<0) _selectCursorPos = 0;
            }else{
                _cursorPos = newCur;
                if(_cursorPos<0) _cursorPos = 0;
                _selectCursorPos = -1;
            }
        }else if(isRight){
            // Move right 1 word
         
            if(isShift){
                if(_selectCursorPos>=0){
                    var curToUse = _selectCursorPos;
                }else{
                    var curToUse = _cursorPos;
                }
            }else{
                var curToUse = _cursorPos;
            }
            curToUse+=1;
            var newCur = curToUse;

            var curLet = string_char_at(bt,newCur);
            while( !(curLet==" " || curLet==nl) && newCur<string_length(wrapped)){
                newCur+=1;
                curLet = string_char_at(bt,newCur);
            }
            if(isShift){
                _selectCursorPos = newCur;
                if(_selectCursorPos>string_length(bt)) _selectCursorPos = string_length(bt);
            }else{
                _cursorPos = newCur;
                if(_cursorPos>string_length(bt)) _cursorPos = string_length(bt);
                _afterTextBoxNewline = 1;
                _selectCursorPos = -1;
            }
        }else if(isA){
            _cursorPos=0;
            _selectCursorPos=string_length(bt);
        }else if(isZ && useUndo){
            // Undo/Redo
            undoLock = 1;
            if(isShift){
                // Step foward
                if(_textUndoPos<ds_list_size(_textUndoList)-1){
                    _textUndoPos+=1;
                    if(_textUndoPos>=ds_list_size(_textUndoList)){
                        _textUndoPos = ds_list_size(_textUndoList)-1;
                    }
                }
                bt=ds_list_find_value(_textUndoList,_textUndoPos);
            }else{
                _textUndoPos-=1;
                var s = ds_list_find_value(_textUndoList,_textUndoPos);
                if(!is_undefined(s)){
                    bt=s;
                }
                if(_textUndoPos<0) _textUndoPos = 0;
            }
            if(_cursorPos>string_length(bt)){
                _cursorPos = string_length(bt);
            }
            if(_selectCursorPos>string_length(bt)){
                _selectCursorPos = -1;
            }
        }else{
            // Handle C, V, and X
            // X is a combo of c and v
     
            if(isC || isX){
                // Copy
                if(_selectCursorPos>=0){
                    if(_cursorPos<_selectCursorPos){
                        var c1 = _cursorPos+1;
                        var c2 = _selectCursorPos+1;
                        var af = 1;
                    }else{
                        var c2 = _cursorPos+1;
                        var c1 = _selectCursorPos+1;
                        var af = 0;
                    }
                    var len = c2-c1;
                    var newStr = string_copy(bt,c1,len);
                    clipboard_set_text(newStr);
                }
            }
     
            if(isV || isX){
                // Paste
                if(clipboard_has_text()){
                    if(_selectCursorPos>=0){ // Trim out selected text, if any
                        if(_cursorPos<_selectCursorPos){
                            var c1 = _cursorPos+1;
                            var c2 = _selectCursorPos+1;
                            var af = 1;
                        }else{
                            var c2 = _cursorPos+1;
                            var c1 = _selectCursorPos+1;
                            var af = 0;
                        }
                        var len = c2-c1;
 
                        bt = string_delete(bt,c1,len);
                        if(!af){
                            _cursorPos-=len;
                        }
                        _selectCursorPos = -1;
                    }
             
                    if(isV){
                                         
                        // Add new text
                        var newStr = clipboard_get_text();
                        if(string_length(bt+newStr)<bl){
                            bt = string_insert(newStr,bt,_cursorPos+1);
                            _cursorPos+=string_length(newStr);
                        }else{
                            // Add character by character
                            var ts = newStr;
                            while(string_length(bt)<bl){
                                bt = string_insert(string_char_at(ts,1),bt,_cursorPos+1);
                                ts=string_delete(ts,1,1);
                                _cursorPos+=1;
                            }
                        }
                    }
                }
            }
        }
    }else if(isBack ){
        // Is there a selection?
        if(_selectCursorPos>=0){
            if(_cursorPos<_selectCursorPos){
                var c1 = _cursorPos+1;
                var c2 = _selectCursorPos+1;
                var af = 1;
            }else{
                var c2 = _cursorPos+1;
                var c1 = _selectCursorPos+1;
                var af = 0;
            }
            var len = c2-c1;
 
            bt = string_delete(bt,c1,len);
            if(!af){
                _cursorPos-=len;
            }
            _selectCursorPos = -1;
        }else{
            bt=string_copy(bt,1,_cursorPos-1)+string_delete(bt,1,_cursorPos);
            _cursorPos-=1;
            if(_cursorPos<0) _cursorPos = 0;
        }
    }else if(isDel ){
        if(_selectCursorPos>=0){
            if(_cursorPos<_selectCursorPos){
                var c1 = _cursorPos+1;
                var c2 = _selectCursorPos+1;
                var af = 1;
            }else{
                var c2 = _cursorPos+1;
                var c1 = _selectCursorPos+1;
                var af = 0;
            }
            var len = c2-c1;
            if(!af){
                _cursorPos-=len;
            }
            bt=string_delete(bt,c1,len);
            _selectCursorPos = -1;
        }else{
            bt=string_copy(bt,1,_cursorPos)+string_delete(bt,1,_cursorPos+1);
        }
    }else if(isLeft){
        if(isShift){
            if(_selectCursorPos<0){
                _selectCursorPos = _cursorPos-1;
            }else if(_selectCursorPos!=0){
                _selectCursorPos-=1;
            }
        }else{
            if(_selectCursorPos>=0&&_selectCursorPos<_cursorPos){
                _cursorPos = _selectCursorPos-1;
            }else{
                _cursorPos-=1;
            }
            _selectCursorPos=-1;
            if(_cursorPos<0) _cursorPos = 0;
            _afterTextBoxNewline = 1;
        }    
    }else if(isRight ){
        if(isShift){
            if(_selectCursorPos<0){
                _selectCursorPos = _cursorPos+1;
            }else if(_selectCursorPos!=string_length(bt)){
                _selectCursorPos+=1;
            }
        }else{
            if(_selectCursorPos>=0&&_selectCursorPos>_cursorPos){
                _cursorPos = _selectCursorPos+1;
            }else{
                _cursorPos+=1;
            }
            _selectCursorPos=-1;
            if(_cursorPos>string_length(bt)) _cursorPos = string_length(bt);
            _afterTextBoxNewline = 0;
        }
    }else if(isUp ){
        // Find the nearest linebreak
     
        var ts = wrapped; var tp = 0;
        var ts = string_copy(wrapped,1,cur);
        if(string_count(nl,ts)>=1){
            while(string_count(nl,ts)>1){
                var p = string_pos(nl,ts);
                tp+=p-1;
                ts = string_delete(ts,1,p);
            }
            var p = string_pos(nl,ts);
            if(p){
                if(isShift){
                    if(_selectCursorPos>0){
                        _selectCursorPos-=(p-1);
                        if(_selectCursorPos<0) _selectCursorPos = 0;
                    }else{
                        _selectCursorPos = _cursorPos-(p-1);
                        if(_selectCursorPos<0) _selectCursorPos = 0;
                    }
                }else{
                    _cursorPos-=(p-1);
                    _selectCursorPos = -1;
                }
            }
        }
     
        if(_cursorPos<0) _cursorPos = 0;
    }else if(isDown ){
        // Find the nearest linebreak
        var ts = wrapped; var tp = 0;
        if(string_count(nl,ts)>=1){
            while(string_count(nl,ts)>1){
                var p = string_pos(nl,ts);
                tp+=p-1;
                ts = string_delete(ts,1,p);
            }
            var p = string_pos(nl,ts);
            if(p){
                if(isShift){
                    if(_selectCursorPos>=0){
                        _selectCursorPos+=(p-1);
                        if(_selectCursorPos>string_length(bt)) _selectCursorPos = string_length(bt);
                    }else{
                        _selectCursorPos = _cursorPos+(p-1);
                        if(_selectCursorPos>string_length(bt)) _selectCursorPos = string_length(bt);
                    }
                }else{
                    _cursorPos+=(p-1);
                    _selectCursorPos = -1;
                }
            }
        }
        if(_cursorPos>string_length(bt)) _cursorPos = string_length(bt);
     
    }else if(isHome ){
        if(_selectCursorPos>=0){
            var curToUse = sCurPos;
        }else{
            var curToUse = cur;
        }
        var ts = string_copy(wrapped,1,curToUse); var tp = 0;
        while(string_count(nl,ts)>0){
            var p = string_pos(nl,ts);
            tp+=p-1;
            ts = string_delete(ts,1,p);
        }
        if(isShift){
            _selectCursorPos = tp;
            _afterTextBoxNewline = 1;
        }else{
            _cursorPos = tp;
            _afterTextBoxNewline = 1;
            _selectCursorPos = -1;
        }
    }else if(isEnd ){
     
        if(_selectCursorPos>=0){
            var curToUse = sCurPos;
        }else{
            var curToUse = cur;
        }
 
        if(_afterTextBoxNewline && string_char_at(wrapped,curToUse)==nl){
            curToUse+=1;
        }
        var ts = wrapped; var tp = 0;
        var p = string_pos(nl,ts);
        var count = 0;
        while(p && p<curToUse){
            ts = string_replace(ts,nl,"_");
            count+=1;
            var p = string_pos(nl,ts);
        }
        var newPos;
        if(p){
            newPos = p-1-count;
        }else{
            newPos = string_length(ts)-count;
        }
        if(isShift){
            _selectCursorPos = newPos;
        }else{
            _cursorPos = newPos;
            _afterTextBoxNewline = 0;
        }
    }else if(isEnter){
        bt = string_insert(nl,bt,_cursorPos+1);
        var l = string_length(nl);
        _cursorPos+= l;
    }
 
    if(_txtBoxDelay<-1){
        _txtBoxDelay = firstDelayFactor*repeatDelay;
        if( (isCtrl || isShift) &&
        !(isLeft || isRight
        || isUp || isDown
        || isHome || isEnd
        || isX || isC || isV || isA || isZ)
        ){
            _txtBoxDelay = -4;
        }
    }else{
        _txtBoxDelay = repeatDelay;
    }
}else{
    if(keyboard_check_released(vk_anykey)){
        _txtBoxDelay=-4;
    }else{
 
        if(_txtBoxDelay>0 && isAny){
            _txtBoxDelay -= 1;
        }else{
            if(!isAny){
                _txtBoxDelay=-4;
            }else{
                _txtBoxDelay=0;
            }
        }
 
    }
}
if(obt!=bt && !undoLock && useUndo){
    while(_textUndoPos<ds_list_size(_textUndoList)-1){
        ds_list_delete(_textUndoList,_textUndoPos+1);
    }
    ds_list_add(_textUndoList,bt)
    _textUndoPos = ds_list_size(_textUndoList)-1;
}
return bt; // Phew
 

Lonewolff

Member
Aww, doesn't work for me. :(

trying to index a variable which is not an array
at gml_Script_draw_input (line 464) - var a = string_length(textArray[cr, cc])-1;
Code:
/// Create

text = "";
width = 800;
height = 600;
caption = "Text box";
limit = 10000;
Code:
/// Draw

draw_input(x,y,width,height,caption,text,limit);
 

FrostyCat

Member
The only thing I ask for from a textbox "script" is API-level native functionality and text rendering capabilities, with full interoperability with IMEs. But as long as you insist on a GML-only solution, chances are you could do this all year and still go nowhere.
 
Last edited:

Tsa05

Member
Aww, doesn't work for me.
GMS1 or 2? It's written for 2, might be some issue in the conversion? Also are you saving the output as the input?
Code:
w=300;
h=150;
limit = 200;
caption = "Enter text below:";
myText = "this text is for test\ning breaking lines and then also multiple lines of text. Now we're even trying going over to another sentence.";
Code:
myText = draw_input(x,y,w,h,caption,myText, limit);
 

Tsa05

Member
Oh, geez, lol, Nvm, @Ghost in the IDE.
Look at line 105 in your script (_cursorPos=12)
Delete line 105.
Fixed!!! I checked the OP and...I had a debug line stuck in there setting the starting position to 12. Naturally, wouldn't work with your empty string. Removed from OP to match my code.
 

Lonewolff

Member
Cool, I'll check it out.

Yeah, using GMS2. Seems to not work at all in GMS1 due to slight advancements in arrays in 2.1.


The only thing I ask for from a textbox "script" is API-level native functionality and text rendering capabilities, with full interoperability with IMEs. But as long as you insist on a GML-only solution, chances are you could do this all year and still go nowhere.
Is this really the job of a text box?

To me a text box should be a vanilla as possible. It's job is to process input.
 
Last edited by a moderator:

Lonewolff

Member
Still got some bugs in there, I'm afraid.
  • After pressing 'enter' the cursor remains at the end of the line (until next character is pressed).
  • When backspace is pressed, it requires a double backspace press to resume deleting, when the cursor moves up to the previous line.
  • When you backspace to the previous line, an additional backspace is required, otherwise the next character typed jumps to the next line.
  • No cursor at the start of the text box.
  • Occasionally a ghosted cursor is left behind after a a couple of presses of 'enter'. Couldn't find a consistent way to replicate this.
  • If too many lines are typed in the text will overflow the text area.
Other than that. Looking good :)
 

FrostyCat

Member
s this really the job of a text box?

To me a text box should be a vanilla as possible. It's job is to process input.
My definition is vanilla, it's just that most people stuck in the Western Hemisphere don't understand what it's like to use computers outside it. Unfortunately YoYo seems to be staffed with these people, and for years they denied and downplayed GML's problems in rendering and accepting text in non-Western locales. The only solace is that native message box functions like get_string() could still get something done, just not as pretty.

One of the jobs for a text box is to interoperate with input technologies. That's what the "I" in IME means. I don't expect most people in the Western Hemisphere to get this because they don't usually use one overtly, but anyone learning or speaking a CJK language will tell you that IMEs don't behave properly with GML-drawn fake textboxes. The input previews appear off to one side. keyboard_string has come some way since legacy days, but on some platforms it either doesn't work (e.g. mobile platforms) or are severely limited (e.g. HTML5). And don't even get me started with GML-implemented pretend on-screen keyboards. Compare with Word or even Notepad for an example of what proper IME-compliant behaviour is like.

Another job for a text box is to display text already inputted. That too every version of GM thus far handles pathetically outside the standard left-to-right realm. Brahmic scripts, right-to-left scripts (e.g. Arabic, Hebrew), and CTL fonts and scripts are all off-limits for built-in GML functions. That's all text, and that has a place in a text box. But if you are to handle them well, you probably won't be doing it within GML alone, and that's what I'm expressing this whole time.
 

Lonewolff

Member
Fair enough. I understand what you are getting at now :)

I must admit, us westerners often forget about the other language formats out there.

I'd hate to even hazard a guess as to what Visual Studio would look like if I tried to code in Hebrew or something.
 
Last edited by a moderator:

Tsa05

Member
I've updated the OP with a few small things that might make the box behave a little more like expected
  • After pressing 'enter' the cursor remains at the end of the line (until next character is pressed).
  • When backspace is pressed, it requires a double backspace press to resume deleting, when the cursor moves up to the previous line.
  • When you backspace to the previous line, an additional backspace is required, otherwise the next character typed jumps to the next line.
  • No cursor at the start of the text box.
  • Occasionally a ghosted cursor is left behind after a a couple of presses of 'enter'. Couldn't find a consistent way to replicate this.
  • If too many lines are typed in the text will overflow the text area.
  • Behavior dependent on whether you want the cursor after a newline on pressing Enter. Easy to adjust, I've done so in the OP
  • Not always. It's happening because you're testing multiple manual Enter. See below
  • Ok, easy enough, fixed in OP
  • Easy way to replicate: Place you mouse over the box. The "ghost cursor" is showing you where the mouse will insert when clicked.
  • Well, we're both right. Choose your box size wisely.
So, some things about the box; I know that there's an interest in getting it to work just like <nameOfFavoriteEditor>. These bugs--most of them aren't bugs; either they're small oversights or working as intended. I'm happy to tweak and adjust to some extent, but we're starting to move away from programming discussion (we can go there, but in that case this should have a marketplace or tutorial forum post if it's all one-way flow where I code and the community reports bugs).

I put this in here for a more specific reason--FrostyCat went instantly straight off-topic, or so I thought but maybe it's worth pointing out the glaringly obvious:
This isn't a platform native api hook. It's GML programming. In a GML forum, for GML programming discussion.

To my mind, a comment like "not interested unless it's a native extension" is off the topic because...just look at it. It should be entirely obvious that this is not a native extension. The forum topic isn't titled "Should a textbox be done in gml or should I use native?" And that's straight where Frosty went anyways. I anticipated this:
Yes, it yanks info from keyboard_string. Yes, it checks some specific vk presses. Yes, I bet there's os hooks and dlls and egos abound. But, here's something made in GML--where should we go from here?
Maybe I wasn't clear though? I get the limitations of doing this in GML, I acknowledge that native support would be superior in every way, I already covered the fact that YYG hasn't shown an interest in going there, and brought it around to "so here's a stab at it using what GMS gives us" to get a discussion going.

I was hoping some people would chip in on GML programming. While it's clever to say "use already-written native code" that just hops over the discussion of how to do "stuff" in GML. Some of the things I thought I might see discussion on:
  • Originally did everything using string position and substring length calculations. Some parts were a huge pain conceptually, others less so. Is this method preferable for expanding on a custom box?
  • Rewrote using arrays to compute everything. Some parts were a huge pain conceptually, others less so. Is this method preferable for expanding on a custom box?
  • Using local variables to track persistent box data pollutes the local scope and restricts to one box, but makes the script very pleasantly black-boxed. In my own work, I don't use this method, and I have another version of the script that reflects this difference. How would you do it? I know how I would...
  • I'm tracking "cursor after newline" as a variable. How should this be handled?
  • I've broken the computations apart in some places, combined them in others. I'm inserting, then removing, then computing coords, then drawing, then computing keyboard input...this can be re-jiggered many ways; what are the ups and downs?
Basically, this script for better or worse is an exploration of a kind of a task, performed using GML. Was looking for thoughts about how to approach the task, alternate strategies and their benefits, etc. Just looking to improve how I/we think about programming in GML through the expedient of a script provided herein. Cool?

Last mention, wrt the "double backspace issue" or such things. Notice how the script works. This is a quirky one. Consider the text:
Code:
"Hello\nWorld"
It should draw as:
Code:
Hello
World
If you're on the first line of text and you press End, the cursor moves after the o and before the newline.
If you're on the second line and press Home, the cursor moves before the W and after the newline.
Easy, right? That newline character is the pivot point. The cursor is either the space before it or the space after it.

Now consider
Code:
"HelloWorld"
But it's in a box that's only 5 characters wide. It should draw as:
Code:
Hello
World
If you're on the first line of text and you press End, the cursor moves after the o and before the...W
If you're on the second line and press Home, the cursor moves before the W and after...o
So in some cases, the cursor is before or after a "pivot point"--the newline--and in other cases, it's between 2 characters and has to decide which line that's supposed to mean. To that end, I'm using _cursorPos to track the position of the character after which to draw the cursor, which makes insertion/deletion super easy but loses the knowledge of whether to draw at the end of a line or the beginning of the next.

To address that, the script tracks actions that would cause the cursor to go towards the end of a line versus the beginning in _cursorAfterNewline. It keeps all of the computations extremely consistent, then just modifies the display coordinates if the cursor is supposed to appear up or down a line. In other words, without that variable, every time that you're at an artificial line break, the cursor appears at the end of the line. This means you can never see the cursor at the start of a line. With that variable, in all cases where that would happen, it jumps down and over.

The double-backspace is part of this. When you add a manual backspace, you have to newline that you added. When there's a fake backspace--added to wrap inside the box, backspace works "correctly?"

Yea, see, there's some quirky behaviors--maybe I planned this wrong? I made a method to deal with the quirkinesses of text; I'm hoping to see what other people suggest. Knowing that people are unlikely to feel inspired to code a whole textbox example to demonstrate, I did one to kick it off. ¯\_(ツ)_/¯
 

Lonewolff

Member
Yea, see, there's some quirky behaviors--maybe I planned this wrong? I made a method to deal with the quirkinesses of text; I'm hoping to see what other people suggest. Knowing that people are unlikely to feel inspired to code a whole textbox example to demonstrate, I did one to kick it off. ¯\_(ツ)_/¯
Yep, fair enough. I understand where you are coming from. :)

I started doing my own text boxes a little while back and yes, it is a very challenging task. Seems trivial until you attempt it.

I haven't read through your code I must admit, as it is massive and a lot to take in.

I must be handling newlines different to the way you are, as I don't store "\n" in the text at all. The renderer side of my code looks after all of that.

Sounds like your code processes the whole text chunk as a single string. Which I fear might give performance issues when you have tens of thousands of characters involved.
 

Tsa05

Member
After much soul-searching and several minutes of thinking, I have re-written the whole thing

Most important new feature:

Basically, script is now capable of handling vastly larger quantities of text without slowing too much.
So, I guess this is good? Essentially limitless text, no slow-down? Right?
There's even extra fancy stuff--custom "tokens" list for defining line break/Ctrl+arrow markers, for example.

But there's issues with this method, too. Anyone want to play with this?
Ok, first thing to note with new method: There's still no scrolling, but it would theoretically be much easier to add now. If anyone wants to put it in there, post here and I'll update (and thanks from the community). Otherwise, I'll...maybe get to it at some point when I need it?

Issue 1: It's slow unless you make it fast; then it leaks unless you fix that.
Major issue with prior implementation: draw_text() is slow compared to other functions. For every new line of text, there's more draw_text() calls. More text = more lag. Combining allll text into one multi-line draw_text() call speeds things up, but only marginally (10% for me). So, I implemented surfaces instead. In this way, drawing is basically "cached" in between typing, so unless you type faster than 60 keystrokes per second, this method gains frames.

The script takes care of freeing and re-creating surfaces while you type, but at some point when you finish using the box, the "last" surface used is not freed. This is a memory leak. The script returns the ID of that surface, but it cannot know when it will not be run again, so...you have to grab that surface ID and free it yourself when you are done with the script.

By default, the script is using a looped draw_text() drawing method. It's way faster than the old way and requires no memory cleanup, but slower than using surfaces. In the Settings area of the script, change useSurface to true.

Issue 2: Clicking and dragging is slow
I don't know what to do here... Clicking and dragging is slowerrrrrrrrrrrr than the rest. Basically, I've got to precisely figure out where you're clicking, as you're clicking. I'm pre-caching the size of each character in a row to reduce the horrendous time needed to run string_width() on everything, but the data lookups are now the slowest part of clicking and dragging. The rest is so optimal that there's basically 0 fps lost most of the time, but I'm losing 60% or more while dragging :(

That being said, here's what's new:
1) Strings are parsed only when needed, and the cached parse is passed into the script. This means that the script no longer returns a simple string--it returns a ds_map with cached info in it. It's trivial to recombine, though.

2) Text is cached into a list, with each line stored as a list entry. This solves a lot of complicated logic I'd done previously with the string method where I tried to keep track of the direction the cursor was moving and whether it was before or after the newlines in lines and stuff. Also makes it easier by far to define line length limits, or to ultimately add scrolling text.

3) The main script accepts either a string of text or a ds_map and returns a ds_map. The return value should be passed back into the script in order to skip extra text parsing.

Useage:
Create event
Code:
myText = "";
Draw event
Code:
// Set w and h to desired box dimensions
// Set caption to desired text @ top of box or ""
myText = draw_input(x,y,w,h,caption,myText);
This will cause myText to change from a string to a ds_map.
When you are done with the input box, you will need to retrieve the string from the structure before you delete it.
Code:
var txtList = myText[?"textRowList"];
finalText = reassemble_textRows(txtList);
That's basically it. But remember, you have to do cleanup stuff if you choose to set useUndo or useSurface to true.

The scripts: (Yes, could have been done as one script, but now that parsing can be asynchronous...)
NAMES OF THE SCRIPTS ARE IN THE COMMENTS
REMEMBER, text WILL overflow the box (invisibly) if you let it.
Code:
/// @description    draw_input(x,y,width,height,caption,text)
/// @arg    x        The x-position of the box
/// @arg    y        The y-position of the box
/// @arg    width    The width of the box
/// @arg    height    The height of the box
/// @arg    caption    Caption to display in the box
/// @arg    text    Text to work on

/**************************************************************/
/*        Information
*    Developed by Tsa05 on the GameMaker forum for use in SLIDGE
*    Use of the script is subject to the following conditions:
*        1) Leave the information section intact
*        2) Give this script freely
*
*    This script should be called from a Draw event.
*    The return value should be continuously passed back in.
*    This script erases keyboard_string.
*    An instance variable is used to track undo state
*
*    Text *will* overflow the box if your limit permits it.
*
*    Undo (Ctrl+Z)/Redo (Ctrl+Shift+Z) is available, but REQUIRES additional steps:
*        1) Set useUndo to 1 in the settings area below. Now you have a memory leak.
*        2) Somewhere in your game, when you decide to stop showing
*           this box, you've got to clear the histroy to solve the leak:
*        if(ds_exists(_textUndoList,ds_type_list)) ds_list_destroy(_textUndoList);
*
*    This script was designed for GMS2, and uses variable_instance_exists.
*    To use with GMS 1.4, you'd need to:
*        Modify the code that defines _textUndoList
*        Change the value of nl to "#"
*/
var bx = argument[0];
var by = argument[1];
var bw = argument[2];
var bh = argument[3];
var bc = argument[4];
var nf = argument[5]; // "Info"; this is text or the total box state ds_list

/******* Settings ********/
var useUndo = false;                // Should undo/redo exist?
var undoTime = room_speed/3;    // Delay after which to cature a state
var maxUndo = 20;                // How many undo levels to remember

var c_border = make_color_rgb(44,44,44);
var c_fill   = make_color_rgb(70,70,70);
var c_text   = c_white;
var c_select = c_white;
var c_cursor = c_white;

var autoAddCursor = true;    // Box is "active" by default
var repeatDelay = room_speed/20;
var firstDelayFactor = 10;
var doubleClick = room_speed/3;
var blinkSpeed = 5; // Higher number == slower cursor blink
var hintOpacity = .15; //Opacuty of the hint cursor. Zero for no hint
var nl = "\n";    // Linebreak character
var space = " "; // Space character
var showPreview = false;
var useSurface = false;
/*************************/
/*    Keys                **/
var isLeft        = keyboard_check(vk_left);
var isRight        = keyboard_check(vk_right);
var isUp        = keyboard_check(vk_up);
var isDown        = keyboard_check(vk_down);
var isShift        = keyboard_check(vk_shift);
var isDel        = keyboard_check(vk_delete);
var isBack        = keyboard_check(vk_backspace);
var isCtrl        = keyboard_check(vk_control);
var isHome        = keyboard_check(vk_home);
var isEnd        = keyboard_check(vk_end);
var isAny        = keyboard_check(vk_anykey);
var isEnter        = keyboard_check(vk_enter);
var isC            = keyboard_check(ord("C"));
var isV            = keyboard_check(ord("V"));
var isX            = keyboard_check(ord("X"));
var isA            = keyboard_check(ord("A"));
var isZ            = keyboard_check(ord("Z"));
var isMove        = (isHome||isEnd||isLeft||isRight||isUp||isDown);
var isRemove    = (isDel||isBack ||isX||isV);
var    isUndo        = (isCtrl&&isZ&&useUndo);
/*************************/
/*    Mouse                **/
var isPressed  = mouse_check_button_pressed(mb_left);
var isHeld     = mouse_check_button(mb_left);
var isReleased = mouse_check_button_released(mb_left);
/*************************/

// Set up things this script needs
var tokens = [" ", ".", ",", "?", "-", "[", "]", "=", ":", ";", "\\"];
var pad = 3;
var indent = 2;
var capH = 3*pad+string_height(bc)+pad;
var okbs = keyboard_string; keyboard_string="";
var tbw = bw-2*indent*pad;
var op = 0; // Current opacity for mouse hint cursor
var undoLock = 0;
var offset = 0;    // Offset used to track how many visual linebreaks added before cursor
var textHeight = string_height("|");
var obx = bx;
var oby = by;
var oMouse_x = mouse_x;
var oMouse_y = mouse_y;
var alpha = draw_get_alpha();
var maxLines = max(1, (bh-capH) div textHeight);
var resetDelay    = (okbs==""&&!isMove&&!isRemove&&!isEnter&&!isUndo);
/******* Define Locals ********/
if(is_string(nf)){
    var retval                = ds_map_create();
    var _txtBoxDelay = -1;
    var _selectCursorPos    = -1;
    var _dragCursor            = false;
    var _doubleClickTimer    = -1;
    var _doubleClickLock    = -1;
    var _doubleClickPos        = ds_list_create();
    var _undoTimer            = -4;
    var _undoPos            = 0;
    ds_map_add_list(retval, "doubleClickPos", _doubleClickPos);
    var _textBlinkCounter    = 0;
    var textRowList            = parse_textToRows(nf, tbw, tokens);
    ds_map_add_list(retval, "textRowList", textRowList);
    var cursorRow            = -1;
    var cursorCol            = -1;
    var selectRow            = -1;
    var selectCol            = -1;
    var mySurf                = -1;
}else{
    var retval                = nf;
    var textRowList            = nf[?"textRowList"];
    var _textBlinkCounter    = nf[?"blink"];
    var cursorRow            = nf[?"cursorRow"];
    var cursorCol            = nf[?"cursorCol"];
    var selectRow            = nf[?"selectRow"];
    var selectCol            = nf[?"selectCol"];
    var _txtBoxDelay        = nf[?"delay"];
    var _dragCursor            = nf[?"drag"];
    var _doubleClickTimer    = nf[?"doubleClick"];
    var _doubleClickLock    = nf[?"doubleClickLock"];
    var _doubleClickPos        = nf[?"doubleClickPos"];
    var _undoTimer            = nf[?"undoTimer"];
    var _undoPos            = nf[?"undoPos"];
    var mySurf                = nf[?"surface"];
}

if(autoAddCursor && cursorRow==-1) cursorRow=0;
if(useUndo){
    // Set up the textUndoList
    // BE SURE TO DELETE IT
    if(!variable_instance_exists(id,"_textUndoList")){
        _textUndoList = ds_list_create();
        var state = ds_map_create();
        ds_map_add_list(state, "textRowList", textRowList);
        var jscopy = json_encode(state);
        ds_map_replace_list(state, "textRowList", ds_list_create());
        ds_map_destroy(state);
        ds_list_add(_textUndoList,jscopy);
    }
}

/***************************/
_textBlinkCounter+=1;
if(_txtBoxDelay) _txtBoxDelay-=1;
if(isAny||isHeld){
    _textBlinkCounter=0;
}
if(_doubleClickTimer){
    _doubleClickTimer-=1;
}
if(_doubleClickLock){
    _doubleClickLock -= 1;
}
if(_undoTimer){
    _undoTimer -= 1;
}
if(useUndo){
    if(_undoTimer<=0 && _undoTimer>-4){
        // Time has passed since an edit; save Undo state
        _undoTimer = -4;
        var sz = ds_list_size(_textUndoList)-1;
        while(_undoPos<sz){
            ds_list_delete(_textUndoList, sz);
            sz-=1;
        }
        var state = ds_map_create();
        ds_map_add_list(state, "textRowList", textRowList);
        ds_map_add(state, "cursorCol", cursorCol);
        ds_map_add(state, "cursorRow", cursorRow);
        ds_map_add(state, "selectCol", selectCol);
        ds_map_add(state, "selectRow", selectRow);
        var jscopy = json_encode(state);
        ds_map_replace_list(state, "textRowList", ds_list_create());
        ds_map_destroy(state);
        ds_list_add(_textUndoList,jscopy);
        _undoPos+=1;
    }
}

/////////////////////////
// Draw the box
draw_set_color(c_fill);
draw_roundrect_ext(bx,by,bx+bw,by+bh, 2*pad, 2*pad, 0);
draw_set_color(c_border);
for(var z=0;z<pad;z+=1){
    draw_roundrect_ext(bx+z,by+z,bx+bw-z,by+bh-z, 2*pad, 2*pad, 1);
}
draw_set_color(c_text);
var omy = by;
if(bc!=""){ // Draw a caption
    draw_text(bx+indent*pad, by+3*pad, bc);
    capH = 3*pad+string_height(bc)+pad;
    by+=capH;
    var omy = by;
}

/*    Draw the text    */
bx+=indent*pad;
var sz = ds_list_size(textRowList);
for(var i=0; i<min(sz, maxLines); i++){
    var txtEntry = textRowList[|i];
    var txt = txtEntry[?"text"];
    /* Draw each line of text to increase lag */
    if(!useSurface){
        draw_text(bx, by, txt);
        by+=textHeight;
    }
   
}
// Use a surface instead of drawing each line
if(useSurface){
    if(!surface_exists(mySurf)){
        var sy = 0;
        mySurf = surface_create(tbw, bh-capH);
        surface_set_target(mySurf);
        draw_clear_alpha(c_white, 0);
        for(var i=0; i<min(sz, maxLines); i++){
            var txtEntry = textRowList[|i];
            var txt = txtEntry[?"text"];
            draw_text(0, sy, txt);
            sy+=textHeight;
        }
        surface_reset_target();
    }
    draw_surface(mySurf, bx, by);
}

var lastCol = string_length(txt)-1;
var lastRow = sz-1;
/* Locate Mouse */
var mX = bx;
var mY = omy;
var mouseRow=-1;
var mouseCol=-1;
var currentRow = 0;
var currentCol = 0;

var mouseInBox = point_in_rectangle(oMouse_x, oMouse_y, bx, omy, bx+tbw, by+bh-capH);

/*
*    Fact-finding loop.
*    Iterate through rows and columns,
*    detect any relevant things
*/
if(showPreview || isHeld){
    var early = 0;
    while(currentRow<min(sz, maxLines) && mouseInBox && !early){
        var row = textRowList[|currentRow];
        var lh = row[?"height"];
        var rowData = row[?"data"];
   
        // Compute special coords
        if(mouseRow==-1 && oMouse_y>=mY && oMouse_y<=mY+lh){
            mouseRow = currentRow;
        }
   
        /*    Rows are shifted by character width / 2
        *    That allows character insertion to feel natural
        */
        var rsz = ds_list_size(rowData);
        mX = bx;
        currentCol = 0;
        while(currentCol<rsz){
            var cw = rowData[|currentCol];
       
            // Compute special coords
            if(mouseRow==currentRow && oMouse_x>=mX+cw/2 && oMouse_x<=mX+cw/2+cw ){
                mouseCol = currentCol;
                early=1;
            }
            currentCol++;
            mX+=cw;
        }
        if(mouseCol==-1 && mouseRow==currentRow){
            if(oMouse_x<mX){
                mouseCol = -1;
            }else{
                mouseCol = rsz-1;
            }
        }
   
        currentRow++;
        mY+=lh;
    }
}
if(!autoAddCursor&&mouseRow==-1&&mouseInBox){
    // If cursor not added automatically,
    // entire box is "activate area"
    mouseRow = 0;
}

/*    Show mouse insertion    */
if(mouseRow>=0 && (showPreview || isHeld)){
    var row = textRowList[|mouseRow];
    var rowData = row[?"data"];
    var w=0;
    var lineX = bx;
    for(var i=0;i<=mouseCol; i++){
        lineX+=rowData[|i];
    }
   
    draw_set_alpha(.4);
    draw_line(lineX, omy+(mouseRow*textHeight), lineX, omy+( (mouseRow+1)*textHeight ));
    draw_set_alpha(alpha);
   
    if(isPressed){
        selectRow = -1;
        selectCol = -1;
       
        if(_doubleClickTimer && !_doubleClickLock){
                var p1 = find_token(textRowList, cursorCol, cursorRow, tokens, -1);
                var p2 = find_token(textRowList, cursorCol, cursorRow, tokens, 1);
                _doubleClickPos[|0] = p1[0];
                _doubleClickPos[|1] = p1[1];
                _doubleClickPos[|2] = p2[0]-1;
                _doubleClickPos[|3] = p2[1];
               
                _doubleClickLock = doubleClick;
               
                _doubleClickTimer = -1;
           
        }else{
            _doubleClickTimer = doubleClick;
        }
    }

    if(isHeld){
        if(_dragCursor||isShift){
            if(cursorRow != mouseRow || cursorCol != mouseCol){
                if(selectRow==-1){    // Starting a new selection
                    selectRow = cursorRow;
                    selectCol = cursorCol;
                }
                // Update cursor position to current mouse coords
                cursorRow = mouseRow;
                cursorCol = mouseCol;
            }
        }else{
            cursorRow = mouseRow;
            cursorCol = mouseCol;
        }
    }
}
if(_doubleClickLock && !isHeld){
    // Double-click occured, mouse was released
    selectCol = _doubleClickPos[|0];
    selectRow = _doubleClickPos[|1];
    cursorCol = _doubleClickPos[|2];
    cursorRow = _doubleClickPos[|3];
}

/*    Show cursor insertion    */
if(cursorRow>=0){
   
    var a = round(.5*cos(_textBlinkCounter*(room_speed/blinkSpeed))+.5);
   
    var rowNum = cursorRow;
    var row = textRowList[|rowNum];
    var rowData = row[?"data"];
    var w=0;
    var lineX = bx;
    for(var i=0;i<=cursorCol; i++){
        lineX+=rowData[|i];
    }
   
    var y1 = omy+(rowNum*textHeight)+.15*textHeight;
    var y2 = omy+( (rowNum+1)*textHeight )-+.15*textHeight;
   
    draw_set_alpha(a);
    draw_line(lineX, y1, lineX, y2 );
    draw_set_alpha(alpha);
   
   
}

/*    Show selection    */
if(selectRow>=0&&cursorRow>=0){
    // Order points
    var low = -1;
    if(selectRow<cursorRow){
        var low = 0;
    }else if(selectRow>cursorRow){
        var low = 1;
    }else{
        if(selectCol<cursorCol){
            var low = 0;
        }else if(selectCol>cursorCol){
            var low = 1;
        }
    }
   
    if(low==0){
        var firstRow = selectRow;
        var firstCol = selectCol;
        var secondRow = cursorRow;
        var secondCol = cursorCol;
    }else if(low==1){
        var firstRow = cursorRow;
        var firstCol = cursorCol;
        var secondRow = selectRow;
        var secondCol = selectCol;
    }
   
    if(low!=-1){
        // Inverse
        gpu_set_blendmode_ext(bm_inv_dest_color, 1-bm_dest_color);

        var rowNum = firstRow;
        var row = textRowList[|rowNum];
        var rowData = row[?"data"];
        var w=0;
        var lineX1 = bx;
        for(var i=0;i<=firstCol; i++){
            lineX1+=rowData[|i];
        }
        var y1 = omy+(rowNum*textHeight);
        var y2 = omy+( (rowNum+1)*textHeight )-1;
   
        secondRow = secondRow;
        var rowNum = secondRow;
        var row = textRowList[|rowNum];
        var rowData = row[?"data"];
        var w=0;
        var lineX2 = bx;
        for(var i=0;i<=secondCol; i++){
            lineX2+=rowData[|i];
        }
        var y3 = omy+(rowNum*textHeight)+.15*textHeight;
        var y4 = omy+( (rowNum+1)*textHeight )-.15*textHeight;
   
        // Draw rows, if needed
        for(var z=0; z<secondRow-firstRow; z++){
            draw_rectangle(lineX1, y1, bx+tbw, y2, 0);
            lineX1 = bx;
            y1+=textHeight;
            y2+=textHeight;      
        }
        draw_rectangle(lineX1, y1, lineX2, y2, 0);
        gpu_set_blendmode(bm_normal);
    }
}

/* Deal with keyboard */
if(isAny && floor(_txtBoxDelay)<=0 &&cursorRow>=0){
    var newCol = cursorCol;
    var newRow = cursorRow;
    var textRows = ds_list_size(textRowList);
    /* Allow keyboard_string's built-in delay instead of script timing...
    if(okbs!="" || isMove || isRemove || isEnter){
    */
    if(isMove || isRemove || isEnter || isUndo){
        if(floor(_txtBoxDelay)==-4){
            _txtBoxDelay = firstDelayFactor*repeatDelay;
        }else{
            _txtBoxDelay = repeatDelay;
        }
    }  
    #region leftKey
    /*
    *    Left, Shift-Left, Ctrl-Left, Ctrl-Shift-Left
    */
    if(isLeft){
        var newCol = cursorCol;
        var newRow = cursorRow;
        if(isCtrl){
            var ret = find_token(textRowList, newCol, newRow, tokens, -1);
            newCol = ret[0]; newRow = ret[1];
        }else{
            // Just move left
            var found = -1;
            while(found==-1){
                if(newCol==-1 && newRow>0){
                    newRow-=1;
                    var row = textRowList[|newRow];
                    var dataList = row[?"data"];
                    var dsz = ds_list_size(dataList);
                    newCol = dsz-1;
                    found=1;
                }else{
                    if(newCol>-1){
                        newCol-=1;
                    }
                    found=1;
                }
            }
        }
    }
    #endregion
    #region rightKey
    /*
    *    Right, Shift-Right, Ctrl-Right, Ctrl-Shift-Right
    */
    if(isRight){
        var newCol = cursorCol;
        var newRow = cursorRow;
       
        var row = textRowList[|newRow];
        var dataList = row[?"data"];
        var dsz = ds_list_size(dataList);
        if(isCtrl){
            var ret = find_token(textRowList, newCol, newRow, tokens, 1);
            newCol = ret[0]; newRow = ret[1];
        }else{
            // Just move Right
            var found = -1;
            while(found==-1){
                if(newCol==dsz-1 && newRow<textRows-1){
                    newRow+=1;
                    newCol = -1;
                    var row = textRowList[|newRow];
                    var dataList = row[?"data"];
                    var dsz = ds_list_size(dataList);
                    found=1;
                }else{
                    if(newCol<dsz-1){
                        newCol+=1;
                    }
                    found=1;
                }
            }
        }
    }
    #endregion
    #region HomeKey
    /*
    *    Home, Shift-Home, Ctrl-Home, Ctrl-Shift-Home
    */
    if(isHome){
        if(isCtrl){
            // To the start of all text
            newRow = 0;
            var newCol = -1;
        }else{
            // To the start of the row
            newCol = -1;
        }
    }
    #endregion
    #region EndKey
    /*
    *    End, Shift-End, Ctrl-End, Ctrl-Shift-End
    */
    if(isEnd){
        var newCol = cursorCol;
        var newRow = cursorRow;
        var row = textRowList[|newRow];
        var dataList = row[?"data"];
        var dsz = ds_list_size(dataList);
        if(isCtrl){
            // To the end of all text
            newRow = textRows-1;
            var lastRow = textRowList[|newRow];
            var lrd = lastRow[?"data"];
            var lrs = ds_list_size(lrd);
            var newCol = lrs-1;
        }else{
            // To the end of the row
            newCol = dsz-1;
        }
    }
    #endregion
    #region DownKey
    /*
    *    Down, Shift-Down
    */
    if(isDown){
        var newCol = cursorCol;
        var newRow = cursorRow;
        if(newRow<textRows-1){
            // Down a line
            newRow += 1;
            var row = textRowList[|newRow];
            var dataList = row[?"data"];
            var dsz = ds_list_size(dataList);
            if(newCol>dsz-1){
                newCol = dsz-1;
            }
        }else{
            // To the end of the row
            var row = textRowList[|newRow];
            var dataList = row[?"data"];
            var dsz = ds_list_size(dataList);
            newCol = dsz-1;
        }
    }
    #endregion
    #region UpKey
    /*
    *    Up, Shift-Up
    */
    if(isUp){
        var newCol = cursorCol;
        var newRow = cursorRow;
        if(newRow>0){
            // Down a line
            newRow -= 1;
            var row = textRowList[|newRow];
            var dataList = row[?"data"];
            var dsz = ds_list_size(dataList);
            if(newCol>dsz-1){
                newCol = dsz-1;
            }
        }else{
            // To the start of the row
            newCol = -1;
        }
    }
    #endregion
    #region AKey
    if(isA && isCtrl){
        selectCol = -1;
        selectRow = 0;
        newRow = textRows-1;
        var row = textRowList[|newRow];
        newCol = ds_list_size(row[?"data"])-1;
    }
    #endregion
    #region CXKey
    if( (isC||isX) && isCtrl){
        if(selectRow!=-1){
            // Copy to clipbrd
            var lineCol = firstCol;
            var lineRow = firstRow;
            var clipTxt = "";
            for(var z=0; z<secondRow-firstRow; z++){
                var row = textRowList[|lineRow];
                var txt = row[?"text"];
                var len = string_length(txt);
                clipTxt += string_copy(txt, lineCol+2, len-lineCol);
                if(isX){
                    //row[?"text"] = string_delete(txt, lineCol+2, len-lineCol);
                }
                lineCol = -1;
                lineRow+=1;
            }
            var row = textRowList[|secondRow];
            var txt = row[?"text"];
            clipTxt += string_copy(txt, lineCol+2, secondCol-lineCol);
            if(isX){
                //row[?"text"] = string_delete(txt, lineCol+2, secondCol-lineCol);
                //newRow = firstRow; newCol = firstCol;
            }
            clipboard_set_text(clipTxt);
        }  
    }
    #endregion
   
    if(isMove){
        if(isShift && selectRow==-1){
            selectCol = cursorCol;
            selectRow = cursorRow;
        }
    }
    #region textEntry
    if(okbs!="" || isRemove || isEnter || (isCtrl&&isV) || (isUndo)){
        // Text was altered
        if(selectRow!=-1 && !isUndo){
            // Remove selected text first
            var lineCol = firstCol;
            var lineRow = firstRow;
            for(var z=0; z<secondRow-firstRow; z++){
                var row = textRowList[|lineRow];
                var txt = row[?"text"];
                var len = string_length(txt);
                row[?"text"] = string_delete(txt, lineCol+2, len-lineCol);
                lineCol = -1;
                lineRow+=1;
            }
            var row = textRowList[|secondRow];
            var txt = row[?"text"];
            row[?"text"] = string_delete(txt, lineCol+2, secondCol-lineCol);
            newRow = firstRow; newCol = firstCol;
        }else{
            if(isDel){
                var row = textRowList[|newRow];
                var dsz = ds_list_size(row[?"data"]);
                while(newCol>=dsz-1 && newRow<textRows-1){
                    // If last char in row, find next character to delete
                    newCol = -1;
                    newRow+=1;
                }
                var row = textRowList[|newRow];
                var txt = row[?"text"];
                row[?"text"] = string_delete(txt, newCol+2, 1);
            }else if(isBack){
                newCol-=1;
                while(newCol<-1 && newRow>0){
                    // If first char in row, find previous character to delete
                    newRow -= 1;
                    var row = textRowList[|newRow];
                    var dsz = ds_list_size(row[?"data"]);
                    newCol = dsz-1;
                }
                var row = textRowList[|newRow];
                var txt = row[?"text"];
                row[?"text"] = string_delete(txt, newCol+2, 1);
            }
        }
        if(okbs!=""){
            // Typing
            var row = textRowList[|newRow];
            var txt = row[?"text"];
            row[?"text"] = string_insert(okbs, txt, newCol+2);
            newCol+=string_length(okbs);
        }
        if(isEnter){
            var row = textRowList[|newRow];
            var txt = row[?"text"];
            row[?"text"] = string_insert(nl, txt, newCol+2);
            newRow+=1; newCol = -1;
        }
        if(isCtrl&&isV){
            var row = textRowList[|newRow];
            var txt = row[?"text"];
            var clipTxt = clipboard_get_text();
            row[?"text"] = string_insert(clipTxt, txt, newCol+2);
            newCol+=string_length(clipTxt);
        }
        if(isCtrl&&isZ){
            if(isShift){ //Redo, plz
                var sz = ds_list_size(_textUndoList);
                if(_undoPos<sz-1){
                    _undoPos +=1;
                }
            }else{
                if(_undoPos>0){
                    _undoPos-=1;
                }
            }
            // Now load that list
            var json = _textUndoList[|_undoPos];
            var txtMap = json_decode(json);
            var newList = txtMap[?"textRowList"];
            newCol = txtMap[?"cursorCol"];
            newRow = txtMap[?"cursorRow"];
            selectCol = txtMap[?"selectCol"];
            selectRow = txtMap[?"selectRow"];
            ds_map_replace_list(txtMap, "textRowList", ds_list_create());
            ds_map_destroy(txtMap);
            ds_list_destroy(textRowList);
            textRowList = newList;
        }
        // Reset the structure
        var newString = reassemble_textRows(textRowList);
        ds_list_destroy(textRowList);
        textRowList            = parse_textToRows(newString, tbw, tokens);
        textRows            = ds_list_size(textRowList);
        ds_map_replace_list(retval, "textRowList", textRowList);
        // Correct positions
        while(newRow>=textRows && newRow>0){
            newRow-=1;
            var row = textRowList[|newRow];
            var dsz = ds_list_size(row[?"data"]);
            newCol  = dsz-1;
        }
        var row = textRowList[|newRow];
        var dsz = ds_list_size(row[?"data"]);
        while(newCol>=dsz){
            newCol = newCol-dsz; // Shift by the amount that we went over...
            if(newRow<textRows-1){
                newRow+=1;
            }else{
                newCol = -1;
            }
        }
        if(surface_exists(mySurf)){
            surface_free(mySurf);
            mySurf = -1;
        }
        _undoTimer = undoTime;
        if(isUndo) _undoTimer = -4;
    }
    #endregion
   
    if( okbs!="" or (isMove&&!isShift) or isRemove or isEnter){
        selectRow = -1;
        selectCol = -1;
    }

    cursorCol = newCol;
    cursorRow = newRow;
}

if(resetDelay){
    _txtBoxDelay = -4;
}
if(isHeld){
    ds_map_replace(retval, "drag", true);
}else{
    ds_map_replace(retval, "drag", false);
}
ds_map_replace(retval, "surface", mySurf);
ds_map_replace(retval, "doubleClick", _doubleClickTimer);
ds_map_replace(retval, "undoTimer", _undoTimer);
ds_map_replace(retval, "undoPos", _undoPos);
ds_map_replace(retval, "doubleClickLock", _doubleClickLock);
ds_map_replace(retval, "blink", _textBlinkCounter);
ds_map_replace(retval, "cursorRow", cursorRow);
ds_map_replace(retval, "cursorCol", cursorCol);
ds_map_replace(retval, "selectRow", selectRow);
ds_map_replace(retval, "selectCol", selectCol);
ds_map_replace(retval, "delay", _txtBoxDelay);
ds_map_replace_list(retval, "textRowList", textRowList);
return retval;
Code:
//    @function        parse_textToRows(text, width, tokenArray)
///    @arg    {string}    text    Text to turn into a structure
///    @arg    {real}        width    Width or area to parse text into
///    @arg    {array}        tokens    An array of token strings to locate
/**************************************************************/
/*        Information
*    Developed by Tsa05 on the GameMaker forum for use in SLIDGE
*    Use of the script is subject to the following conditions:
*        1) Leave the information section intact
*        2) Give this script freely
*/

var text    = argument[0];
var w        = argument[1];
var tokens    = argument[2];

var nl = "\n";
var retval = ds_list_create();

var length = string_length(text);
var temp = "";
var sh = string_height("|");

var start = 1;
var row = ds_map_create();
var rowData = ds_list_create();
var tokenList = ds_list_create();
var offset = 0; // Track rowlength for tokens
for(var i=1; i<=length; i++){
    var nextChar = string_char_at(text, i);
    var nlf = ( nextChar==nl );
    var cW = string_width(nextChar);
    var tW = string_width(temp);
 
    if(cW+tW>w || nlf){
        if(nlf){
            temp += nextChar;
            ds_list_add(rowData, cW);
        }
        ds_map_add(row, "text",  temp);
        ds_map_add(row, "height",  sh);
        ds_map_add_list(row, "data", rowData);
        ds_map_add_list(row, "tokens", tokenList);
        var sz = ds_list_size(retval);
        ds_list_add(retval, row);
        ds_list_mark_as_map(retval, sz);
        offset+=string_length(temp);
        temp="";
        row = ds_map_create();
        rowData = ds_list_create();
        tokenList = ds_list_create();
    }
    if(!nlf){
        temp += nextChar;
        ds_list_add(rowData, cW);
        if(is_in(nextChar, tokens)){
            ds_list_add(tokenList, i-offset);
        }
    }
}

ds_map_add(row, "text",  temp);
ds_map_add(row, "height",  sh);
ds_map_add_list(row, "data", rowData);
ds_map_add_list(row, "tokens", tokenList);
var sz = ds_list_size(retval);
ds_list_add(retval, row);
ds_list_mark_as_map(retval, sz);

return retval;
Code:
///    @function        find_token(textRows, column, row, tokenArray, direction)
///    @arg    {real}    textRows    A ds_list of ds_maps with row data
///    @arg    {real}    column        Column to start seeking from
///    @arg    {real}    row            Row to start seeking from
///    @arg    {array}    tokens        An array containing tokens to search
///    @arg    {real}    direction    Which direction to search (positive, negative)
/**************************************************************/
/*        Information
*    Developed by Tsa05 on the GameMaker forum for use in SLIDGE
*    Use of the script is subject to the following conditions:
*        1) Leave the information section intact
*        2) Give this script freely
*/

var textRowList    = argument[0];
var newCol        = argument[1];
var newRow        = argument[2];
var tokens        = argument[3];
var dir            = argument[4];

var textRows = ds_list_size(textRowList);
if(sign(dir)){
    var row = textRowList[|newRow];
    var dataList = row[?"data"];
    var dsz = ds_list_size(dataList);
    var found = -1; var done = 0;
    while(found==-1 &&!done){
        if(newCol==dsz-1 && newRow<textRows-1){
            newRow+=1;
            newCol = -1;
            var row = textRowList[|newRow];
            var dataList = row[?"data"];
            var dsz = ds_list_size(dataList);
        }else{
            var row = textRowList[|newRow];
        }
        var tokenList = row[?"tokens"];
        var sz = ds_list_size(tokenList);
        if(sz){
            if(newCol>=tokenList[|sz-1]){
                newCol = dsz-1;
                if(newRow<textRows-1){  }else{ found = 1; }
            }else{
                var i=0; var done = 0;
                while(i<sz && !done){
                    var tcol = tokenList[|i]-1;
                    if(tcol>newCol){
                        newCol = tcol;
                        done=1;
                    }
                    i+=1;
                }
                if(!done){ newCol=dsz-1; }
                if(newCol==dsz-1&&newRow<textRows-1){  }else{ found = 1; }
            }
        }else{
            // There's no tokens in this row
            newCol = dsz-1;
            if(newRow<textRows-1){  }else{ found = 1; }
        }
    }
}else{
    var found = -1;
    while(found==-1){
        if(newCol==-1 && newRow>0){
            newRow-=1;
            var row = textRowList[|newRow];
            var dataList = row[?"data"];
            var dsz = ds_list_size(dataList);
            newCol = dsz-1;
        }else{
            var row = textRowList[|newRow];
        }
        var tokenList = row[?"tokens"];
        var sz = ds_list_size(tokenList);
        if(sz){
            if(newCol<=tokenList[|0]-1){
                newCol = -1;
                if(newRow>0){  }else{ found = 1; }
            }else{
                var i=sz-1; var done = 0;
                while(i>=0 && !done){
                    var tcol = tokenList[|i]-1;
                    if(tcol<newCol){
                        newCol = tcol;
                        done=1;
                    }
                    i-=1;
                }
                if(!done){ newCol=-1; }
                if(newCol==-1&&newRow>0){  }else{ found = 1; }
            }
        }else{
            // There's no tokens in this row
            newCol = -1;
            if(newRow>0){  }else{ found = 1; }
        }
    }
}

return [newCol, newRow];
Code:
///    @function    reassemble_textRows(textRowList)
//    @returns    String of text concatenated from a textRowList
///    @arg    {real}    textRowList
/**************************************************************/
/*        Information
*    Developed by Tsa05 on the GameMaker forum for use in SLIDGE
*    Use of the script is subject to the following conditions:
*        1) Leave the information section intact
*        2) Give this script freely
*/
var textRowList = argument[0];

var sz = ds_list_size(textRowList);
var ret = "";
for(var i=0; i<sz; i++){
    var row = textRowList[|i];
    var txt = row[?"text"];
    ret+=txt;
}
return ret;
 
Last edited:
Top