HomeForumSourceResearchGuide
Sign in to contribute to source. how it works
Component ui.TextArea by barry
expand copy to clipboardexpand
//NEXT: optimise, e.g. by storing character lengths, fragment lengths, line heights

//BUG: placing the cursor at the start of a subline renders the cursor at the far right of that line :-S

//NOTE: we could potentially separate out the text storage and management logic into a separate API TextModel
// - and consider simplifying getTextRegion() similar to deleteSelection(), again avoiding the from-start scan :-S
// - it's maybe worth totally reworking insertTextTo(), it's extremely complicated and is only called from one place!

//this is the horizontal border between the textual contents of the TextArea, and the outer edge of its background
const int TEXT_BORDER_H = 3

//and the vertical border
const int TEXT_BORDER_V = 1

const int CURSOR_WIDTH = 1

const byte CONTEXT_MENU = 0
const byte CONTEXT_HINT = 1

const int SCR_BUTTON_HEIGHT = 20
const int SCR_BUTTON_WIDTH = 20

const int LINE_NUMBER_MARGIN = 8

uses IOWindow

//each line of text is made of a list of text fragments
data TextFrag {
	const byte T_TEXT = 1 //regular text, can be expanded and contracted
	const byte T_PARTICLE = 2 //a particle, like a space or full-stop, which act as word-wrap points, and aren't expanded or contracted
	const byte T_TAB = 3 //a tab
	byte type
	StringUTF value

	TextFrag next
	TextFrag prev
}

//text on a "logical" line may be split over multiple display lines, e.g. in a word-wrap mode
data TextContent {
	TextFrag frags
	TextFrag fragsEnd

	TextContent next
	TextContent prev
}

data TextLine {
	TextContent text
	TextContent textEnd

	int charLength
	int pixelLength

	int lineCount

	//these references track the in-order list of lines in the text area
	TextLine next
	TextLine prev

	//these references are used to keep a sorted list of line-lengths (by pixel length) for scroll area calculations
	TextLine nextLen
	TextLine prevLen
	}

data CursorPos {
	int character
	int yPosition
	TextLine line
	TextContent subline
	int subCharacter
	}

component provides TextArea(Destructor) requires io.Output out, data.IntUtil iu, ui.Font, os.SystemInfo sysInfo, os.Clipboard clipboard, locale.KeyMapping keyMapping, encoding.StringUTF, ScrollBar, ScrollBar:h, data.StringUtil stringUtil {
	
	Mutex textLock = new Mutex()

	TextLine lines
	TextLine lastLine

	TextLine shortestLine
	TextLine longestLine

	//number of actual lines
	int lineCount
	//number of "virtual" or "view" lines, which may be greater than actual lines rent when word-wrap is enabled
	int lineCountV
	
	//NOTE: these could both be CursorPos instances...?
	int cursorPosition
	TextLine cursorLine
	int cursorPositionY
	TextContent cursorSubline

	int selectStartPos
	TextLine selectStartLine
	int selectStartY
	TextContent selectSubline

	Font textFont
	
	int width = 40
	int height = 20
	int textAreaWidth = 20
	int textAreaHeight = 20
	int cursorHeight = 20

	int lineNumberAreaWidth = 0

	int TAB_SPACES = 4
	int spaceWidth = 2
	
	bool focus
	
	bool shiftDown

	bool mouseDragCapture

	byte keyState

	bool showScrollV = true
	ScrollBar scrollV
	int yScroll
	
	bool showScrollH = true
	ScrollBar scrollH
	int xScroll

	GraphicsObject clickDown

	bool lineNumbers = false
	bool wordWrap = false
	bool autoIndent = false

	byte cursorType

	int SCROLL_TICKS = 20
	
	ContextMenuSpec menu
	MenuItem cutMenu = new MenuItem("Cut", "Ctrl+X")
	MenuItem copyMenu = new MenuItem("Copy", "Ctrl+C")
	MenuItem pasteMenu = new MenuItem("Paste", "Ctrl+V")
	MenuItem selectAllMenu= new MenuItem("Select All", "Ctrl+A")
	
	HotKey hkeys[]
	HotKey hkCut = new HotKey(KeyState.KEY_CTRL, KeyCode.X)
	HotKey hkCopy = new HotKey(KeyState.KEY_CTRL, KeyCode.C)
	HotKey hkPaste = new HotKey(KeyState.KEY_CTRL, KeyCode.V)
	HotKey hkSelectAll = new HotKey(KeyState.KEY_CTRL, KeyCode.A)
	
	Color bgColor = new Color(250, 250, 255, 255)
	Color borderColor = new Color(180, 180, 190, 255)
	Color highlightColor = new Color(160, 160, 200, 255)
	Color textColor = new Color(0, 0, 0, 255)
	Color textColorLN = new Color(0, 0, 90, 255)
	
	TextArea:TextArea()
		{
		textFont = new Font(sysInfo.getSystemFont(false), 15)
		
		menu = new ContextMenuSpec()
		
		menu.items = new MenuItem[](cutMenu,
									copyMenu,
									pasteMenu,
									selectAllMenu
									)
		
		hkeys = new HotKey[](hkCut,
							hkCopy,
							hkPaste,
							hkSelectAll
							)
		
		height = textFont.getFontMetrics().height + (TEXT_BORDER_V*2)
		cursorHeight = textFont.getFontMetrics().height + (TEXT_BORDER_V*2)
		SCROLL_TICKS = cursorHeight
		
		lines = makeLine()
		lastLine = lines
		cursorLine = lines
		selectStartLine = cursorLine

		shortestLine = lines
		longestLine = lines

		scrollV = new ScrollBar()
		scrollV.setPosition(width - SCR_BUTTON_WIDTH, 0)
		
		scrollH = new ScrollBar:h()
		scrollH.setPosition(0, height - SCR_BUTTON_WIDTH)

		spaceWidth = textFont.getTextWidth(" ")

		sinkevent ScrollEvents(scrollV)
		sinkevent ScrollEvents(scrollH)
		}
	
	TextLine makeLine()
		{
		TextLine newLine = new TextLine(new TextContent(new TextFrag(TextFrag.T_TEXT, new StringUTF(""))))
		newLine.textEnd = newLine.text
		newLine.text.fragsEnd = newLine.text.frags

		newLine.lineCount = 1

		return newLine
		}
	
	void insertFragAfter(TextLine line, TextFrag newFrag, TextFrag after)
		{
		if (after.next != null)
			after.next.prev = newFrag
			else
			line.textEnd.fragsEnd = newFrag
		newFrag.next = after.next
		newFrag.prev = after
		after.next = newFrag
		}
	
	void insertTab(TextLine line, int atCharacter)
		{
		TextFrag newFrag = new TextFrag(TextFrag.T_TAB, new StringUTF("\t"))

		if (atCharacter == 0)
			{
			newFrag.next = line.text.frags
			line.text.frags.prev = newFrag
			line.text.frags = newFrag
			}

		TextFrag fw = line.text.frags
		int pos = atCharacter
		while (fw != null)
			{
			if (fw.type == TextFrag.T_TEXT)
				{
				if (fw.value.length() == pos)
					{
					//insert the tab after this text section
					insertFragAfter(line, newFrag, fw)
					break
					}
					else if (pos < fw.value.length())
					{
					//break this text section into two, with the tab inserted in the middle
					TextFrag fragB = new TextFrag(TextFrag.T_TEXT, new StringUTF(fw.value.subString(pos, fw.value.length() - pos)))
					fw.value = new StringUTF(fw.value.subString(0, pos))
					insertFragAfter(line, fragB, fw)
					insertFragAfter(line, newFrag, fw)
					break
					}
					else
					{
					pos -= fw.value.length()
					}
				}
				else
				{
				if (pos == 1)
					{
					//insert the tab after this tab
					insertFragAfter(line, newFrag, fw)
					break
					}
					else
					{
					pos --
					}
				}
			
			fw = fw.next
			}
		}
	
	void appendTab(TextLine line)
		{
		if (line.textEnd.frags === line.textEnd.fragsEnd && line.textEnd.frags.value.length() == 0)
			{
			line.textEnd.frags.type = TextFrag.T_TAB
			line.textEnd.frags.value.append("\t")
			}
			else
			{
			TextFrag newFrag = new TextFrag(TextFrag.T_TAB, new StringUTF("\t"))
			newFrag.prev = line.textEnd.fragsEnd
			line.textEnd.fragsEnd.next = newFrag

			line.textEnd.fragsEnd = newFrag
			}
		}
	
	//given an absolute cursor position on a line, return the relative position on the given subline
	int getSublinePos(TextLine line, TextContent c, int pos)
		{
		TextContent cw = line.text
		while (cw !== c && cw != null)
			{
			pos -= getSublineCharLength(cw)
			cw = cw.next
			}
		
		return pos
		}
	
	//given a relative position on a given subline, return the absolute cursor position on the line
	int getLineCursorPos(TextLine line, TextContent c, int pos)
		{
		TextContent cw = line.text
		int len = 0
		while (cw !== c)
			{
			len += getSublineCharLength(cw)
			cw = cw.next
			}
		
		return len + pos
		}
	
	int getSublineCharLength(TextContent cw)
		{
		int len = 0
		TextFrag fw = cw.frags
		while (fw != null)
			{
			if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
				{
				len += fw.value.length()
				}
				else
				{
				len ++
				}
			
			fw = fw.next
			}
		
		return len
		}
	
	int getLineCharLength(TextLine line)
		{
		int len = 0
		TextContent cw = line.text
		while (cw != null)
			{
			len += getSublineCharLength(cw)
			cw = cw.next
			}
		
		return len
		}
	
	eventsink ScrollEvents(EventData ed)
		{
		if (ed.source === scrollV && ed.type == ScrollBar.[scrollMoved])
			{
			yScroll = scrollV.getScrollPos()
			}
			else if (ed.source === scrollV && ed.type == ScrollBar.[repaint])
			{
			postRepaint()
			}
			else if (ed.source === scrollH && ed.type == ScrollBar.[scrollMoved])
			{
			xScroll = scrollH.getScrollPos()
			}
			else if (ed.source === scrollH && ed.type == ScrollBar.[repaint])
			{
			postRepaint()
			}
		}
	
	void TextArea:setBackground(store Color c)
		{
		bgColor = c
		
		postRepaint()
		}
	
	void TextArea:setBorder(store Color c)
		{
		borderColor = c
		
		postRepaint()
		}
	
	void TextArea:setHighlight(store Color c)
		{
		highlightColor = c
		
		postRepaint()
		}
	
	void TextArea:setTextColor(store Color c)
		{
		textColor = c
		
		postRepaint()
		}
	
	void TextArea:setFont(store Font f)
		{
		textFont = f
		
		cursorHeight = textFont.getFontMetrics().height + (TEXT_BORDER_V*2)

		spaceWidth = textFont.getTextWidth(" ")

		if (wordWrap) wrapLines()
		
		postRepaint()
		}
	
	char[] getCharacters(TextLine line, int start, int len)
		{
		char result[] = null
		TextContent tc = line.text
		TextFrag fw = tc.frags

		while (start > fw.value.length())
			{
			start -= fw.value.length()
			fw = fw.next
			if (fw == null)
				{
				tc = tc.next
				fw = tc.frags
				}
			}
		
		//out.println("start: $start len $len [$(fw.value.length())]")

		if (start + len <= fw.value.length())
			{
			result = new char[](result, fw.value.subString(start, len))
			return result
			}
			else
			{
			int dlen = (fw.value.length() - start)
			len -= dlen
			result = new char[](result, fw.value.subString(start, dlen))
			fw = fw.next

			if (fw == null)
				{
				tc = tc.next
				fw = tc.frags
				}
			}
		
		while (len >= fw.value.length())
			{
			TextFrag td = fw
			len -= fw.value.length()

			result = new char[](result, fw.value.getRaw())

			fw = fw.next

			if (len == 0)
				{
				return result
				}

			if (fw == null)
				{
				tc = tc.next
				fw = tc.frags
				}
			}
		
		if (len > 0)
			{
			result = new char[](result, fw.value.subString(0, len))
			}
		
		return result
		}
	
	char[] getTextRegion(TextLine startLine, int startPos, TextLine endLine, int endPos, char lineSeparator[])
		{
		if (startLine !== endLine)
			{
			//here we use the process from highlight, but to collect those sections (and add newline characters between lines!)
			char result[] = null

			TextLine ssline = startLine
			TextLine seline = endLine
			int sspos = startPos
			int sepos = endPos

			int cy = 0
			bool inSelect

			TextLine lw = lines

			while (lw != null)
				{
				if (lw === ssline)
					{
					inSelect = true

					result = getCharacters(lw, sspos, getLineCharLength(lw)-sspos)
					}
					else if (lw === seline)
					{
					result = new char[](result, lineSeparator, getCharacters(lw, 0, sepos))
					}
					else if (inSelect)
					{
					result = new char[](result, lineSeparator, getCharacters(lw, 0, getLineCharLength(lw)))
					}
				
				if (lw === seline)
					{
					inSelect = false
					break
					}

				lw = lw.next
				cy += cursorHeight
				}

			return result
			}
			else
			{
			int s = startPos
			int e = endPos
			
			return getCharacters(startLine, s, e-s)
			}

		return null
		}
	
	char[] getSelectedText()
		{
		if (selectStartLine !== cursorLine)
			{
			//here we use the process from highlight, but to collect those sections (and add newline characters between lines!)
			TextLine ssline
			TextLine seline
			int sspos
			int sepos

			if (cursorPositionY < selectStartY)
				{
				ssline = cursorLine
				seline = selectStartLine
				sspos = cursorPosition
				sepos = selectStartPos
				}
				else
				{
				ssline = selectStartLine
				seline = cursorLine
				sspos = selectStartPos
				sepos = cursorPosition
				}
			
			return getTextRegion(ssline, sspos, seline, sepos, "\n")
			}
			else
			{
			int s
			int e
			
			if (selectStartPos < cursorPosition)
				{
				s = selectStartPos
				e = cursorPosition
				}
				else
				{
				e = selectStartPos
				s = cursorPosition
				}
			
			return getCharacters(cursorLine, s, e-s)
			}
		}
	
	void updateScrollMax()
		{
		//vertical scroll
		int maxHeight = (lineCountV * cursorHeight) + cursorHeight
		int maxScrollY = 0
		if (maxHeight > textAreaHeight)
			maxScrollY = maxHeight - textAreaHeight
		scrollV.setMaxValue(maxScrollY)

		//horizontal scroll
		int maxWidth = (longestLine.pixelLength) + CURSOR_WIDTH
		int maxScrollX = 0
		if (maxWidth > textAreaWidth)
			maxScrollX = maxWidth - textAreaWidth
		scrollH.setMaxValue(maxScrollX)
		}

	void removeLine(TextLine td)
		{
		if (td.prev != null) td.prev.next = td.next
		if (td.next != null) td.next.prev = td.prev

		if (lines === td) lines = td.next
		if (lastLine === td) lastLine = td.prev

		td.prev = null
		td.next = null

		lineCount --
		lineCountV -= td.lineCount

		td = null

		int newWidth = getNumberAreaWidth(lineCount)
		if (newWidth != lineNumberAreaWidth)
			{
			lineNumberAreaWidth = newWidth
			updateTextAreaWidth()
			if (wordWrap) wrapLines()
			}

		updateScrollMax()
		}
	
	void addLine(TextLine nl)
		{
		TextLine tw = shortestLine
		while ((tw != null) && (nl.pixelLength > tw.pixelLength))
			{
			tw = tw.nextLen
			}
		
		if (tw != null)
			{
			nl.nextLen = tw
			if (tw.prevLen != null)
				tw.prevLen.nextLen = nl
				else
				shortestLine = nl
			nl.prevLen = tw.prevLen
			tw.prevLen = nl
			}
			else
			{
			longestLine.nextLen = nl
			nl.prevLen = longestLine
			longestLine = nl
			}
		
		lineCount ++
		lineCountV ++

		int newWidth = getNumberAreaWidth(lineCount)
		if (newWidth != lineNumberAreaWidth)
			{
			lineNumberAreaWidth = newWidth
			updateTextAreaWidth()
			if (wordWrap) wrapLines()
			}

		updateScrollMax()
		}
	
	void removeFrag(TextLine line, TextFrag frag)
		{
		if (line.text === frag)
			{
			line.text.frags = frag.next
			line.text.frags.prev = null
			}
			else if (line.textEnd.fragsEnd === frag)
			{
			line.textEnd.fragsEnd = frag.prev
			line.textEnd.fragsEnd.next = null
			}
			else
			{
			frag.prev.next = frag.next
			frag.next.prev = frag.prev
			}
		
		frag.next = null
		frag.prev = null
		}
	
	void clearLine(TextLine line)
		{
		TextContent sw = line.text

		int lineHeight = 0

		while (sw != null)
			{
			TextContent td = sw

			TextFrag fw = sw.frags
			while (fw != null)
				{
				TextFrag ftd = fw

				fw = fw.next

				ftd.next = null
				ftd.prev = null
				}
			
			sw = sw.next

			td.frags = null
			td.fragsEnd = null

			td.next = null
			td.prev = null
			}
		
		line.text = null
		line.textEnd = null

		line.text = new TextContent(new TextFrag(TextFrag.T_TEXT, new StringUTF("")))
		line.textEnd = line.text
		line.text.fragsEnd = line.text.frags

		updateLineLength(line)
		}
	
	void deleteCharacters(TextLine line, int start, int len)
		{
		//NOTE: this function never deletes TextContent sub-lines because wrapLine() always resets each line, so the work is done there
		if (start == 0 && len == getLineCharLength(line))
			{
			clearLine(line)
			}
			else
			{
			TextContent tc = line.text
			TextFrag fw = tc.frags

			while (start > fw.value.length())
				{
				start -= fw.value.length()
				fw = fw.next
				if (fw == null)
					{
					tc = tc.next
					fw = tc.frags
					}
				}
			
			//out.println("start: $start len $len [$(fw.value.length())]")

			if (start + len <= fw.value.length())
				{
				fw.value.delete(start, len)
				return
				}
				else
				{
				int dlen = (fw.value.length() - start)
				len -= dlen
				fw.value.delete(start, dlen)
				fw = fw.next

				if (fw == null)
					{
					tc = tc.next
					fw = tc.frags
					}
				}
			
			while (len >= fw.value.length())
				{
				TextFrag td = fw
				len -= fw.value.length()

				fw = fw.next

				if (td === tc.frags)
					{
					if (td.next != null) td.next.prev = null
					tc.frags = td.next
					}
					else if (td === tc.fragsEnd)
					{
					tc.fragsEnd = td.prev
					td.prev.next = null
					}
					else
					{
					td.prev.next = td.next
					td.next.prev = td.prev
					}
				
				td.prev = null
				td.next = null

				if (len == 0)
					{
					TextContent cd = tc
					tc = tc.next

					return
					}

				if (fw == null)
					{
					TextContent cd = tc
					tc = tc.next
					fw = tc.frags
					}
				}
			
			if (len > 0)
				{
				//TODO: if tc is not the starting one, and this delete would leave the frag empty, delete the frag, and if it's the only frag in the tc, delete the tc as well
				fw.value.delete(0, len)
				}
			}
		}
	
	void appendLine(TextLine line, TextLine append)
		{
		/*
		if (line.textEnd.type == TextFrag.T_TEXT && append.text.type == TextFrag.T_TEXT)
			{
			line.textEnd.value.append(append.text.getRaw())
			append.text = append.text.next
			append.text.prev = null
			}
		*/

		line.textEnd.fragsEnd.next = append.text.frags
		append.text.frags.prev = line.textEnd.fragsEnd
		line.textEnd.fragsEnd = append.textEnd.fragsEnd

		//if we're appending to an empty line, we'll have a zero-length text starting frag, which we remove (some functions assume no zero-length frags)
		if (line.textEnd.frags.type != TextFrag.T_TAB && line.textEnd.frags.value.length() == 0)
			{
			line.textEnd.frags.next.prev = null
			line.textEnd.frags = line.textEnd.frags.next
			}
		}
	
	void deleteSelection()
		{
		if (selectStartLine !== cursorLine)
			{
			//use process from highlight, but to delete those sections

			TextLine ssline
			TextLine seline
			int sspos
			int sepos
			int yFinish = 0

			if (cursorPositionY < selectStartY)
				{
				ssline = cursorLine
				seline = selectStartLine
				sspos = cursorPosition
				sepos = selectStartPos
				yFinish = cursorPositionY
				}
				else
				{
				ssline = selectStartLine
				seline = cursorLine
				sspos = selectStartPos
				sepos = cursorPosition
				yFinish = selectStartY
				}
			
			//deleteCharacters on the end of the starting line, if appropriate
			// - delete each intermediate line we reach seline
			// - deleteCharacters on seline, if appropriate
			// - append the last line to the first line, if appropriate
			// (in general we want to minimise the number of places we deal directly with frags/fragsEnd, to scope word-wrap calculations to a few functions)

			int e = getLineCharLength(ssline)
			deleteCharacters(ssline, sspos, e - sspos)
			if (wordWrap) wrapLine(ssline) //deleteCharacters() assumes wrapLine is always called after it, to re-flow fragments over sublines

			while (ssline.next !== seline)
				{
				removeLine(ssline.next)
				}
			
			//now we need to join seline onto ssline
			if (sepos < getLineCharLength(seline))
				{
				//join seline onto ssline
				deleteCharacters(seline, 0, sepos)
				if (wordWrap) wrapLine(seline)
				removeLine(seline)
				appendLine(ssline, seline)
				}
				else
				{
				cursorLine = seline.next
				removeLine(seline)
				}
			
			cursorLine = ssline
			selectStartLine = ssline
			selectStartPos = sspos
			selectStartY = yFinish
			cursorPositionY = yFinish

			emitevent textChanged()
			}
			else
			{
			int s
			int e
			
			if (selectStartPos < cursorPosition)
				{
				s = selectStartPos
				e = cursorPosition
				}
				else
				{
				e = selectStartPos
				s = cursorPosition
				}
			
			if (s != e)
				{
				deleteCharacters(cursorLine, s, e-s)
				emitevent textChanged()
				}
			}
		}
	
	void indentLine(TextLine line)
		{
		insertTab(line, 0)
		}
	
	void indentSelection()
		{
		TextLine ssline
		TextLine seline

		if (cursorPositionY < selectStartY)
			{
			ssline = cursorLine
			seline = selectStartLine
			}
			else
			{
			ssline = selectStartLine
			seline = cursorLine
			}
		
		indentLine(ssline)

		if (ssline === seline) return

		ssline = ssline.next
		while (ssline !== seline)
			{
			indentLine(ssline)
			ssline = ssline.next
			}
		
		indentLine(ssline)

		if (cursorPositionY < selectStartY)
			selectStartPos += 1
			else
			cursorPosition += 1
		}
	
	void unindentLine(TextLine line)
		{
		if (line.text.frags.type == TextFrag.T_TAB)
			{
			if (line.text.frags.next != null)
				{
				line.text.frags.next.prev = null
				line.text.frags = line.text.frags.next
				}
				else
				{
				line.text.frags.type = TextFrag.T_TEXT
				line.text.frags.value = new StringUTF("")
				}
			}
			//TODO: else if it's a space, try to delete up to TAB_SPACES of them
		}
	
	void unindentSelection()
		{
		TextLine ssline
		TextLine seline

		if (cursorPositionY < selectStartY)
			{
			ssline = cursorLine
			seline = selectStartLine
			}
			else
			{
			ssline = selectStartLine
			seline = cursorLine
			}
		
		unindentLine(ssline)

		if (ssline === seline)
			{
			if (cursorPositionY < selectStartY)
				selectStartPos -= 1
				else
				cursorPosition -= 1
			return
			}

		ssline = ssline.next
		while (ssline !== seline)
			{
			unindentLine(ssline)
			ssline = ssline.next
			}
		
		unindentLine(ssline)

		if (cursorPositionY < selectStartY)
			selectStartPos -= 1
			else
			cursorPosition -= 1
		}
	
	void appendTextTo(TextLine line, char txt[], opt bool isParticle)
		{
		if (!isParticle && line.textEnd.fragsEnd.type == TextFrag.T_TEXT)
			{
			line.textEnd.fragsEnd.value.append(txt)
			}
			else
			{
			TextFrag nf
			if (line.textEnd.fragsEnd.value.length() == 0)
				{
				nf = line.textEnd.fragsEnd
				nf.value = new StringUTF(txt)
				nf.type = TextFrag.T_TEXT
				}
				else
				{
				nf = new TextFrag(TextFrag.T_TEXT, new StringUTF(txt))
				line.textEnd.fragsEnd.next = nf
				nf.prev = line.textEnd.fragsEnd
				line.textEnd.fragsEnd = nf
				}
			
			if (isParticle) nf.type = TextFrag.T_PARTICLE
			}
		}
	
	void insertTextTo(TextLine line, int atCharacter, char txt[], opt bool isParticle)
		{
		if (atCharacter == 0)
			{
			if (!isParticle && line.text.frags.type == TextFrag.T_TEXT)
				{
				line.text.frags.value.insert(0, txt)
				}
				else
				{
				TextFrag nf = null
				if (isParticle)
					nf = new TextFrag(TextFrag.T_PARTICLE, new StringUTF(txt))
					else
					nf = new TextFrag(TextFrag.T_TEXT, new StringUTF(txt))
				nf.next = line.text.frags
				line.text.frags.prev = nf
				line.text.frags = nf
				}
			
			return
			}
			else
			{
			//search for insertion position
			TextContent tc = line.text
			int ndx = 0

			while (tc != null)
				{
				TextFrag fw = tc.frags
				while (fw != null)
					{
					int thisLen = fw.value.length()
					if (fw.type == TextFrag.T_TAB) thisLen = 1

					if ((ndx + thisLen) == atCharacter)
						{
						//append, in appropriate mode
						if (!isParticle && fw.type == TextFrag.T_TEXT)
							{
							fw.value.append(txt)
							}
							else
							{
							TextFrag nf = new TextFrag(TextFrag.T_TEXT, new StringUTF(txt))
							if (isParticle) nf.type = TextFrag.T_PARTICLE

							nf.next = fw.next
							if (fw.next != null)
								fw.next.prev = nf
								else
								tc.fragsEnd = nf
							fw.next = nf
							nf.prev = fw 
							}
						
						return
						}
						else if ((ndx + thisLen) > atCharacter)
						{
						//insert in the middle, either as text or by splitting fragments
						if (!isParticle && fw.type == TextFrag.T_TEXT)
							{
							fw.value.insert(atCharacter - ndx, txt)
							}
							else
							{
							int qpos = atCharacter - ndx
							TextFrag nf = new TextFrag(TextFrag.T_TEXT, new StringUTF(fw.value.subString(qpos, fw.value.length() - qpos)))
							fw.value.delete(qpos, fw.value.length() - qpos)

							//out.println("split frag: '$(fw.value.getRaw())' / '$(nf.value.getRaw())'")

							TextFrag nfB = new TextFrag(TextFrag.T_PARTICLE, new StringUTF(txt))

							nfB.next = nf
							nfB.prev = fw

							nf.prev = nfB
							nf.next = fw.next
							
							if (fw.next != null)
								fw.next.prev = nf
								else
								line.text.fragsEnd = nf
							fw.next = nfB

							//out.println("seqe: frag: '$(fw.value.getRaw())' -> '$(fw.next.value.getRaw())' -> '$(fw.next.next.value.getRaw())'")
							}
						
						return
						}
						else
						{
						//move to next frag
						ndx += thisLen
						}

					fw = fw.next
					}
				
				tc = tc.next
				}
			}
		}
	
	bool isFilterCharacter(char k[])
		{
		if (k == "\r")
			return true
		
		return false
		}
	
	bool isLineWrapCharacter(char k[])
		{
		if (k == " ")
			return true
		
		return false
		}
	
	void appendText(TextLine toLine, char text[])
		{
		int lastIndex = 0

		for (int i = 0; i < text.arrayLength; i++)
			{
			char k[] = text[i]
			if (k == "\n")
				{
				//make a new line

				char nxt[] = text.subString(lastIndex, i - lastIndex)

				appendTextTo(toLine, nxt)
				updateLineLength(toLine)

				TextLine ntl = makeLine()

				ntl.next = toLine.next
				ntl.prev = toLine

				if (toLine.next != null)
					toLine.next.prev = ntl
					else
					lastLine = ntl
				
				toLine.next = ntl

				toLine = ntl
				cursorPositionY += cursorHeight
				
				addLine(ntl)

				lastIndex = i + 1
				}
				else if (isFilterCharacter(k) || k == "\t" || isLineWrapCharacter(k))
				{
				if (lastIndex != i)
					{
					char nxt[] = text.subString(lastIndex, i - lastIndex)
					appendTextTo(toLine, nxt)
					updateLineLength(toLine)
					}
				lastIndex = i + 1

				if (k == "\t")
					{
					appendTab(toLine)
					}
					else if (isLineWrapCharacter(k))
					{
					appendTextTo(toLine, k, true)
					updateLineLength(toLine)
					}
				}
			}
		
		char nxt[] = text.subString(lastIndex, text.arrayLength - lastIndex)

		appendTextTo(toLine, nxt)
		updateLineLength(toLine)
		}
	
	void insertText(char text[])
		{
		int lastIndex = 0

		char appendX[] = null

		if (cursorPosition < getLineCharLength(cursorLine))
			{
			appendX = getCharacters(cursorLine, cursorPosition, getLineCharLength(cursorLine) - cursorPosition)
			deleteCharacters(cursorLine, cursorPosition, getLineCharLength(cursorLine) - cursorPosition)
			if (wordWrap) wrapLine(cursorLine)
			}
		
		appendText(cursorLine, text)

		if (appendX != null)
			{
			appendText(cursorLine, appendX)
			updateLineLength(cursorLine)
			}
		
		cursorPosition = getLineCharLength(cursorLine)

		selectStartLine = cursorLine
		selectStartPos = cursorPosition
		selectStartY = cursorPositionY
		
		emitevent textChanged()
		}
	
	bool isWhitespace(char value[])
		{
		for (int i = 0; i < value.arrayLength; i++)
			{
			if (value[i] != " " && value[i] != "\t")
				return false
			}
		
		return true
		}
	
	void splitLineDown()
		{
		//check if any text comes after this cursor position on the current line; if so move it down to the next line
		TextLine ntl = makeLine()
		addLine(ntl)

		char str[] = null
		
		if (cursorPosition < getLineCharLength(cursorLine))
			{
			str = getCharacters(cursorLine, cursorPosition, getLineCharLength(cursorLine) - cursorPosition)
			deleteCharacters(cursorLine, cursorPosition, getLineCharLength(cursorLine) - cursorPosition)
			updateLineLength(cursorLine)
			}
		
		//if we're NOT currently on the last line, we need to INSERT a line after the current one
		if (cursorLine.next != null)
			{
			ntl.next = cursorLine.next
			ntl.prev = cursorLine
			
			cursorLine.next.prev = ntl
			cursorLine.next = ntl
			}
			else
			{
			lastLine.next = ntl
			ntl.prev = lastLine
			lastLine = ntl
			}
		
		cursorLine = ntl
		cursorSubline = cursorLine.text
		selectStartLine = cursorLine
		cursorPosition = 0
		selectStartPos = 0

		bool insertion = false

		if (autoIndent)
			{
			TextFrag fw = ntl.prev.text.frags
			while (fw != null)
				{
				if (!isWhitespace(fw.value.getRaw()))
					{
					break
					}
					else
					{
					insertText(fw.value.getRaw())
					insertion = true
					}
				fw = fw.next
				}
			}

		if (str != null)
			{
			int prevCP = cursorPosition
			insertText(str)
			cursorPosition = prevCP
			selectStartPos = cursorPosition

			insertion = true
			}
		
		if (!insertion)
			{
			cursorPositionY += cursorHeight
			selectStartY = cursorPositionY
			}
		}
	
	void updateLineLength(TextLine line)
		{
		//re-calculate the length-in-pixels of this line

		int preLength = line.pixelLength

		line.pixelLength = 0

		TextFrag fw = line.text.frags
		while (fw != null)
			{
			if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
				line.pixelLength += textFont.getTextWidth(fw.value.getRaw())
				else
				line.pixelLength += (TAB_SPACES * spaceWidth)
			fw = fw.next
			}
		
		//re-calculate word-wrap status, if appropriate
		if (wordWrap) wrapLine(line)

		//...and now adjust sorting order relative to lines around us

		if (preLength < line.pixelLength && longestLine !== line)
			{
			TextLine tw = line.nextLen

			while ((tw != null) && (line.pixelLength > tw.pixelLength))
				{
				tw = tw.nextLen
				}
			
			if (tw !== line.nextLen)
				{
				//remove it from its old position
				if (line.prevLen != null)
					line.prevLen.nextLen = line.nextLen
					else
					shortestLine = line.nextLen
				
				line.nextLen.prevLen = line.prevLen

				//add it to its new position
				if (tw != null)
					{
					tw.prevLen.nextLen = line
					line.prevLen = tw.prevLen
					tw.prevLen = line
					}
					else
					{
					line.prevLen = longestLine
					longestLine.nextLen = line
					longestLine = line
					}

				line.nextLen = tw
				}
			}
			else if (line.pixelLength < preLength && shortestLine !== line)
			{
			//search in the other direction (we got shorter)

			TextLine tw = line.prevLen

			while ((tw != null) && (line.pixelLength < tw.pixelLength))
				{
				tw = tw.prevLen
				}
			
			if (tw !== line.prevLen)
				{
				//remove it from its old position
				if (line.nextLen != null)
					line.nextLen.prevLen = line.prevLen
					else
					longestLine = line.prevLen
				
				line.prevLen.nextLen = line.nextLen

				//add it to its new position
				if (tw != null)
					{
					tw.nextLen.prevLen = line
					line.nextLen = tw.nextLen
					tw.nextLen = line
					}
					else
					{
					line.nextLen = shortestLine
					shortestLine.prevLen = line
					shortestLine = line
					}

				line.prevLen = tw
				}
			}
		
		updateScrollMax()
		}
	
	void printLine(char dbg[], TextLine forLine)
		{
		out.print(dbg)

		TextContent sw = forLine.text

		int lineHeight = 0

		while (sw != null)
			{
			TextFrag fw = sw.frags
			while (fw != null)
				{
				if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
					{
					out.print("'$(fw.value.getRaw())' ")
					}
					else
					{
					out.print("")
					}
				
				fw = fw.next
				}
			
			sw = sw.next
			}
		
		out.println("")
		}
	
	bool TextArea:keyDown(int keyCode)
		{
		//here we check if the key press was a printable character and if so insert it, else handle other key strokes
		
		char ch[] = keyMapping.getCharacter(keyCode, keyState)
		byte kCode
		
		if (ch != null)
			{
			mutex(textLock)
				{
				if (selectStartLine !== cursorLine || selectStartPos != cursorPosition)
					{
					deleteSelection()
					if (selectStartPos > cursorPosition)
						selectStartPos = cursorPosition
						else
						cursorPosition = selectStartPos
					}
				
				if (cursorPosition == getLineCharLength(cursorLine))
					{
					appendTextTo(cursorLine, ch, isLineWrapCharacter(ch))
					}
					else
					{
					insertTextTo(cursorLine, cursorPosition, ch, isLineWrapCharacter(ch))
					}
				
				cursorPosition ++
				selectStartPos = cursorPosition

				//printLine("DBG-UL:", cursorLine)

				updateLineLength(cursorLine)

				//printLine("DBG-UC:", cursorLine)
				}
			
			updateCursorPosition()

			//printLine("DBG-TC:", cursorLine)
			
			emitevent textChanged()
			
			postRepaint()
			}
			else if ((kCode = keyMapping.getKeyCode(keyCode)) != KeyCode.OTHER)
			{
			if (kCode == KeyCode.SHIFT_LEFT || kCode == KeyCode.SHIFT_RIGHT)
				{
				keyState |= KeyState.KEY_SHIFT
				shiftDown = true
				return true
				}
			
			if (kCode == KeyCode.CTRL_LEFT || kCode == KeyCode.CTRL_RIGHT)
				{
				keyState |= KeyState.KEY_CTRL
				return true
				}
			
			if (kCode == KeyCode.ALT_LEFT)
				{
				keyState |= KeyState.KEY_ALT
				return true
				}
			
			if (kCode == KeyCode.ALT_RIGHT)
				{
				keyState |= KeyState.KEY_ALTGR
				return true
				}
			
			if (kCode == KeyCode.CMD)
				{
				keyState |= KeyState.KEY_CMD
				return true
				}
			
			if (kCode == KeyCode.TAB)
				{
				mutex(textLock)
					{
					if (selectStartLine !== cursorLine || (selectStartPos != cursorPosition && (selectStartPos == 0 || cursorPosition == 0)))
						{
						//indent+/- this text region
						if (!shiftDown)
							indentSelection()
							else
							unindentSelection()
						}
						else
						{
						if (selectStartPos != cursorPosition)
							{
							deleteSelection()
							if (selectStartPos > cursorPosition)
								selectStartPos = cursorPosition
								else
								cursorPosition = selectStartPos
							}
						
						if (cursorPosition == getLineCharLength(cursorLine))
							{
							appendTab(cursorLine)
							}
							else
							{
							insertTab(cursorLine, cursorPosition)
							}
						
						cursorPosition ++
						selectStartPos = cursorPosition
						}

					updateLineLength(cursorLine)
					updateCursorPosition()
					}
				}
			
			if (kCode == KeyCode.DELETE)
				{
				mutex(textLock)
					{
					if (cursorLine === selectStartLine && cursorPosition == selectStartPos)
						{
						//deal with cursor at the end of a line, deleting the next line and appending any content to this one

						if (cursorPosition < getLineCharLength(cursorLine))
							{
							selectStartPos = cursorPosition + 1
							deleteSelection()
							selectStartPos --

							updateLineLength(cursorLine)
							}
							else if (cursorLine.next != null)
							{
							TextLine td = cursorLine.next

							appendLine(cursorLine, cursorLine.next)

							removeLine(td)

							updateLineLength(cursorLine)
							}
						}
						else
						{
						deleteSelection()
						
						if (cursorPosition > selectStartPos)
							{
							cursorPosition = selectStartPos
							}
							else
							{
							selectStartPos = cursorPosition
							}
						}
					}
				
				updateCursorPosition()
				}
			
			if (kCode == KeyCode.BACKSPACE)
				{
				mutex(textLock)
					{
					if (cursorLine === selectStartLine && cursorPosition == selectStartPos)
						{
						//deal with cursor at the start of a line, deleting the current line and appending any content to the previous one

						if (cursorPosition > 0)
							{
							selectStartPos = cursorPosition - 1
							deleteSelection()
							cursorPosition --

							updateLineLength(cursorLine)
							}
							else if (cursorLine.prev != null)
							{
							cursorPosition = cursorLine.prev.text.frags.value.length()
							selectStartPos = cursorPosition

							cursorPositionY -= cursorHeight

							TextLine td = cursorLine

							appendLine(cursorLine.prev, cursorLine)

							cursorLine = cursorLine.prev

							removeLine(td)

							selectStartLine = cursorLine
							selectStartY = cursorPositionY

							updateLineLength(cursorLine)
							}
						}
						else
						{
						deleteSelection()
						updateLineLength(cursorLine)
						
						if (cursorPosition > selectStartPos)
							{
							cursorPosition = selectStartPos
							}
							else
							{
							selectStartPos = cursorPosition
							}
						}
					}
				
				updateCursorPosition()
				}
			
			if (kCode == KeyCode.RETURN)
				{
				mutex(textLock)
					{
					//delete selection, if any
					deleteSelection()

					splitLineDown()

					updateCursorPosition()
					}
				}
			
			if (kCode == KeyCode.ARROW_LEFT)
				{
				int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)

				if (thisLinePos > 0)
					{
					cursorPosition --
					
					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						}
					
					updateCursorPosition()
					}
					else if (cursorSubline.prev != null)
					{
					TextContent goto = cursorSubline.prev
					int lineEndPos = getSublineCharLength(goto)
					int gotoPos = getLineCursorPos(cursorLine, goto, lineEndPos)
					
					cursorPosition = gotoPos
					cursorSubline = goto
					
					cursorPositionY -= cursorHeight

					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						selectSubline = cursorSubline
						selectStartY = cursorPositionY
						}
					
					updateCursorPosition()
					}
					else if (cursorLine.prev != null)
					{
					TextLine goto = cursorLine.prev
					TextContent gotoSub = goto.textEnd

					cursorPosition = getLineCharLength(goto)
					cursorPositionY -= cursorHeight

					cursorLine = goto
					cursorSubline = gotoSub

					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						selectSubline = cursorSubline
						selectStartY = cursorPositionY
						}
					
					updateCursorPosition()
					}
				}
			
			if (kCode == KeyCode.ARROW_RIGHT)
				{
				int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
				
				if (thisLinePos < getSublineCharLength(cursorSubline))
					{
					cursorPosition ++
					
					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						}
					
					updateCursorPosition()
					}
					else if (cursorSubline.next != null)
					{
					TextContent goto = cursorSubline.next
					int gotoPos = getLineCursorPos(cursorLine, goto, 0)
					
					cursorPosition = gotoPos
					cursorSubline = goto
					
					cursorPositionY += cursorHeight

					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						selectSubline = cursorSubline
						selectStartY = cursorPositionY
						}
					
					updateCursorPosition()
					}
					else if (cursorLine.next != null)
					{
					cursorPosition = 0
					cursorPositionY += cursorHeight

					cursorLine = cursorLine.next
					cursorSubline = cursorLine.text

					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						selectSubline = cursorSubline
						selectStartY = cursorPositionY
						}
					
					updateCursorPosition()
					}
				}
			
			if (kCode == KeyCode.ARROW_UP)
				{
				if (cursorSubline.prev != null)
					{
					TextContent goto = cursorSubline.prev

					int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)

					int gotoPos = getLineCursorPos(cursorLine, goto, thisLinePos)
					int lineEndPos = getSublineCharLength(goto)

					if (thisLinePos > lineEndPos)
						{
						gotoPos = getLineCursorPos(cursorLine, goto, lineEndPos)
						}
					
					cursorSubline = goto
					cursorPosition = gotoPos
					
					cursorPositionY -= cursorHeight

					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						selectSubline = cursorSubline
						selectStartY = cursorPositionY
						}
					
					updateCursorPosition()
					}
					else if (cursorLine.prev != null)
					{
					TextLine goto = cursorLine.prev
					TextContent gotoSub = goto.textEnd

					int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
					int lineEndPos = getSublineCharLength(gotoSub)
					
					if (thisLinePos > lineEndPos)
						{
						cursorPosition = getLineCursorPos(goto, gotoSub, lineEndPos)
						}
						else
						{
						cursorPosition = getLineCursorPos(goto, gotoSub, thisLinePos)
						}
					
					cursorLine = goto
					cursorSubline = gotoSub
					
					cursorPositionY -= cursorHeight

					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						selectStartY = cursorPositionY
						}
					
					updateCursorPosition()
					}
					else
					{
					cursorPosition = 0
					selectStartPos = 0
					}
				}
			
			if (kCode == KeyCode.ARROW_DOWN)
				{
				if (cursorSubline.next != null)
					{
					TextContent goto = cursorSubline.next

					int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)

					int gotoPos = getLineCursorPos(cursorLine, goto, thisLinePos)
					int lineEndPos = getSublineCharLength(goto)

					if (thisLinePos > lineEndPos)
						{
						gotoPos = getLineCursorPos(cursorLine, goto, lineEndPos)
						}
					
					cursorSubline = goto
					cursorPosition = gotoPos
					
					cursorPositionY += cursorHeight

					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						selectSubline = cursorSubline
						selectStartY = cursorPositionY
						}
					
					updateCursorPosition()
					}
					else if (cursorLine.next != null)
					{
					TextLine goto = cursorLine.next
					TextContent gotoSub = goto.text

					int thisLinePos = getSublinePos(cursorLine, cursorSubline, cursorPosition)
					int lineEndPos = getSublineCharLength(gotoSub)
					
					if (thisLinePos > lineEndPos)
						{
						cursorPosition = getLineCursorPos(goto, gotoSub, lineEndPos)
						}
						else
						{
						cursorPosition = getLineCursorPos(goto, gotoSub, thisLinePos)
						}
					
					cursorLine = goto
					cursorSubline = gotoSub
					
					cursorPositionY += cursorHeight

					if (!shiftDown)
						{
						selectStartPos = cursorPosition
						selectStartLine = cursorLine
						selectStartY = cursorPositionY
						}
					
					updateCursorPosition()
					}
					else
					{
					cursorPosition = getLineCharLength(cursorLine)
					selectStartPos = cursorPosition
					}
				}
			
			if (kCode == KeyCode.END)
				{
				cursorPosition = getLineCharLength(cursorLine)
				//cursorPositionY = getLineHeight(cursorLine) //TODO.
				
				if (!shiftDown)
					selectStartPos = cursorPosition
				
				updateCursorPosition()
				}
			
			if (kCode == KeyCode.HOME)
				{
				cursorPosition = 0
				
				if (!shiftDown)
					selectStartPos = cursorPosition
				
				updateCursorPosition()
				}
			
			postRepaint()
			}
		
		return true
		}
	
	bool TextArea:keyUp(int keyCode)
		{
		byte kCode
		
		if ((kCode = keyMapping.getKeyCode(keyCode)) != KeyCode.OTHER)
			{
			if (kCode == KeyCode.SHIFT_LEFT || kCode == KeyCode.SHIFT_RIGHT)
				{
				keyState &= ~KeyState.KEY_SHIFT
				shiftDown = false
				return true
				}
			
			if (kCode == KeyCode.CTRL_LEFT || kCode == KeyCode.CTRL_RIGHT)
				{
				keyState &= ~KeyState.KEY_CTRL
				return true
				}
			
			if (kCode == KeyCode.ALT_LEFT)
				{
				keyState &= ~KeyState.KEY_ALT
				}
			
			if (kCode == KeyCode.ALT_RIGHT)
				{
				keyState &= ~KeyState.KEY_ALTGR
				return true
				}
			
			if (kCode == KeyCode.CMD)
				{
				keyState &= ~KeyState.KEY_CMD
				}
			}
		
		return true
		}
	
	Rect TextArea:getBounds()
		{
		return new Rect(xPosition, yPosition, width, height)
		}
	
	int getLineNumberAreaWidth()
		{
		char lnMax[] = lineCount.makeString()
		int len = textFont.getTextWidth(lnMax) + LINE_NUMBER_MARGIN
		return len
		}
	
	CursorPos findCursorPos(int x, int y)
		{
		if (lineNumbers) x -= getLineNumberAreaWidth()

		//out.println("FCP $x.$y")

		int lnIndex = 0

		//first we find the line
		TextLine lw = lines
		int lineBottom = 0
		int lineTop = 0
		while (lw != null)
			{
			lineTop = lineBottom
			lineBottom += getLineHeight(lw)

			//out.println("LB: $lineBottom / LH $(getLineHeight(lw)) / LT $lineTop")
			if (y < lineBottom)
				break

			lnIndex ++
			lw = lw.next
			}
		
		if (lw == null)
			{
			//out.println("line-revert")
			lw = lastLine
			lineBottom -= cursorHeight
			}
		
		//now find the character on that line
		TextContent cw = lw.text
		int charCount = 0

		while ((lineTop + cursorHeight) < y && cw.next != null)
			{
			charCount += getSublineCharLength(cw)
			cw = cw.next
			lineTop += cursorHeight
			}
		
		//we're now on the right subline
		int subIndex = 0
		int totalWidth = 0
		TextFrag fw = cw.frags
		for (int i = 0; i < getSublineCharLength(cw); i++)
			{
			int ptw = totalWidth

			if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
				{
				//out.println("'$(fw.value.getRaw())' $(fw.value.length()) / $subIndex")
				totalWidth += textFont.getTextWidth(fw.value.subString(subIndex, 1))
				charCount ++
				subIndex ++

				if (subIndex == fw.value.length())
					{
					fw = fw.next
					subIndex = 0
					}
				}
				else if (fw.type == TextFrag.T_TAB)
				{
				int nspaces = TAB_SPACES - (charCount % TAB_SPACES)
				totalWidth += nspaces * spaceWidth
				fw = fw.next
				}
			
			if (totalWidth >= x)
				{
				//decide which side of the character is closer to the actual x location
				if ((totalWidth - x) < (x - ptw))
					return new CursorPos(charCount, lineTop, lw, cw)
					else
					return new CursorPos(charCount-1, lineTop, lw, cw)
				}
			}
		
		return new CursorPos(getLineCursorPos(lw, cw, getSublineCharLength(cw)), lineTop, lw, cw)
		}
	
	void TextArea:click(int x, int y, int button)
		{
		if (showScrollV && (x >= width-SCR_BUTTON_WIDTH && x <= width))
			{
			scrollV.click(x - (width-SCR_BUTTON_WIDTH), y, button)
			}
			else if (showScrollH && (y >= height-SCR_BUTTON_WIDTH && y <= height))
			{
			scrollH.click(x, y - (height - SCR_BUTTON_HEIGHT), button)
			}
			else
			{
			if (!focus)
				{
				setFocus()
				postRepaint()
				}
			
			if (button == MouseButtons.BUTTON_RIGHT)
				{
				menu.xAnchor = x
				menu.yAnchor = y
				emitevent contextMenuOn(menu)
				}
			}
		}
	
	bool isAlphaNum(char x[])
		{
		if (x.arrayLength > 1) return false
		
		if ((x[0] >= "a") && (x[0] <= "z")) return true
		
		if ((x[0] >= "A") && (x[0] <= "Z")) return true
		
		if ((x[0] >= "0") && (x[0] <= "9")) return true
		
		return false
		}
	
	void TextArea:clickMulti(int x, int y, int button, int clicks)
		{
		if (showScrollV && (x >= width-SCR_BUTTON_WIDTH && x <= width))
			{
			scrollV.click(x - (width-SCR_BUTTON_WIDTH), y, button)
			}
			else if (showScrollH && (y >= height-SCR_BUTTON_WIDTH && y <= height))
			{
			scrollH.click(x, y - (height - SCR_BUTTON_HEIGHT), button)
			}
			else
			{
			if (button == MouseButtons.BUTTON_LEFT && clicks == 2)
				{
				//check if this position is inside a word, with no special characters; if so, highlight that word and place the cursor at its end
				// - we keep doing subString() on either side of the cursor position, for a length of one character, to see if that single character is a  character, and if so we keep expanding our selection box on both sides until we hit something else
				CursorPos cp = findCursorPos(x + xScroll, y + yScroll)

				cursorPosition = cp.character
				selectStartPos = cursorPosition
				
				int xl = cursorPosition
				
				while (xl < getLineCharLength(cursorLine))
					{
					char n[] = getCharacters(cursorLine, xl, 1)
					
					if (isAlphaNum(n))
						{
						xl ++
						}
						else
						{
						break
						}
					}
				
				cursorPosition = xl
				
				int xr = selectStartPos
				
				while (xr > 0)
					{
					char n[] = getCharacters(cursorLine, xr-1, 1)
					
					if (isAlphaNum(n))
						{
						xr --
						}
						else
						{
						break
						}
					}
				
				selectStartPos = xr

				updateCursorPosition()

				postRepaint()
				}
				else if (button == MouseButtons.BUTTON_LEFT && clicks == 3)
				{
				//select all
				selectStartPos = 0
				cursorPosition = getLineCharLength(cursorLine)

				postRepaint()
				}
			}
		}
	
	void processMenuClick(MenuItem item)
		{
		if (item === copyMenu)
			{
			clipboard.setContent(getSelectedText())
			}
			else if (item === cutMenu)
			{
			mutex(textLock)
				{
				clipboard.setContent(getSelectedText())
				deleteSelection()
				if (selectStartPos > cursorPosition)
					selectStartPos = cursorPosition
					else
					cursorPosition = selectStartPos
				}
			
			updateCursorPosition()
			
			emitevent repaint()
			}
			else if (item === pasteMenu)
			{
			mutex(textLock)
				{
				char itext[] = clipboard.getContent()
				deleteSelection()
				if (selectStartPos > cursorPosition)
					selectStartPos = cursorPosition
					else
					cursorPosition = selectStartPos
				
				StringUTF tmp = new StringUTF(itext)
				int ncp = cursorPosition + tmp.length()

				insertText(itext)

				cursorPosition = ncp
				selectStartPos = cursorPosition

				updateCursorPosition()
				}
			
			emitevent repaint()
			}
			else if (item === selectAllMenu)
			{
			selectStartLine = lines
			cursorLine = lastLine

			selectStartPos = 0
			cursorPosition = getLineCharLength(cursorLine)

			selectStartY = 0
			cursorPositionY = lineCountV * cursorHeight

			updateCursorPosition()
			
			emitevent repaint()
			}
		}
	
	void TextArea:contextClick(MenuItem item)
		{
		processMenuClick(item)
		}
	
	void TextArea:hotKeyClick(HotKey h)
		{
		if (h === hkCut)
			processMenuClick(cutMenu)
			else if (h === hkCopy)
			processMenuClick(copyMenu)
			else if (h === hkPaste)
			processMenuClick(pasteMenu)
			else if (h === hkSelectAll)
			processMenuClick(selectAllMenu)
		}
	
	void TextArea:mouseWheel(int xAdd, int xSub, int yAdd, int ySub)
		{
		int maxHeight = (lineCountV * cursorHeight) + cursorHeight
		int maxScrollY = 0
		if (maxHeight > textAreaHeight)
			maxScrollY = maxHeight - textAreaHeight

		if (ySub != 0)
			{
			if (yScroll + (ySub*SCROLL_TICKS) > maxScrollY)
				{
				scrollV.setScrollPos(maxScrollY)
				yScroll = maxScrollY
				}
				else
				{
				scrollV.setScrollPos(yScroll + (ySub*SCROLL_TICKS))
				yScroll += (ySub*SCROLL_TICKS)
				}
			}
		
		if (yAdd != 0)
			{
			if (yScroll < (yAdd*SCROLL_TICKS))
				{
				scrollV.setScrollPos(0)
				yScroll = 0
				}
				else
				{
				scrollV.setScrollPos(yScroll - (yAdd*SCROLL_TICKS))
				yScroll -= (yAdd*SCROLL_TICKS)
				}
			}
		
		postRepaint()
		}
	
	void TextArea:mouseUp(int x, int y, int button)
		{
		if (showScrollV && clickDown === scrollV)
			{
			scrollV.mouseUp(x - (width-SCR_BUTTON_WIDTH), y, button)
			}
			else if (showScrollH && clickDown === scrollH)
			{
			scrollH.mouseUp(x, y - height, button)
			}
			else if (button == MouseButtons.BUTTON_LEFT)
			{
			mouseDragCapture = false
			}
		
		clickDown = null
		}
	
	void TextArea:mouseMove(int x, int y)
		{
		if (showScrollV && ((x >= width-SCR_BUTTON_WIDTH && x <= width) || clickDown === scrollV))
			{
			if (cursorType != IOWindow.CURSOR_DEFAULT)
				{
				emitevent setCursor(new CursorSetEvent(IOWindow.CURSOR_DEFAULT))
				cursorType = IOWindow.CURSOR_DEFAULT
				}
			
			if (clickDown === scrollV)
				{
				scrollV.mouseMove(x, y)
				}
			}
			else if (showScrollH && ((y >= height-SCR_BUTTON_HEIGHT && y <= height) || clickDown === scrollH))
			{
			if (cursorType != IOWindow.CURSOR_DEFAULT)
				{
				emitevent setCursor(new CursorSetEvent(IOWindow.CURSOR_DEFAULT))
				cursorType = IOWindow.CURSOR_DEFAULT
				}
			
			if (clickDown === scrollH)
				{
				scrollH.mouseMove(x, y)
				}
			}
			else
			{
			if (cursorType != IOWindow.CURSOR_IBEAM)
				{
				emitevent setCursor(new CursorSetEvent(IOWindow.CURSOR_IBEAM))
				cursorType = IOWindow.CURSOR_IBEAM
				}
			
			if (mouseDragCapture)
				{
				CursorPos cp = findCursorPos(x + xScroll, y + yScroll)

				if (cp.character != cursorPosition || cp.line != cursorLine)
					{
					cursorPosition = cp.character
					cursorLine = cp.line
					cursorPositionY = cp.yPosition
					postRepaint()
					}
				}
			}
		}
	
	void TextArea:mouseDown(int x, int y, int button)
		{
		if (showScrollV && (x >= width-SCR_BUTTON_WIDTH && x <= width))
			{
			scrollV.mouseDown(x - (width-SCR_BUTTON_WIDTH), y, button)
			clickDown = scrollV
			}
			else if (showScrollH && (y >= height-SCR_BUTTON_WIDTH && y <= height))
			{
			scrollH.mouseDown(x, y - (height - SCR_BUTTON_HEIGHT), button)
			clickDown = scrollH
			}
			else if (button == MouseButtons.BUTTON_LEFT)
			{
			//locate this position in the text, and set the cursor there
			CursorPos cp = findCursorPos(x + xScroll, y + yScroll)

			cursorPosition = cp.character
			cursorLine = cp.line
			cursorSubline = cp.subline
			cursorPositionY = cp.yPosition

			if (!shiftDown)
				{
				selectStartPos = cursorPosition
				selectStartLine = cursorLine
				selectSubline = cursorSubline
				selectStartY = cursorPositionY
				}

			mouseDragCapture = true

			clickDown = null

			postRepaint()
			}
		}

	char[] TextArea:getText(opt char lineSeparator[])
		{
		if (lineSeparator == null) lineSeparator = "\n"

		return getTextRegion(lines, 0, lastLine, getLineCharLength(lastLine), lineSeparator)
		}
	
	void clearText()
		{
		bool wrapNote = wordWrap
		wordWrap = false
		TextLine lw = lines.next
		while (lw != null)
			{
			TextLine td = lw
			lw = lw.next

			clearLine(td)

			td.next = null
			td.prev = null
			td = null
			}
		
		lw = shortestLine
		while (lw != null)
			{
			TextLine td = lw
			lw = lw.nextLen

			td.nextLen = null
			td.prevLen = null
			td = null
			}
		
		lineCount = 0
		lineCountV = 0
		
		lines.next = null

		clearLine(lines)
		
		lastLine = lines

		shortestLine = lines
		longestLine = lines

		lines.pixelLength = 0
		lines.charLength = 0
		lines.lineCount = 1

		cursorPosition = 0
		selectStartPos = cursorPosition
		cursorLine = lines
		selectStartLine = cursorLine

		wordWrap = wrapNote
		}
	
	void TextArea:setText(char text[])
		{
		mutex(textLock)
			{
			//delete all lines except the first one, clear the first line, then do a text-insert
			clearText()

			insertText(text)
			}

		updateScrollMax()

		postRepaint()
		}
	
	int getY(TextLine forLine)
		{
		int y = 0
		TextLine lw = lines

		while (lw !== forLine)
			{
			y += getLineHeight(lw)
			lw = lw.next
			}
		
		return y
		}
	
	int getSubX(TextContent tc, int pos)
		{
		int xpos = 0
		int charCount = 0
		TextFrag fw = tc.frags
		while (pos > 0 && fw != null)
			{
			if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
				{
				//out.println(" - chk $(fw.value.length()) / '$(fw.value.getRaw())' pos $pos")
				if (fw.value.length() >= pos)
					{
					char mt[] = fw.value.subString(0, pos)
					//out.println(" - ret m")
					return xpos + textFont.getTextWidth(mt)
					}
					else
					{
					xpos += textFont.getTextWidth(fw.value.getRaw())
					pos -= fw.value.length()
					charCount += fw.value.length()
					}
				}
				else
				{
				//out.println(" - chk T")
				int nspaces = TAB_SPACES - (charCount % TAB_SPACES)
				xpos += (nspaces * spaceWidth)
				pos --
				charCount += nspaces
				}
			
			fw = fw.next
			}
		
		return xpos
		}
	
	Point getXY(TextLine forLine, int pos)
		{
		int y = 0
		TextContent cw = forLine.text

		//out.println("getXY @ $pos")

		int xpos = 0
		int charCount = 0
		int subline = 0

		while (pos > 0 && cw != null)
			{
			TextFrag fw = cw.frags
			xpos = 0
			//out.println("ln")
			while (pos > 0 && fw != null)
				{
				if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
					{
					//out.println(" - chk $(fw.value.length()) / '$(fw.value.getRaw())' pos $pos")
					if (fw.value.length() >= pos)
						{
						char mt[] = fw.value.subString(0, pos)
						//out.println(" - ret m [$xpos, $(textFont.getTextWidth(mt))] after '$mt'")
						return new Point(xpos + textFont.getTextWidth(mt), y + (cursorHeight * subline))
						}
						else
						{
						xpos += textFont.getTextWidth(fw.value.getRaw())
						pos -= fw.value.length()
						charCount += fw.value.length()
						}
					}
					else
					{
					//out.println(" - chk T")
					int nspaces = TAB_SPACES - (charCount % TAB_SPACES)
					xpos += (nspaces * spaceWidth)
					pos --
					charCount += nspaces
					}
				
				fw = fw.next
				}
			
			cw = cw.next
			if (cw != null) subline ++
			}
		
		//out.println(" - ret e")
		return new Point(xpos, y + (cursorHeight * subline))
		}
	
	void drawCursor(Canvas c, TextLine forLine, int pos, int ypos)
		{
		Point p = getXY(forLine, pos)

		//out.println("DC $(p.x).$(p.y) yp $ypos")
		
		c.line(new Line2D(p.x, ypos, p.x, ypos+cursorHeight, new Color(0, 0, 0, 255)))
		}
	
	void highlightLine(Canvas c, TextLine lw, int sspos, int sepos, int cy)
		{
		TextContent tc = lw.text
		int totalLen = 0
		bool inSelect = false

		if (sspos == sepos) return

		while (tc != null)
			{
			int sublen = getSublineCharLength(tc)
			int acclen = sublen + totalLen

			if (!inSelect && (sspos < acclen) && (sepos <= acclen))
				{
				//single-line highlight
				int xs = getSubX(tc, sspos - totalLen)
				int xe = getSubX(tc, sepos - totalLen)
				c.rect(new Rect2D(xs, cy, xe - xs, cursorHeight, highlightColor))
				return
				}
				else if (!inSelect && sspos < acclen)
				{
				int xs = getSubX(tc, sspos - totalLen)
				int xe = getSubX(tc, sublen)
				c.rect(new Rect2D(xs, cy, xe - xs, cursorHeight, highlightColor))
				inSelect = true
				}
				else if (sepos <= acclen)
				{
				int xs = getSubX(tc, 0)
				int xe = getSubX(tc, sepos - totalLen)
				c.rect(new Rect2D(xs, cy, xe - xs, cursorHeight, highlightColor))
				return
				}
				else if (inSelect)
				{
				int xs = getSubX(tc, 0)
				int xe = getSubX(tc, sublen)
				c.rect(new Rect2D(xs, cy, xe - xs, cursorHeight, highlightColor))
				}
			
			tc = tc.next
			cy += cursorHeight
			totalLen = acclen
			}
		}
	
	void drawSelectionBox(Canvas c)
		{
		if (selectStartLine !== cursorLine)
			{
			TextLine ssline
			TextLine seline
			int sspos
			int sepos

			if (cursorPositionY < selectStartY)
				{
				ssline = cursorLine
				seline = selectStartLine
				sspos = cursorPosition
				sepos = selectStartPos
				}
				else
				{
				ssline = selectStartLine
				seline = cursorLine
				sspos = selectStartPos
				sepos = cursorPosition
				}
			
			TextLine lw = ssline

			int cy = getY(lw)
			highlightLine(c, lw, sspos, getLineCharLength(lw), cy)
			cy += getLineHeight(lw)

			lw = lw.next

			while (lw !== seline)
				{
				highlightLine(c, lw, 0, getLineCharLength(lw), cy)
				cy += getLineHeight(lw)
				lw = lw.next
				}
			
			highlightLine(c, lw, 0, sepos, cy)
			}
			else if (selectStartPos != cursorPosition)
			{
			int startPos = 0
			int endPos = 0
			
			if (selectStartPos < cursorPosition)
				{
				startPos = selectStartPos
				endPos = cursorPosition
				}
				else
				{
				startPos = cursorPosition
				endPos = selectStartPos
				}
			
			int cy = getY(cursorLine) //NOTE: this "getY" is only needed because we don't track the cursorPositionY for start-of-line
			highlightLine(c, cursorLine, startPos, endPos, cy)
			}
		}
	
	//this function is called whenever the cursor moves; it updates (if needed) the scroll position of the text relative to the cursor
	void updateCursorPosition()
		{
		Point cp = getXY(cursorLine, cursorPosition)

		if (cp.x > (xScroll + textAreaWidth))
			{
			xScroll = (cp.x - textAreaWidth) + CURSOR_WIDTH
			scrollH.setScrollPos(xScroll)
			}
			else if (cp.x < xScroll)
			{
			xScroll = cp.x
			scrollH.setScrollPos(xScroll)
			}
		
		//test if the total text width has become less than the scroll width (i.e., there's empty space and xScroll is not 0)
		Point qcp = getXY(cursorLine, getLineCharLength(cursorLine))
		
		if ((xScroll != 0) && (qcp.x + CURSOR_WIDTH < (xScroll + textAreaWidth)))
			{
			if (qcp.x > (textAreaWidth))
				xScroll = (qcp.x - textAreaWidth) + CURSOR_WIDTH
				else
				xScroll = 0
			
			scrollH.setScrollPos(xScroll)
			}
		
		//check if the cursor is out of view
		int efy = getY(cursorLine)

		if (efy > (yScroll + textAreaHeight))
			{
			yScroll = (efy - textAreaHeight)
			scrollV.setScrollPos(yScroll)
			}
			else if ((efy-cursorHeight) < yScroll)
			{
			yScroll = (efy-cursorHeight)
			scrollV.setScrollPos(yScroll)
			}
		
		//identify the subline of the cursor, for correct cursor-key navigation
		if (wordWrap)
			{
			TextContent tc = cursorLine.text
			int count = 0
			while (tc != null)
				{
				int slc = getSublineCharLength(tc)
				if ((cursorPosition >= count) && (cursorPosition <= (count + slc)))
					{
					cursorSubline = tc
					break
					}

				count += slc
				tc = tc.next
				}
			}
		}
	
	int drawLine(Canvas c, TextLine lw, int yDraw, int viewTop, int viewBottom)
		{
		TextContent sw = lw.text

		int lineHeight = getLineHeight(lw)

		if (((yDraw + lineHeight) >= viewTop) && (yDraw <= viewBottom))
			{
			while (sw != null)
				{
				TextFrag fw = sw.frags
				int xpos = 0
				int charCount = 0
				while (fw != null)
					{
					if (fw.type == TextFrag.T_TEXT || fw.type == TextFrag.T_PARTICLE)
						{
						c.text(new Point2D(xpos, yDraw, textColor), textFont, fw.value.getRaw())
						charCount += fw.value.length()
						xpos += textFont.getTextWidth(fw.value.getRaw())
						}
						else
						{
						int nspaces = TAB_SPACES - (charCount % TAB_SPACES)
						xpos += (nspaces * spaceWidth)
						charCount += nspaces
						}
					
					fw = fw.next
					}
				
				yDraw += cursorHeight
				sw = sw.next
				}
			}
		
		return lineHeight
		}
	
	int getLineHeight(TextLine t)
		{
		return t.lineCount * cursorHeight
		}
	
	void TextArea:paint(Canvas c)
		{
		c.rect(new Rect2D(xPosition, yPosition, width, height, bgColor))

		int xOffset = 0
		if (lineNumbers)
			{
			xOffset = lineNumberAreaWidth
			}

		c.pushSurface(new Rect(xPosition + xOffset + TEXT_BORDER_H, yPosition, width - (TEXT_BORDER_H*2) - lineNumberAreaWidth, height), xScroll, yScroll, 255)
		
		c.rect(new Rect2D(xScroll, yScroll, width, height, bgColor))
		
		mutex(textLock)
			{
			drawSelectionBox(c)
			
			int yDraw = TEXT_BORDER_V
			
			TextLine lw = lines
			
			while (lw != null)
				{
				int yAdd = drawLine(c, lw, yDraw, yScroll, yScroll + height)
				
				lw = lw.next
				yDraw += yAdd
				}
			
			if (focus)
				{
				drawCursor(c, cursorLine, cursorPosition, cursorPositionY)
				}
			}
		
		c.popSurface()

		if (lineNumbers)
			{
			c.pushSurface(new Rect(xPosition + TEXT_BORDER_H, yPosition, width - (TEXT_BORDER_H*2), height), xScroll, yScroll, 255)
			
			c.rect(new Rect2D(0, yScroll, lineNumberAreaWidth, height, bgColor))

			int viewTop = yScroll
			int viewBottom = yScroll + height

			int yDraw = TEXT_BORDER_V
			
			TextLine lw = lines
			int ln = 0
			while (lw != null)
				{
				int yAdd = getLineHeight(lw)

				if (((yDraw + yAdd) >= viewTop) && (yDraw <= viewBottom))
					{
					c.text(new Point2D(0, yDraw, textColorLN), textFont, ln.makeString())
					}
					else if (yDraw > viewBottom)
					{
					break
					}
				
				lw = lw.next
				yDraw += yAdd
				ln ++
				}

			c.popSurface()
			}

		if (showScrollV)
			{
			scrollV.paint(c)
			}
		
		if (showScrollH)
			{
			scrollH.paint(c)
			}
		
		if (showScrollV && showScrollH)
			{
			//fill the square with something...
			c.rect(new Rect2D(xPosition + (width-SCR_BUTTON_WIDTH-1), yPosition + (height-SCR_BUTTON_HEIGHT-1), SCR_BUTTON_WIDTH, SCR_BUTTON_HEIGHT, bgColor))
			}

		c.rectOutline(new Rect2D(xPosition, yPosition, width, height, borderColor))
		}
	
	void unwrapLine(TextLine lw)
		{
		//reset to a single line
		while (lw.text.next != null)
			{
			if (lw.text.next.frags != null)
				{
				lw.text.fragsEnd.next = lw.text.next.frags
				lw.text.next.frags.prev = lw.text.fragsEnd
				lw.text.fragsEnd = lw.text.next.fragsEnd
				}
			
			lw.text.next.frags = null
			lw.text.next.fragsEnd = null
			lw.text.next.prev = null
			lw.text.next = lw.text.next.next
			}
		lw.textEnd = lw.text

		lineCountV -= lw.lineCount - 1

		lw.lineCount = 1
		}
	
	void unwrapLines()
		{
		TextLine lw = lines
		while (lw != null)
			{
			unwrapLine(lw)

			lw = lw.next
			}
		}
	
	void wrapLine(TextLine lw)
		{
		//reset to a single line
		unwrapLine(lw)

		Point cp = null
		int efy = 0

		//recalculate
		TextFrag fw = lw.text.frags
		int pixelLength = 0
		while (fw != null)
			{
			int test = pixelLength + textFont.getTextWidth(fw.value.getRaw())

			if ((test > textAreaWidth) && (pixelLength > 0))
				{
				//move it to a new line
				//out.println("new line W")

				TextContent ntc = new TextContent(fw, lw.textEnd.fragsEnd)
				lw.textEnd.fragsEnd = fw.prev
				lw.textEnd.fragsEnd.next = null

				lw.textEnd.next = ntc
				ntc.prev = lw.textEnd
				lw.textEnd = ntc

				pixelLength = textFont.getTextWidth(fw.value.getRaw())

				lw.lineCount ++
				}
				else
				{
				pixelLength = test
				}

			fw = fw.next
			}
		
		lineCountV += lw.lineCount - 1
		
		if (cursorLine === lw)
			{
			efy = getY(cursorLine)
			cp = getXY(cursorLine, cursorPosition)
			cursorPositionY = efy + cp.y
			}
		
		updateScrollMax()
		}
	
	void wrapLines()
		{
		//re-calculate from scratch
		TextLine lw = lines
		while (lw != null)
			{
			wrapLine(lw)

			lw = lw.next
			}
		}
	
	void TextArea:postRepaint()
		{
		emitevent repaint()
		}
	
	void TextArea:setPosition(int x, int y)
		{
		xPosition = x
		yPosition = y

		scrollV.setPosition(xPosition + (width - SCR_BUTTON_WIDTH - 1), yPosition+1)
		scrollH.setPosition(xPosition + 1, yPosition + (height - SCR_BUTTON_HEIGHT - 1))
		}
	
	Point TextArea:getPosition()
		{
		return new Point(xPosition, yPosition)
		}
	
	WH TextArea:getPreferredSize()
		{
		return new WH(width, height)
		}
	
	CursorSetEvent TextArea:mouseOver(int x, int y)
		{
		cursorType = IOWindow.CURSOR_IBEAM
		return new CursorSetEvent(IOWindow.CURSOR_IBEAM)
		}
	
	void TextArea:setFocus()
		{
		emitevent requestFocus()
		}
	
	void TextArea:setDisabled(bool d)
		{
		disabled = d
		postRepaint()
		}
	
	bool TextArea:recvFocus()
		{
		focus = true
		return true
		}
	
	HotKey[] TextArea:getHotKeys()
		{
		return hkeys
		}
	
	void TextArea:loseFocus()
		{
		focus = false
		selectStartPos = cursorPosition
		}
	
	void positionScrollH()
		{
		if (lineNumbers)
			{
			char lnMax[] = lineCount.makeString()
			int len = textFont.getTextWidth(lnMax) + LINE_NUMBER_MARGIN
			scrollH.setPosition(xPosition + len + 1, scrollH.getPosition().y)
			
			if (!showScrollV)
				scrollH.setLength(width - len - 2)
				else
				scrollH.setLength(width - len - SCR_BUTTON_HEIGHT - 2)
			}
			else
			{
			scrollH.setPosition(xPosition + 1, yPosition + (height - SCR_BUTTON_HEIGHT - 1))
				
			if (!showScrollV)
				scrollH.setLength(width - 2)
				else
				scrollH.setLength(width - SCR_BUTTON_HEIGHT - 2)
			}
		}
	
	void TextArea:setSize(int w, int h)
		{
		width = w
		height = h

		updateTextAreaWidth()

		textAreaHeight = height

		if (showScrollV)
			{
			scrollV.setPosition(xPosition + (width - SCR_BUTTON_WIDTH - 1), yPosition+1)
			
			if (!showScrollH)
				scrollV.setLength(height - 2)
				else
				scrollV.setLength(height - SCR_BUTTON_HEIGHT - 2)
			}
		
		if (showScrollH)
			{
			textAreaHeight -= SCR_BUTTON_WIDTH

			positionScrollH()
			}
		
		if (wordWrap) wrapLines()
		
		updateScrollMax()
		postRepaint()
		}
	
	void TextArea:setCursorPos(int p)
		{
		if (p <= getLineCharLength(cursorLine))
			{
			cursorPosition = p
			selectStartPos = cursorPosition
			updateCursorPosition()
			postRepaint()
			}
			else
			{
			throw new Exception("index out of bounds")
			}
		}
	
	int TextArea:getCursorPos()
		{
		return cursorPosition
		}
	
	void TextArea:setWordWrap(bool v)
		{
		wordWrap = v

		if (wordWrap)
			{
			wrapLines()
			showScrollH = false
			}
			else
			{
			unwrapLines()
			showScrollH = true
			}

		postRepaint()
		}
	
	int getNumberAreaWidth(int largestNumber)
		{
		char lnMax[] = largestNumber.makeString()
		return textFont.getTextWidth(lnMax) + LINE_NUMBER_MARGIN
		}
	
	void updateTextAreaWidth()
		{
		int newWidth = width-(TEXT_BORDER_H*2)

		if (showScrollV)
			{
			newWidth -= SCR_BUTTON_WIDTH
			}
		
		newWidth -= lineNumberAreaWidth

		textAreaWidth = newWidth
		}
	
	void TextArea:setLineNumbers(bool v)
		{
		lineNumbers = v

		lineNumberAreaWidth = getNumberAreaWidth(lineCount)
		updateTextAreaWidth()

		if (wordWrap) wrapLines()

		positionScrollH()

		postRepaint()
		}
	
	void TextArea:setAutoIndent(bool v)
		{
		autoIndent = v
		}
	
	void Destructor:destroy()
		{
		clearText()
		}
	
	}
Revision history
To propose a new revision to this entity, use dana source put -uc your/new/version.dn -n ui.TextArea -m "reason for update" -u yourUsername
Version 2 (this version) by barry
Notes for this version: Corrections to prepare for upcoming compiler strictness changes in function parameter qualifier equivalence
Version 1 by barry