Adventures in TextInputCanvas : 7

I have been asked why I’m writing all this code in API 1
Primarily because I dont find anything in API worth moving up to - yet. I’ve got client projects in older versions and so far there hasnt been a compelling reason to move those up. Yet.
The other thing is that using API 1 makes it so anyone, including those using older versions, can follow along. Using API 2 would limit the audience that can use this.

So I’m going to carry on this way for now.

Onwards !!!

Lets get a couple trivial housing keeping things out of the way.
There are some events that, if you’re writing a code editor, probably dont make any sense to do much with.
For instance, in the Xojo IDE the code is ALL rendered in the same font and at the same point size.
I cant say i’ve used a code editor where they did more than use color. Certainly not a different font or even a different font size as there is research that suggests that altering fonts and font sizes actually slows reading.
So while we actually expose properties for the font size and font name that can be set by other code we arent returning that for the FontNameAtLocation or the FontSizeAtLocation events.
For THIS use case we’ll just make those two events return the setting those properties hold.

Function FontSizeAtLocation(location as integer) Handles FontSizeAtLocation as integer
  dbglog currentmethodname
  
  Return Self.TextSize
  
End Function
Function FontNameAtLocation(location as integer) Handles FontNameAtLocation as string
  dbglog currentmethodname
  
  return self.TextFont
End Function

Now thats out of the way we move on.

So since we last visited and added the insertion point tracking lets handle some more kind of simple things. This will be more to get the hang of how stuff works than really to write “production ready code” today. I expect to have to revise things along the way as we learn more about the TIC.

Lets start with a few of the easier movement command as that will also force us to rethink how we do text insertion. Once we can move the insertion point around just appending to the end of our text buffer wont be adequate any longer.

The commands CmdMoveForward and CmdMoveRight will, in our implementation behave the same. Note however that IF you were supporting both left to right and right to left writing systems they might not as “forward” might mean moving more to the left, not to the right.
The same is true of CmdMoveLeft and CmdMoveBackward
Move Left is triggered by pressing the right arrow key and Move Right by pressing the right arrow key. Obviously. In the default macOS key bindings Move Forward is triggered by pressing Ctrl-F. And move backward by pressing Ctrl-B.

For this implementation they can be handled the same - but in others they might need to be different.

Again unfortunately Apple’s docs really dont cover any detail. When we get to supporting selections we’ll have to review what these moves mean when there is a selection active.

Moving “backward” or “left” is quite simple. Lets move the insertion one position backwards. And we’ll do that by just decrementing the property, mInsertPosition, by one. And lets limit is lowest possible value to be 0. There is no point making it more negative once we get to 0. So I’ve added to DoCommand the following

Case CmdMoveBackward
  mInsertionPosition = Max(mInsertionPosition - 1, 0)
Case CmdMoveLeft
  mInsertionPosition = Max(mInsertionPosition - 1, 0)

This lets the insertion point move, but once its at the beginning it wont move any further back.

Moving forward is similar, but we should limit how far forward you can move to just after the end of the buffer.
To do this I’ve added these two cases to DoCommand

Case CmdMoveForward
  mInsertionPosition = Min(mInsertionPosition + 1, Len(mTextBuffer))
Case CmdMoveRight
  mInsertionPosition = Min(mInsertionPosition + 1, Len(mTextBuffer))

So now we can put in some debug logs in these cases if you want to verify that the insertion point IS indeed being altered.

And now we will need to revisit our paint event because since the insertion point can be moved we’ll need to make sure it gets drawn in the right position.
First we’ll need to figure out what line # a specific position is on - and there are some edge cases to deal with here. And while we’re at it we might as well figure out what character position on the line this is.

We’re going to map a simple integer, mInsertionPosition, to a specific line # and column number.
And then to draw the insertion point we’ll need to turn a give line # and column position into x , x coordinates.

So in the Paint event where we previously had

// IF the cursor should be visible then draw it - otherwise dont and the clear above will have done the right thing
If mCursorVisible Then
  g.DrawLine drawAtX, drawAtY - g.TextAscent, drawAtX, drawAtY - g.TextAscent + g.TextHeight
End If

we’ll want to change that to something like

// IF the cursor should be visible then draw it - otherwise dont and the clear above will have done the right thing
If mCursorVisible Then
 Dim position As REALbasic.Point = PositionToLineAndColumn(mInsertionPosition)
 Dim drawPosition As REAlbasic.point = LineColumnToXY(position.Y,position.X)
 drawAtX = drawPosition.X
 drawAtY = drawPosition.Y
 g.DrawLine drawAtX, drawAtY - g.TextAscent, drawAtX, drawAtY - g.TextAscent + g.TextHeight
End If

PositionToLineAndColumn will take a simple position and give us back two values as a Point (Y will be line and X will be the column)
LineColumnToXY will take a line and column and return two values for X and Y. X will be the offset from the left edge and Y will be the TEXT BASELINE position.

We’ll need to define those two methods

Protected Function PositionToLineAndColumn(position as integer) as REALbasic.Point
End Function
Protected Function LineColumnToXY(g as Graphics, lineNumber as integer, column as Integer) as REALbasic.Point
End Function

The blindingly lazy and obvious (and really slow) way to implement PositionToLineAndColumn is to do something really simple like :

Protected Function PositionToLineAndColumn(position as integer) as REALbasic.Point

	Dim currPos As Integer
	Dim line As Integer
	Dim col As Integer

	While currPos <= position
  
	  If mTextBuffer.Mid(currPos,1) = EndOfLine Then
		line = line + 1
		col = 0
	  Else
		col = col + 1
	  End If
  
	  currPos = currPos + 1
  
	Wend

	Return New realbasic.point( line, col )
End Function

The problem with this mechanism is that the larger our text buffer gets the slower this is going to get.
There is LOTS of room for improvement - we can cover these things at the end. The point of this series is to illustrate how to implement TIC - not how to implement it optimally or for all cases.

So lets stick with this for now and we can improve it later on.

Basically we just count lines and columns until we get to the position where the passed position is and return those.

Because LineColumnToXY needs to have the TextAscent, TextHeight and Stringwidth from the current graphics being drawn to to get the right results as when you draw we pass the graphics in.
In here we can compute the baseline for any line since the first line (line 0) has the baseline at the "TextAscent position) and every line after that is offset from the previous by the text height.
That makes it easy to compute the baseline, or Y position, (assuming you’re NOT using multiple different fonts text heights etc and then you will nee to do something different)
The X position is tricker becuase even if things are in a single font that could be a proportional typeface. So you HAVE to use the correct portion of the string and measure that.
So we have to have access to the lines, and be able to get one portion of a line.
Once we have that the position is a simple call to stringwidth.

Protected Function LineColumnToXY(g as Graphics, lineNumber as integer, column as Integer) as REALbasic.Point
  Dim xPos As Double
  Dim yPos As Double
  
  // not this is not EFFICIENT
  // since we split things into lines in PAINT and again here
  // if we really need lines we should figure out how to do this as few times as possible
  
  Dim lines() As String = Split( ReplaceLineEndings(mTextBuffer, EndOfLine), EndOfLine )
  
  Dim X As Double
  Dim Y As Double
  
  Y = g.TextAscent // line 0
  y = y + (g.TextHeight * lineNumber)

  if lineNumber >= 0 And lineNumber <= lines.ubound Then
	  X = g.StringWidth( Left(lines(lineNumber), column ) )
  End If

  Return New REALbasic.Point(x, y)
End Function

And now as you move the insertion point the cursor should move around and redraw blinking in the right position.
We’re still not handling insertions AT the insertion point though.

[its been a long day but I wanted to post this - watch for an edit to come to this article]

4 Likes