I do this for fun, not money. Got vertical scrolling. It's not quite perfect yet, needs a few tweaks. You can move the caret with left/right arrow keys and delete, enter, backspace all work. I wrote a different function for detecting the location of the caret when clicked, I think it's more efficient than my original method. If you had a lot of text, there's improvements that could be made for efficiency with the tradeoff of a little extra memory usage, but for the moment the added complexity isn't worth the benefit. Maybe someday I'll tweak it.
// Project: scrollbox
// Created: 2025-05-06
// show all errors
SetErrorMode(2)
// set window properties
SetWindowTitle( "scrollbox" )
SetWindowSize( 1024, 768, 0 )
SetWindowAllowResize( 1 ) // allow the user to resize the window
// set display properties
SetVirtualResolution( 1024, 768 ) // doesn't have to match the window
SetOrientationAllowed( 1, 1, 1, 1 ) // allow both portrait and landscape on mobile devices
SetSyncRate(0, 0 ) // 30fps instead of 60 to save battery
SetScissor( 0,0,0,0 ) // use the maximum available screen space, no black borders
UseNewDefaultFonts( 1 ) // since version 2.0.22 we can use nicer default fonts
#CONSTANT _N = chr(10)+chr(13)
Type ResizableBox
TL as integer
TM as integer
TR as integer
L as integer
M as integer
R as integer
BL as integer
BM as integer
BR as integer
resize as integer
ox as integer
oy as integer
t as integer
`caretX as integer
`caretY as integer
caretFlash as integer
showCaret as integer
caretIndex as integer
vScrollT as integer
vScrollM as integer
vScrollB as integer
vthumb as integer
hthumb as integer
vThumbPos as float
hThumbPos as float
dragThumb as integer
tov as integer
EndType
mybox as ResizableBox
mybox = createResizableBox(loadImage("box.png"), 11, 11)
setResizableBoxScrollbars(mybox, loadImage("vtrack.png"), 6, 6)
setResizableBoxThumb(mybox, loadImage("thumb.png"))
setResizableBoxSize(mybox, 200, 300)
setResizableBoxPosition(mybox, 200,200)
`setResizableBoxSize(mybox, 300, 200)
c1 = random()*4294967295 : c2 = random()*4294967295 : c3 = random()*4294967295 : c4 = random()*4294967295
drawBox(0,0,1024,768,c1, c2, c3, c4, 1)
q = createSprite(getImage(0, 0, 1024,768))
setSpriteDepth(q, 40)
c = 0xFF0000
do
if doinput = 1
// enter
if getRawKeyPressed(13)
a$ = mid(getTextString(mybox.t), 1, mybox.caretIndex)
b$ = mid(getTextString(mybox.t), mybox.caretIndex+1, -1)
setTextString(mybox.t, a$ + _N + b$)
positionCaret(mybox, mybox.caretIndex + 2)
endif
if getCharBufferLength() > 0
a$ = mid(getTextString(mybox.t), 1, mybox.caretIndex)
b$ = mid(getTextString(mybox.t), mybox.caretIndex+1, -1)
m$ = getCharBuffer()
setTextString(mybox.t, a$ + m$ + b$)
positionCaret(mybox, mybox.caretIndex+len(m$))
endif
// backspace
if getRawKeyPressed(8) = 1
a$ = mid(getTextString(mybox.t), 1, mybox.caretIndex-1)
b$ = mid(getTextString(mybox.t), mybox.caretIndex+1, -1)
setTextString(mybox.t, a$ + b$)
positionCaret(mybox, mybox.caretIndex-1)
endif
// delete
if getRawKeyPressed(46) = 1
a$ = mid(getTextString(mybox.t), 1, mybox.caretIndex)
b$ = mid(getTextString(mybox.t), mybox.caretIndex+2, -1)
setTextString(mybox.t, a$ + b$)
endif
// left/right arrows
if getRawKeyPressed(37) = 1 then positionCaret(mybox, mybox.caretIndex-1)
if getRawKeyPressed(39) = 1 then positionCaret(mybox, mybox.caretIndex+1)
endif
if getRawKeyPressed(13) = 1 and doinput = 0
doinput = 1
getCharBuffer()
s$ = getTextString(mybox.t)
`replaceString(s$, _N, "-", -1)
setTextString(mybox.t, s$)
endif
handleResizableBox(mybox)
Sync()
loop
function positionCaret(r ref as ResizableBox, n)
r.caretIndex = n
r.caretFlash = getMilliseconds()
r.showCaret = 1
endfunction
function setCaret2(r ref as ResizableBox)
mx = getRawMouseX() - getTextX(r.t)
my = getRawMouseY() - getTextY(r.t)
r.caretIndex = 0
s$ = getTextString(r.t)
for i = 1 to len(s$)
cx = GetTextCharX(r.t, i)
cy = GetTextCharY(r.t, i)
if my >= cy and my < cy+22
if mx >= cx and mx < GetTextCharX(r.t, i+1)
r.caretIndex = i
exit
else
r.caretIndex = i
endif
endif
next i
if i = len(s$) then r.caretIndex = i
//if r.caretIndex > 0 then print(mid(s$, r.caretIndex, 1))
r.caretFlash = getMilliseconds()
r.showCaret = 1
endfunction
/*
function setCaret(r ref as ResizableBox)
dummy = createText("")
setTextVisible(dummy, 0)
setTextSize(dummy, getTextSize(r.t))
textOffsetX = getSpriteX(r.TL) + getSpriteWidth(r.TL)
textOffsetY = getSpriteY(r.TL) + getSpriteHeight(r.TL)
mx = getRawMouseX() - textOffsetX
my = getRawMouseY() - textOffsetY
totalRows = floor(getTextTotalHeight(r.t) / getTextSize(r.t))
row = floor(my / getTextSize(r.t))
if row > totalRows-1 then row = totalRows-1
if row < 0 then row = 0
s$ = getTextString(r.t)
// Determines which row in the text based on _N (new line chars)
// to place the caret
if row >= totalRows
A = len(s$)
B = A
else
A = 0
B = 0
rows = 0
for i = 1 to len(s$)-1
if mid(s$, i, 2) = _N then inc rows
if rows = row and A = 0
A = i
endif
if A > 0 and B = 0 and rows = row+1
B = i
i = len(s$)
endif
next i
if A = 0 then A = 1
if B = 0 then B = len(s$)
endif
l$ = mid(s$, A, B-A+1)
caretX = 0
t$ = "_"
for i = 1 to len(l$)
setTextString(dummy, mid(l$, 1, i))
x = getTextTotalWidth(dummy)
if x >= mx or i = len(l$)
t$ = mid(l$, i, 1)
caretX = x
exit
endif
next i
setTextString(dummy, l$)
if row = 0 and mx <= 0 then caretX = 0
if row = totalRows-1 and mx > getTextTotalWidth(dummy) then caretX = getTextTotalWidth(dummy)
r.caretX = caretX
r.caretY = row*getTextSize(r.t)
deleteText(dummy)
// always show caret when repositioning it
r.caretFlash = getMilliseconds()
r.showCaret = 1
endfunction
*/
function setResizableBoxThumb(r ref as ResizableBox, img)
r.vthumb = createSprite(img)
setSpriteDepth(r.vthumb, 7)
r.hthumb = cloneSprite(r.vthumb)
endfunction
function setResizableBoxScrollbars(r ref as ResizableBox, img, chunkWidth, chunkHeight)
x = getSpriteX(r.R)
y = getSpriteY(r.R)
r.vScrollT = createSprite(img)
setSpriteDepth(r.vScrollT, 8)
setSpriteAnimation(r.vScrollT, chunkWidth, chunkHeight, 3)
setSpritePosition(r.vScrollT, x, y)
r.vScrollM = cloneSprite(r.vScrollT) : setSpriteFrame(r.vScrollM, 2) : setSpritePosition(r.vScrollM, x, y+chunkHeight)
r.vScrollB = cloneSprite(r.vScrollT) : setSpriteFrame(r.vScrollB, 3) : setSpritePosition(r.vScrollB, x, y+chunkHeight*2)
endfunction
function handleResizableBox(r ref as ResizableBox)
delta = 5
x = getSpriteX(r.BR) + (getSpriteWidth(r.BR)/2)
y = getSpriteY(r.BR) + (getSpriteHeight(r.BR)/2)
mx = getRawMouseX()
my = getRawMouseY()
if inside(mx, my, x-delta, y-delta, x+delta, y+delta )
if getRawMouseLeftPressed()
r.resize = 1
r.ox = x - mx + (getSpriteWidth(r.BR)/2)
r.oy = y - my + (getSpriteHeight(r.BR)/2)
endif
else
if GetTextHitTest(r.t, mx, my)
if getRawMouseleftPressed()
setCaret2(r)
endif
endif
endif
if r.resize = 1
if getRawMouseLeftState() = 1
width = mx - getSpriteX(r.TL) + r.ox
if width < getSpriteWidth(r.TL)*3 then width = getSpriteWidth(r.TL)*3
height = my - getSpriteY(r.TL) + r.oy
if height < getSpriteHeight(r.TL)*3 then height = getSpriteHeight(r.TL)*3
setResizableBoxSize(r, width, height)
else
r.resize = 0
endif
endif
if inside(mx, my, getSpriteX(r.vthumb), getSpriteY(r.vthumb), getSpriteX(r.vthumb)+getSpriteWidth(r.vthumb), getSpriteY(r.vthumb)+getSpriteHeight(r.vthumb) )
if getRawMouseLeftState() = 1 and r.dragThumb = 0
r.dragThumb = 1
r.oy = my - getSpriteY(r.vthumb)
endif
endif
if r.dragThumb = 1
r.vThumbPos = ((getRawMouseY()+r.oy-getSpriteHeight(r.vthumb)/2) - getSpriteY(r.vScrollM)) / getSpriteHeight(r.vScrollM)
if r.vThumbPos < 0 then r.vThumbPos = 0
if r.vThumbPos > 1 then r.vThumbPos = 1
updateVThumbPosition(r)
if getRawMouseLeftState() = 0
r.dragThumb = 0
endif
endif
// draw caret
ms = getMilliseconds()
if r.caretFlash + 600 <= ms
r.caretFlash = ms
r.showCaret = 1 - r.showCaret
endif
if r.showCaret = 1
if r.caretIndex = 0
cx = 0
cy = 0
else
cx = GetTextCharX(r.t, r.caretIndex)
cy = GetTextCharY(r.t, r.caretIndex)
endif
c = 0xFFFFFF
textOffsetX = getTextX(r.t)
textOffsetY = getTextY(r.t)
drawBox(textOffsetX+cx, textOffsetY+cy, textOffsetX+cx+2, textOffsetY+cy+22, c,c,c,c,1)
endif
endfunction
function inside(px, py, x1, y1, x2, y2)
if px > x1 and px < x2 and py > y1 and py < y2 then exitfunction 1
endfunction 0
function updateVThumbPosition(r ref as ResizableBox)
x = getSpriteX(r.R) - getSpriteWidth(r.vthumb)/2
y = getSpriteY(r.vScrollM)
h = getSpriteHeight(r.vScrollM)
ty = h*r.vThumbPos - getSpriteHeight(r.vthumb)/2
setSpritePosition(r.vthumb, x, y+ty)
th = getTextTotalHeight(r.t)
mh = getSpriteHeight(r.M)
extraVerticalView = th - mh
if extraVerticalView < 0 then extraVerticalView = 0
r.tov = extraVerticalView * r.vThumbPos
setTextPosition(r.t, getSpriteX(r.TL)+getSpriteWidth(r.TL), getSpriteY(r.TL)+getSpriteHeight(r.TL)-r.tov)
endfunction
function createResizableBox(img, chunkWidth, chunkHeight )
n as ResizableBox
n.TL = createSprite(img) : setSpriteAnimation(n.TL, chunkWidth, chunkHeight, 9)
n.TM = cloneSprite(n.TL) : setSpriteFrame(n.TM, 2) : setSpritePosition(n.TM, chunkWidth, 0)
n.TR = cloneSprite(n.TL) : setSpriteFrame(n.TR, 3) : setSpritePosition(n.TR, chunkWidth*2, 0)
n.L = cloneSprite(n.TL) : setSpriteFrame(n.L, 4) : setSpritePosition(n.L, 0, chunkHeight)
n.M = cloneSprite(n.TL) : setSpriteFrame(n.M, 5) : setSpritePosition(n.M, chunkWidth, chunkHeight)
n.R = cloneSprite(n.TL) : setSpriteFrame(n.R, 6) : setSpritePosition(n.R, chunkWidth*2, chunkHeight)
n.BL = cloneSprite(n.TL) : setSpriteFrame(n.BL, 7) : setSpritePosition(n.BL, 0, chunkHeight*2)
n.BM = cloneSprite(n.TL) : setSpriteFrame(n.BM, 8) : setSpritePosition(n.BM, chunkWidth, chunkHeight*2)
n.BR = cloneSprite(n.TL) : setSpriteFrame(n.BR, 9) : setSpritePosition(n.BR, chunkWidth*2, chunkHeight*2)
t$ = "Sing, O goddess, the anger of Achilles son of Peleus, "+_N+"that brought countless ills upon the Achaeans. "
t$ = t$ +_N+_N+"Many a brave soul did it send hurrying down to Hades, "+_N+"and many a hero did it yield a prey to dogs and vultures, "
t$ = t$ +_N+"for so were the counsels of Jove fulfilled from the day on which the son of Atreus, "+_N+"king of men, and great Achilles, first fell out with one another."
n.t = createText(t$)
setTextSize(n.t, 22)
//setTextColor(n.t, 217, 178, 0, 255)
setTextColor(n.t, 0,0, 0, 255)
setTextPosition(n.t, chunkWidth, chunkHeight)
setTextScissor(n.t, chunkWidth, chunkHeight, chunkWidth*2, chunkHeight*2)
endfunction n
function setResizableBoxSize(r ref as ResizableBox, width, height)
x = getSpriteX(r.TL)
y = getSpriteY(r.TL)
w = getSpriteWidth(r.TL)
h = getSpriteHeight(r.TL)
setSpriteSize(r.TM, width-w*2, h) // upper middle
setSpritePosition(r.TR, x+width-w, y) // upper right corner
setSpritePosition(r.L, x, y+h) // left side
setSpriteSize(r.L, w, height-h*2)
setSpriteSize(r.M, width-w*2, height-h*2) // body
setSpritePosition(r.R, x+width-w, y+h) // right side
setSpriteSize(r.R, w, height-h*2)
setSpritePosition(r.BL, x, y+height-h) // bottom left
setSpriteSize(r.BM, width-w*2, h) // bottom middle
setSpritePosition(r.BM, x+w, y+height-h)
setSpritePosition(r.BR, x+width-w, y+height-h) // bottom right
setTextPosition(r.t, x+w, y+h-r.tov)
`setTextScissor(r.t, x+w, y+h, x+getSpriteWidth(r.M), y+getSpriteHeight(r.M))
setTextScissor(r.t, x+w, y+h, x+getSpriteWidth(r.M), getSpriteY(r.BM))
x = getSpriteX(r.R) - getSpriteWidth(r.vthumb)/2
y = getSpriteY(r.R)
sh = getSpriteHeight(r.vScrollT)
sw = getSpriteWidth(r.vScrollT)
trackHeight = height - h*2
setSpriteSize(r.vScrollM, sw, trackHeight-sh*2)
ty = getSpriteHeight(r.M) * r.vThumbPos
setSpritePosition(r.vthumb, x, y+ty)
x = getSpriteX(r.R) - sw/2
setSpritePosition(r.vScrollT, x, y)
setSpritePosition(r.vScrollM, x, y+sh)
setSpritePosition(r.vScrollB, x, y+trackHeight-sh)
endfunction
function setResizableBoxPosition(r ref as ResizableBox, x, y)
w = getSpriteWidth(r.TL)
h = getSpriteHeight(r.TL)
width = getSpriteWidth(r.M)
height = getSpriteHeight(r.M)
setSpritePosition(r.TL, x, y)
setSpritePosition(r.TM, x+w, y)
setSpritePosition(r.TR, x+width+w, y)
setSpritePosition(r.L, x, y+h)
setSpritePosition(r.M, x+w, y+h)
setSpritePosition(r.R, x+width+w, y+h)
setSpritePosition(r.BL, x, y+height+h)
setSpritePosition(r.BM, x+w, y+height+h)
setSpritePosition(r.BR, x+width+w, y+height+h)
setTextPosition(r.t, x+w, y+h-r.tov)
setTextScissor(r.t, x+w, y+h, x+width, y+height)
x = getSpriteX(r.R) - getSpriteWidth(r.vthumb)/2
y = getSpriteY(r.R)
sh = getSpriteHeight(r.vScrollT)
sw = getSpriteWidth(r.vScrollT)
trackHeight = height - h*2
ty = getSpriteHeight(r.M) * r.vThumbPos
setSpritePosition(r.vthumb, x, y+ty)
x = getSpriteX(r.R) - sw/2
setSpritePosition(r.vScrollT, x, y)
setSpritePosition(r.vScrollM, x, y+sh)
setSpritePosition(r.vScrollB, x, y+sh+getSpriteHeight(r.vScrollM))
endfunction