Secrets of the TextInputCanvas

The TextInputCanvas, after a self-study and @npalardy’s helpful series, is far less “mysterious” than some might think. However, there are some TIC events that are not readily explained and it is hard to figure out when they are called by the operating system. Here is an overview of the “mysterious” events:

BaselineAtIndex() As Integer
CharacterAtPoint(x As Integer, y As Integer) As Integer
IncompleteTextRange() As TextRange
SelectedRange() As TextRange
SetIncompleteText(text As String, replacementRange As TextRange, relativeSelection As TextRange)
TextForRange(range As TextRange) As String
TextLength() As Integer

' From the title it is clear what needs to be returned,
' the only question is when is this event triggered by
' the OS? What is it good for? Why should I implement it?
' Especially if you as a developer have to take care of
' programming an editor yourself anyway. Then I can/must
' ever create such methods on my own anyway, since you
' can't call the events.

Or: For example, how to make macOS call this Popover? It appears when you apply a “firm, longer” pressure with the mouse/trackpad on the word.

Maybe someone can provide a little more clarity.

Many of the events are a requirement of the textInputclient protocol for Coco text handling
Some of thee you may never encounter - I know there are some I havent

Selected Range, IncompleteTextRange and RectFor Range are used wen you press & hold a key that has several forms (press & hold down the U and you get

Screen Shot 2021-04-21 at 8.59.07 PM

Those events are the OS asking you for information it needs to know where to pop up that little panel

A Text range is basically a start (location, and a length) So in a text editor you have to treat the text as if its one giant long string and return the starting position and length of any selected text

FontNameAtLocation and FontSizeAtLocation are required only because a text input client MUST implement these to be a proper text input client
I cant say I’ve seen them called - but if you can determine the correct font name and font size for the specific location then return the right value

Baseline at index literally needs to know the baseline offset for text at the position indicated
Its usually relative to a bounding box that includes the selected range
In a document that uses but a single typeface then you can return the same textAscent (which is as close as you can get to the baseline in Xojo) Where you have multiple fonts you will need to know th font & size to return the right Text Ascent

TextLength is the total length of the text in characters (something slightly more fine grained that that actually but Xojo really cant access it so “characters” has to suffice)

CharacterAtPoint(x As Integer, y As Integer) As Integer is a tad badly named
Given an x,y coordinate in the control what character INDEX is at that position

DiscardIncompleteText() and SetIncompleteText are used as part of Cocoas text handling
I’ve not seen them used but that doesnt mean they NEVER are
SetIncompletetext can be if there is a reasn for the os to use the marked text (see Apple Developer Documentation ) DiscardIncomplete is Apple Developer Documentation

Oh and I’m not sure that dialog you showed is pat ot “text input client”
unless thats what the services menu has turned into on BS ? Not sure about that one


Thank you for the detailed post @npalardy. Now it’s getting interesting:

The RectForRange event fires at my code as your described sample “press & hold down the U and you get”. SelectedRange and IncompleteTextRange firing every time I press a key, also “normal” keys without “several forms”. Therefore, it is not clear to me why it needs these two events.

In my project, the InsertText and RectForRange events are sufficient to calculate the position of the panel. I do not need the other two events.

Aha, that means that these events have no direct influence on my control.

BaselineAtIndex, TextLength, FontNameAtLocation, FontSizeAtLocation, CharacterAtPoint - I understand the benefit just from naming the events. However, I do not understand why the OS needs these events, i.e. when are they queried and for what purpose?

Because they are REQUIRED when you implement a text input client
You’d have to read Apple’s documentation to know why it requires them

EDIT : FWIW Apples docs on this SUCK by the way

This has nothing to do with YOUR needs
Its the OS calling your code that needs this information

This is the OS asking you for information

You’d have to read Apple’s documentation to know why it requires them
NSTextInput, NSTextInputClient and there may be another
The Text input system is VERY complex


We’ve been using the TextInputCanvas to implement a text editor for the past few years. Its been a while since I looked at this code but here is some info that might help:

• TextRanges
I think these are 0 based.

• Incomplete Text Events
These are related to inline input such as CJK text entry. I think they may also be triggered for long key presses when the character can have an accent. We track when inline input starts and ends also also track which characters in our buffer are related to inline input.

• BaselineAtIndex
We just return 0 here.

• DiscardIncompleteText
We use this to remove text from our buffer which we flagged as belonging to inline input.

• DoCommand
We use this to handle commands such as:
a) CmdMoveLeft, CmdMoveLeftAndModifySelection, CmdMoveRight, CmdMoveRightAndModifySelection, CmdMoveUp, CmdMoveUpAndModifySelection, CmdMoveDown, CmdMoveDownAndModifySelection, CmdMoveToBeginningOfDocument, CmdMoveToBeginningOfDocumentAndModifySelection, CmdScrollToBeginningOfDocument, CmdMoveToEndOfDocument, CmdMoveToEndOfDocumentAndModifySelection, CmdScrollToEndOfDocument, CmdScrollPageUp, CmdScrollPageDown to move our internal cursor position.
b) CmdDeleteBackward & CmdDeleteForward to delete text.
c) CmdInsertNewline, CmdInsertLineBreak to insert a line break.

• FontNameAtLocation
We just return the string System.

• FontSizeAtLocation
We just return 0.

• GotFocus
We use this event to notify our text editor that it has received focus. We store the current focus state as a property in the class.

• IncompleteTextRange
We return a TextRange which indicates the start & length of the characters in our buffer we have flagged as inline input.

• InsertText
We use this to insert the supplied text into our text buffer. If the range parameter is supplied then we use that to replace the text at that range.

• IsEditable
We return True / False depending on if we think we have focus or not.

• KeyDown
We use this to process other key events.

• KeyFallsThrough
We just return False.

• LostFocus
We use this event to notify our text editor that it has lost focus.

• MouseDown
We use this to handle mouse down events which could invoke a context menu, the start of a drag to make a selection or change the current cursor position. We also have code that counts the clicks to handle double and triple clicking.

• MouseDrag
We use this to handle dragging a selection.

• MouseUp
We use this to handle the end of a drag.

• MouseWheel
We pass this event up to the parent so that it can handle scrolling.

• RectForRange
We return a REALbasic.Rect that has the Left & Top properties set to the x & y pixel position of the first character we have flagged as being inline. I think these have to be global (ie: including Window.Left & Window.Top).

• SelectedRange
We return a TextRange that describes the cursor position or selection start / length.

• SetIncompleteText
We use this to add text to our buffer and flag it as being inline. If replacementRange is set we use it to replace existing text. We ignore relativeSelection.

• TextForRange
We return the characters in our text buffer for the specified range.

• TextLength
We return the number of characters in our text buffer.

1 Like

Ah yeah that rings a bell
not something I have a bunch of experience with

So, thanks to those involved for pointers up to this point.

I once created a Quick’n Dirty example that implements the Basic events. Unfortunately it is still unclear to me when exactly these events fire: DiscardIncompleteText, IncompleteTextRange, SetIncompleteText. Yes, I can set a break point and then the app jumps to the debugger as well. I’m trying to understand the logic behind this, that’s the point. If I press longer on, for example, the letter “O”, this “accent” popup menu appears. Unfortunately, the “O” is always inserted first. I’m also interested in how you integrated the CJK support.

I would be happy if you @s7g2vp2 and @npalardy take a look at the example project and add suggestions (CJK etc.) and fixes, as a community project. The point here is not to make the example project a full-fledged editor, I am concerned with the proper handling of the events implemented in the project.

BTW: What exactly do you mean when you talk about “inline input”?

Thanks a lot.

I think you’d have to switch your input mechanism to one of the Chine Japanese or Korean ones
Then when you “type” you may actually be composing a single glyph through the use of several keystrokes. At that point the input text may be “incomplete” and these events would be used

By implementing these events - thats the point
If you implement these events then you are a compliant text input client and can handle whatever input mechanism is selected

1 Like

New version of the project here which should now work for long key presses & CJK input. I also implemented cursor left, cursor right & backspace commands. NOTE. This has been changed to use API v1 commands as I don’t do API v2 (yet).

Inline Input is a term Apple used in their Carbon Text Services Manager API and meant applications could accept CJK input directly into their application. If the application wasn’t text services aware then the operating system would proxy all input through a floating window and only passed it to the application when the user confirmed it. NOTE. This is not the same as the input method editor candidate floating window which you still get even if the application has implemented the correct events.