Margins and Long Lines

Design & Implementation of a Win32 Text Editor

This will probably be quite a short tutorial as the subject of margins is really quite simple to implement. This time around we will look at implementing a selection margin (complete with full-line selection), line numbers, line-indicator icons (e.g. like the breakpoint bitmaps in Visual Studio), and lastly the problem of long-line display.

Drawing a margin

The main screenshot shows Neatpad with a selection-margin, line-numbers, bitmapped bookmarks and long-line highlights. Although it looks quite complicated drawing a margin is really easy, as long as you understand that it is just a rectangular area to the left of the display that is drawn and scrolled differently.

I've highlighted the picture above to illustrate that even though we've added a vertical margin, lines are still drawn horizontally one-by-one. However the line-drawing process (the TextView::PaintLine function) has changed to allow for the margins.

First of all the margin is drawn using the new function TextView::PaintMargin

int TextView::PaintMargin(HDC hdc, ULONG nLineNo, RECT *margin)

The area taken by the margin is then clipped to prevent anything drawing over it. Next, the text is drawn as normal - but offset to the right of the margin. The clipping is important because it ensures that when we scroll horizontally the lines of text don't overdraw the margin and instead "disappear" behind the margin. Basically the process looks like this:

void TextView::PaintLine(HDC hdc, ULONG nLineNo)
{
    RECT rect; 

    // work out where to draw the line// handle the margins
    if(LeftMarginWidth() > 0)
    {
        RECT margin;    // work out the margin coordinates

        // paint the margin
        PaintMargin(hdc, nLineNo, &margin);

        // clip the margin so the text doesn't draw over it
        ExcludeClipRect(hdc, margin.left, margin.top, margin.right, margin.bottom);

        // offset the text placement
        rect.left += LeftMarginWidth();
    }

    // paint the text as normal
    PaintText(hdc, nLineNo, &rect);	
}

The key point to understand is that the normal line-text must be offset to the right to take into account the margin. It's really very simple so just take a look at the sourcecode download to see it in action.

Two more important things to mention: I have also had to modify the scrolling routine so that the margin is not scrolled when the main text is scrolled. This was very simple as the clipping rectangle we specify in ScrollWindowEx is simply adjusted so that the margin does not get included.

HRGN TextView::ScrollRgn(int dx, int dy, bool fReturnUpdateRgn)
{
    ...

    // take margin into account
    clip.left += LeftMarginWidth();
    
    ...
}

The other modification was the mouse-input (selection) code. The cursor-position must be adjusted so that the margin is again taken into account. I really don't want to include any code for this as you should be getting the idea by now. Basically, any x-coordinate is shifted to the right by the size of the margin before any input or drawing takes place.

Line bitmaps

Just a quick word about the line-indicator bitmaps you can see. I've added a new ImageList member variable to the TextView class which can hold a user-specified collection of bitmaps:

HIMAGELIST m_hImageList;

The image-list can be set using the new TextView TXM_SETIMAGELIST message (or the TextView_SetImageList macro).

HIMAGELIST hImageList = ImageList_LoadImage(...);

TextView_SetImageList(hwndTextView, hImageList);

Using an image-list is the simplest way to work with images as it manages the bitmap memory, and also provides drawing support as well - so storing/drawing bitmaps is really easy.

The difficult part comes when we draw each line. How do we know what image to place in the margin-area? For a text-editors which use an array or linked-list of lines this is really easy - because additional information can be easily stored for each line entry in the editor. However for our Neatpad design this is not possible because we have to support large-file editing at some point - and having a line-buffer for a 4gb file would not be a good idea.

For this reason I have used a separate array which holds only those lines that have bitmaps associated with them. The array is always sorted so that we can use a binary-search to quickly determine if a specific line has any bitmap assocated with it.

typedef struct
{
    ULONG nLineNo;
    ULONG nImageIdx;

} LINEINFO;LINEINFO m_LineInfo[MAX_LINE_INFO];

Each time a line is drawn the LINEINFO array is searched using the TextView::GetLineInfo function:

LINEINFO* TextView::GetLineInfo(ULONG nLineNo)
{
    LINEINFO key = { nLineNo, 0 };

    // perform the binary search
    return (LINEINFO *) bsearch(
                          &key, 
                          m_LineInfo,
                          m_nLineInfoCount,
                          sizeof(LINEINFO),
                          (COMPAREPROC)CompareLineInfo
                       );
}

This function returns a pointer to the appropriate LINEINFO structure if successful, or NULL if there is no stored information for the specified line.

I anticipate that more information could be stored about lines such as a whole-line highlight colour, bookmarks, annotations etc. However for now I have just included support for an image-index. The images for each line can be set using another new TextView message, TXM_SETLINEIMAGE:

TextView_SetLineImage(hwndTextView, nLineNo, nImageIdx);

Line-selection and mouse input

Now that we have a margin to the left of the main text display we can use this area to initiate line-based selections.

To allow for this line-based selection method, we need to keep track of more than just the fact that we are making a selection. The previous boolean m_fSelection (which was just used to indicate if a selection was in progress or not) has been replaced by a new variable:

 SELMODE m_nSelectionMode;

m_nSelectionMode is a SELMODE enumeration with the following values:

enum SELMODE 
{ 
   SELMODE_NONE, 
   SELMODE_NORMAL, 
   SELMODE_MARGIN 
};

So at the moment we support two types of selection - "normal" and "margin". At some point in the future this could be extended to support other types of selection such as column and block selections. Of course the mouse-routines have been modified to understand the new types of selection.

Highlighting long lines

One feature which I find particularly useful is the highlighting of long lines. I can still remember the Borland C++ 4.0 IDE for Windows 3.1 which featured a single vertical-line margin to indicate where column-80 was. I actually found this a little tacky but I wanted to create a similar effect.

The reason for highlighting longs lines is provide a way to indicate to the user when a line of text becomes too long. This is most useful for programmers who don't want their text to "wrap" when it is printed out - so the aim is to keep all lines of text under a certain limit.

There are basically two ways of implementing long-line highlights:

  • Use a two-stage approach. The first stage prepares the background using two colours to distinguish "normal" text on the left and "clipped" text on the right. i.e. A fixed-sized area is filled using the normal background colour, up to (say) column-position 80. The background after this is either filled using a different shade, or a vertical line is drawn to indicate the long-line limit.

    The text is then drawn over the top of this background in transparent mode - so the background shows through. This is really a "pixel-based" approach because the text drawing is independent of the long-line display.

  • The alternative method is to simply change the background-colour of any character that is drawn past column 80. This is what the Scintilla text-editor component does and is alot simpler than the method above - so this is what I've implemented inside Neatpad.

In order to support this new long-line display I've had to extend the TextView::ApplyTextAttributes member-function with a new parameter - &nColumn :

int TextView::ApplyTextAttributes(ULONG nLineNo, ULONG nOffset, ULONG &nColumn, TCHAR *szText, int nTextLen, ATTR *attr)

Notice that nColumn is a C++ reference. It is continually updated by ApplyTextAttributes to keep track of the current column-position (we need a C++ reference so that the value is preserved through successive calls). Once nColumn reaches a certain value, ApplyTextAttributes will use a different default background colour for the text.

A new message (TXM_SETLONGLINE) has been added to the TextView to allow the long-line limit to be programmatically altered. The TextView_SetLongLine can be used to set this value:

TextView_SetLongLine(hwndTextView, 80);

64bit support

This probably a little late in the day (it should have happened from day#1) but I've started to make the Neatpad and TextView projects 64bit compatible. You will therefore need to use a recent Platform SDK when compiling Neatpad in order to get the new mixed 32bit/64bit definitions. This has also resulted so far in the following change:

  • Get/SetWindowLong has changed to Get/SetWindowLongPtr

To test that these changes actually worked I compiled the project using the Microsoft 64bit compiler, targetted for the IA64 architecture. Follow the steps below to duplicate this:

  • Export the Neatpad project as a makefile:Project -> Export Makefile
  • Open the Platform SDK 64bit build environment from the start-menu:Microsoft Platform SDK -> Windows XP 64bit Build Environment
  • Change to the Neatpad project directory and open Neatpad.mak\Neatpad07\Neatpad
  • Modify the LINK32_FLAGS so that both the /machine switches reads "IA64" instead of "I386", and remove any reference to ODBC32.LIB and ODBCCP32.LIB
  • Make sure that the Neatpad project has been "cleaned" then build using the 64bit tools:nmake Neatpad.mak

There are still alot of changes to make (ULONG vs ULONG64 issues) and as I don't have access to a 64bit machine currently I won't be able to make any more modifications until I can test properly. However I hope to have Neatpad fully 64bit compatible before the end of the series!

Conclusion

This tutorial was a little off-track as margins and long-line highlights are not really that important for a text-editor's design. Anyhow I wanted to get it out of the way so I could concentrate on the core functionality.

Coming up in Part 8 will be support for UTF-8 and Unicode!