GMS 2 Multiline Textbox: Improved, still needs optimization ideas

Discussion in 'Advanced Programming Discussion' started by Tsa05, Feb 9, 2018.

  1. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    545
    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: Jun 24, 2018
  2. Pixelated_Pope

    Pixelated_Pope Member

    Joined:
    Jun 20, 2016
    Posts:
    780
  3. Tthecreator

    Tthecreator Your Creator!

    Joined:
    Jun 20, 2016
    Posts:
    725
    Wow I can see you did a lot of work!
    How many days did you spend on this?
     
  4. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    545
    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.
     
    Ghost in the IDE and Tthecreator like this.
  5. Ghost in the IDE

    Ghost in the IDE Member

    Joined:
    Jan 8, 2018
    Posts:
    576
    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.
     
  6. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    545
    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!
     
    Ghost in the IDE likes this.
  7. Ghost in the IDE

    Ghost in the IDE Member

    Joined:
    Jan 8, 2018
    Posts:
    576
    Doing extremely well, man :)
     
  8. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    545
    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...
    That's an interesting point; I probably should be separating the mouse position and cursor position computations to avoid the order-of-operations issue
    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
    
     
    Ghost in the IDE likes this.
  9. Ghost in the IDE

    Ghost in the IDE Member

    Joined:
    Jan 8, 2018
    Posts:
    576
    Aww, doesn't work for me. :(

    Code:
    /// Create
    
    text = "";
    width = 800;
    height = 600;
    caption = "Text box";
    limit = 10000;
    
    Code:
    /// Draw
    
    draw_input(x,y,width,height,caption,text,limit);
    
     
  10. FrostyCat

    FrostyCat Member

    Joined:
    Jun 26, 2016
    Posts:
    4,033
    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: Feb 21, 2018
  11. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    545
    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);
    
     
  12. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    545
    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.
     
    Ghost in the IDE likes this.
  13. Ghost in the IDE

    Ghost in the IDE Member

    Joined:
    Jan 8, 2018
    Posts:
    576
    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.


    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: Feb 21, 2018
  14. Ghost in the IDE

    Ghost in the IDE Member

    Joined:
    Jan 8, 2018
    Posts:
    576
    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 :)
     
  15. FrostyCat

    FrostyCat Member

    Joined:
    Jun 26, 2016
    Posts:
    4,033
    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.
     
    Ghost in the IDE likes this.
  16. Ghost in the IDE

    Ghost in the IDE Member

    Joined:
    Jan 8, 2018
    Posts:
    576
    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: Feb 22, 2018
  17. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    545
    I've updated the OP with a few small things that might make the box behave a little more like expected
    • 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.
    • [​IMG] 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:
    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. ¯\_(ツ)_/¯
     
    Ghost in the IDE likes this.
  18. Ghost in the IDE

    Ghost in the IDE Member

    Joined:
    Jan 8, 2018
    Posts:
    576
    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.
     
  19. Tsa05

    Tsa05 Member

    Joined:
    Jun 21, 2016
    Posts:
    545
    After much soul-searching and several minutes of thinking, I have re-written the whole thing

    Most important new feature:
    [​IMG]

    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: Jun 26, 2018
    Ido-f and Nocturne like this.

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice