ADC Membership Technical Business Join ADC
Search Advanced Search
Technical Note TN1196
Cursor Components

CONTENTS

Opening the Cursor
Setting the Cursor
Closing the Cursor
Modifying the Cursor
Miscellaneous

This Technote describes the building and use of cursor components. Cursor components allow you to build custom color cursors completely under your control. Unlike previous color cursors on the Macintosh, these components allow much more flexibility.

Cursors created utilizing this component are pretty unlimited. They could be extra large, thousands or millions of colors, transparent, situationally intelligent, and/or animated. The design is constrained only by your imagination.

Cursor components are built in two parts. The main part is the component itself, which can be built by expanding on the sample code sections provided in this Technote. The second part is the application that instantiates and controls the cursor. The second half of this technote will provide guidance on ways to do this. The last section of this technote will address cursor flicker and present a solution to the problem.

 Updated: [Apr 17 2000]






Prerequisites and Resources

The Cursor Component SDK provides a sample application from which most of this Technotes code samples are taken. You should study this application and its cursor components carefully. These are very good starting points.

When building an application, there will be two parts (at least): the application itself and the cursor component(s). The simplest way to put together the components is as code resources. To this end, the SDK sample projects provide a good starting point. (Duplicating the cursor component projects and working from there would seem to be the simplest approach.) The application is much simpler. A short study of the cursor code shows that it is very portable, and can be worked into almost any existing application.

Developers will need Universal Interfaces 3.3 to develop cursor components. This is due to new functionality that has been added to QuickDraw headers and interfaces to support cursor components.

One last note: cursor components are available only with Mac OS 9.0 and later; therefore, you should always have a fallback if you plan deployment on systems prior to Mac OS 9.0. Additionally, currently cursor components are not supported under Mac OS X and Carbon. This may change in the future, and this Note will be updated as necessary.

Back to top


Limitations

Currently, on Mac OS 9, Cursor Components have a few limitations. Due to ATI RAGE 128 video hardware architecture the cursor can exhibit flicker. This flicker varies with different monitor settings and from system to system. See the Fighting Flicker for more information of preventing this problem.

Additionally, Cursor Components do not function with PowerBooks due to adverse interactions with the "mouse trails" feature. There is no workaround, although this may be fixed in the future.

Finally, all cursor drawing and erasing happens at interrupt time. (This will be discussed later in this Technote.) Thus, you should use particular care in designing the drawing and erasing portion of the component, relying only on interrupt-safe functions for these sections. The Interrupt-Safe Routines Technote is a good source for this information.

Back to top


Building Components

The component is built as a code resource. The main parts are the actual cursor code file and the resource data. In most cases, image data is stored in the resource, while behavior is defined by the code file.

The cursor component API consists two basic sections. The first section, the component manager, can be adopted straight from the code below and provides one main and four basic functions. This section handles the main function, dispatching, opening and closing the component, and responding to version information requests. The code provided will suit most needs. Be sure to look at the allocation of the CursorGlobalsRecord. It should be adapted to the specific needs of your cursor component. Normally, you can keep the standard return of TRUE for all the cases in the ComponentCanDo functions, but if you do not support all cases you should return FALSE for those that you do not. An example implementation of the component manager section is shown below. As stated previously, this section is fairly straightforward and the code below can be utilized with very little modification.



struct CursorGlobalsRecord
{
    ComponentInstance       self;
    Rect                    bounds;
    Point                   hotSpot;
    UInt32                  animIndex;
    UInt32                  deviceCount;
    CursorDeviceRecord      deviceList[12]; // fixed 12 displays max
};

typedef struct CursorGlobalsRecord      CursorGlobalsRecord;
typedef CursorGlobalsRecord *           CursorGlobalsPtr;
typedef CursorGlobalsPtr *              CursorGlobalsHnd;

// -------------------------------------------------------------------

// Routine descriptors
RoutineDescriptor   ComponentCanDoRD =
    BUILD_ROUTINE_DESCRIPTOR (uppCallComponentCanDoProcInfo,
                              (ProcPtr)ComponentCanDo);
RoutineDescriptor   ComponentGetVersionRD =
    BUILD_ROUTINE_DESCRIPTOR (uppCallComponentVersionProcInfo,
                              (ProcPtr)ComponentGetVersion);
// Describe the main entry
RoutineDescriptor   mainRD =
    BUILD_ROUTINE_DESCRIPTOR (uppComponentRoutineProcInfo, main);

pascal ComponentResult
main
(ComponentParameters *params,char **storage)
{
    ComponentResult result = noErr;

    // If the selector is less than zero, it's a Component manager selector.
    if (params->what < 0)
    {
        switch (params->what)
        {
            case kComponentOpenSelect:
                return CallWithStoragePI(storage,
                                         params,
                                         ComponentOpen,
                                         uppCallComponentOpenProcInfo);

            case kComponentCloseSelect:
                return CallWithStoragePI(storage,
                                         params,
                                         ComponentClose,
                                         uppCallComponentCloseProcInfo);

            case kComponentCanDoSelect:
                return CallComponentFunction(params,
                                             &ComponentCanDoRD);

            case kComponentVersionSelect:
                return CallComponentFunction(params,
                                             &ComponentGetVersionRD);

            default:
                return noErr;
        }
    }

    switch (params->what)
    {
        case kCursorComponentInit:
            return CallWithStoragePI(storage,
                                     params,
                                     CursorInitialize,
                                     uppCursorInitProcInfo);

        case kCursorComponentGetInfo:
            return CallWithStoragePI(storage,
                                     params,
                                     GetCursorInfo,
                                     uppGetCursorInfoProcInfo);

        case kCursorComponentSetData:
            return CallWithStoragePI(storage,
                                     params,
                                     SetCursorData,
                                     uppSetCursorDataProcInfo);

        case kCursorComponentReconfigure:
            return CallWithStoragePI(storage,
                                     params,
                                     CursorReconfigure,
                                     uppCursorReconfigureProcInfo);

        case kCursorComponentDraw:
            return CallWithStoragePI(storage,
                                     params,
                                     CursorDraw,
                                     uppCursorDrawProcInfo);

        case kCursorComponentErase:
            return CallWithStoragePI(storage,
                                     params,
                                     CursorErase,
                                     uppCursorEraseProcInfo);

        case kCursorComponentMove:
            return CallWithStoragePI(storage,
                                     params,
                                     CursorMove,
                                     uppCursorMoveProcInfo);

        case kCursorComponentAnimate:
            return CallWithStoragePI(storage,
                                     params,
                                     CursorAnimate,
                                                uppCursorAnimateProcInfo);

        default:
            result = unimpErr;
            break;
    }

    return result;

}

pascal ComponentResult
ComponentOpen
(CursorGlobalsHnd storage, ComponentInstance self)
{
#pragma unused (storage)
    ComponentResult result = noErr;

    storage =
        (CursorGlobalsHnd)NewHandleClear(sizeof(CursorGlobalsRecord));
    if (storage != nil )
    {
        SetComponentInstanceStorage(self, (Handle)storage);

        // keep an instance of ourself
        (*storage)->self = self;

    }
    else
        result = MemError();

    return result;
}

pascal ComponentResult
ComponentClose
(CursorGlobalsHnd storage, ComponentInstance self)
{
    if (storage != nil)
    {
        DisposeDeviceRecords(storage);
        DisposeHandle((Handle)storage);
    }

    return(noErr);
}

pascal ComponentResult
ComponentCanDo
(short selector)
{
    switch (selector)
    {
        case kComponentOpenSelect:
        case kComponentCloseSelect:
        case kComponentCanDoSelect:
        case kComponentVersionSelect:
        case kCursorComponentInit:
        case kCursorComponentGetInfo:
        case kCursorComponentSetData:
        case kCursorComponentReconfigure:
        case kCursorComponentDraw:
        case kCursorComponentErase:
        case kCursorComponentMove:
            return true;

        default:
            return false;
    }
}

/* Version information */
#define CURSOR_COMPONENT_REV       1
#define kCursorComponentVersion    1

pascal ComponentResult
ComponentGetVersion
(void)
{
    // Cursor Component interface version in hi word,
    // code rev in lo word
    return ((kCursorComponentVersion << 16) | CURSOR_COMPONENT_REV);
}


The second section of the component is the cursor component functions. This consists of eight basic functions that handle the main processing, drawing and erasing of your cursor. We will cover each functional area in detail, but you are advised to look closely at the examples provided for basic structure and functionality.

CursorInitialize

This function should create a cursor image, mask and save under buffers, and other info for each device. It will be called once when the component is opened and again when display devices are reconfigured or removed. The function CreateDeviceRecords, included in the sample and shown below, shows an example of handling the tasks of creating the image, mask and save under buffers for each device. This is important since cursors can span multiple devices, and devices can have different color depths resulting in different buffer pixel formats. If the buffers are successfully allocated, CreateDeviceRecords calls back to SetCursorData to set up the new cursors. Notice also the call to InitializeCursorFlush. This is designed to correct a flicker condition that may occur in some system configurations. See the Fighting Flicker for more information. Below is an example of a basic implementation of CursorInitialize and CreateDeviceRecords.



pascal ComponentResult
CursorInitialize
(CursorGlobalsHnd storage)
{
    ComponentResult result;
    Rect            theRect = {0, 0, 16, 16};
    Point           hotSpot = {0, 0};

    if (storage != nil)
    {
        result = InitializeCursorFlush();
        if (result == noErr)
        {

            (*storage)->bounds          = theRect;
            (*storage)->hotSpot         = hotSpot;
            (*storage)->deviceCount     = 0;
            result = CreateDeviceRecords(storage);
        }
    }
    return result;
}

static OSErr
CreateDeviceRecords
(CursorGlobalsHnd cursorGlobals)
{
    OSErr               err;
    GDHandle            theDevice;
    GWorldPtr           theWorld;
    PrivateCursorData   data;
    UInt32              counter;
    SInt16              depth;
    Rect                bounds;
    short               index;

    bounds = (*cursorGlobals)->bounds;

    // start with the first device
    theDevice = DMGetFirstScreenDevice(true);
    while (theDevice != nil)
    {
        counter = (*cursorGlobals)->deviceCount;
        depth = (*(*theDevice)->gdPMap)->pixelSize;

        (*cursorGlobals)->deviceList[counter].device = nil;
        (*cursorGlobals)->deviceList[counter].saveUnder = nil;

        for (index = 0; index < kNumberOfAnimations; index++)
        {
            (*cursorGlobals)->deviceList[counter].cursorImage[index] = nil;
            // create GWorlds to hold images of cursor expanded to the bitdepth
            // of this device

            err = CreateImageWorld(&theWorld, depth, &bounds, theDevice);
            if (err == noErr)
               (*cursorGlobals)->deviceList[counter].cursorImage[index]=
                    theWorld;
        }

        if (err == noErr)
        {
            // create a GWorld to hold mask
            err = CreateImageWorld(&theWorld, depth, &bounds, theDevice);
            if (err == noErr && theWorld != nil)
            {
                (*cursorGlobals)->deviceList[counter].maskImage = theWorld;

                // create a GWorld to hold saved screen image
                err = CreateImageWorld(&theWorld, depth, &bounds, theDevice);
                if (err == noErr && theWorld != nil)
                {
                    (*cursorGlobals)->deviceList[counter].saveUnder = theWorld;
                    (*cursorGlobals)->deviceList[counter].device = theDevice;

                    // increment number of instances counter
                    (*cursorGlobals)->deviceCount++;
                }
            }
        }
        theDevice = DMGetNextScreenDevice(theDevice, true);
    }

    if (err != noErr)
    {
        for (index = 0; index < kNumberOfAnimations; index++)
        {
            if ((*cursorGlobals)->deviceList[counter].cursorImage[index] != nil)
                DisposeGWorld
                    ((*cursorGlobals)->deviceList[counter].cursorImage[index]);
        }

        if ((*cursorGlobals)->deviceList[counter].maskImage != nil)
            DisposeGWorld((*cursorGlobals)->deviceList[counter].maskImage);

        if ((*cursorGlobals)->deviceList[counter].saveUnder != nil)
            DisposeGWorld((*cursorGlobals)->deviceList[counter].saveUnder);
    }
    else
    {
        // fill out the cursor data
        err = SetCursorData(cursorGlobals, &data);
    }
    return err;
}



GetCursorInfo

This function fills out the CursorInfo structure passed in. The structure pointer should be checked for validity and filled with the appropriate information. The version will be kCursorComponentsVersion. The capabilities field can either be 0 for minimal capabilities or cursorDoesAnimate to indicate that the cursor will be animating (i.e., changing image data on per-frame basis). animateDuration should be set to the number of ticks between frames. At this time only constant-rate animation is supported (i.e., each frame has the same duration). The last two fields, bounds and hotspot, should be appropriate for the cursor and must be zero based. Remember that you will be drawing the entire bounds at interrupt time. Cursors that are too large will cause slow overall machine performance while they are displayed. An example of GetCursorInfo is shown below.



pascal ComponentResult
GetCursorInfo
(CursorGlobalsHnd storage, CursorInfo *info)
{
    OSErr       err = noErr;

    if (info != nil)
    {
        info->version               = kCursorComponentsVersion;
        // use zero for minimal capabilities
        info->capabilities          = cursorDoesAnimate
        // number of ticks between animations (zero for none)
        info->animateDuration       = 15;
        info->bounds                = (*storage)->bounds;
        info->hotspot               = (*storage)->hotSpot;
    }
    else
        err = paramErr;
    return err;
}


SetCursorData

This function enables an application to pass data to the cursor component. The first parameter is a handle to the CursorGlobalsRecord. The second parameter is a pointer to private data that is passed in by the application. This function's implementation can range from nothing to as complex as needed. The following example implementation shows using SetCursorData to change the cursor image via pictures from a resource file.



pascal ComponentResult
SetCursorData
(CursorGlobalsHnd storage, PrivateCursorPtr data)
{
    ComponentResult     result = -1;
    PicHandle           cursorPict;
    PicHandle           maskPict;
    GWorldPtr           imageWorld;
    GWorldPtr           oldPort;
    GDHandle            oldGD;
    Rect                theRect;
    short               resFile;
    short               c;
    short               index;

    // hide the cursor since we are going to change it
    HideCursor();

    // wack the values to those known by cursor so demo app works
    data->pictureID = kCursorPictureID;
    data->hotSpot.h = (short)(((*storage)->bounds.right -
                            (*storage)->bounds.left) / 2);
    data->hotSpot.v = (short)(((*storage)->bounds.bottom -
                            (*storage)->bounds.top) / 2);

    resFile = OpenComponentResFile((Component)(*storage)->self);
    if (resFile != -1)
    {
        cursorPict = GetPicture((short)data->pictureID);
        maskPict = GetPicture((short)(data->pictureID + kNumberOfAnimations));
        if (cursorPict != nil && maskPict != nil)
        {
            theRect = (*storage)->bounds;
            GetGWorld(&oldPort, &oldGD);

            for (c = 0; c < (*storage)->deviceCount; c++)
            {
                for (index = 0; index < kNumberOfAnimations; index++)
                {
                    imageWorld = (*storage)->deviceList[c].cursorImage[index];
                    cursorPict = GetPicture((short)(data->pictureID + index));
                    if (imageWorld != nil && cursorPict != nil)
                    {
                        SetGWorld(imageWorld, nil);
                        DrawPicture(cursorPict, &theRect);
                    }
                }
                imageWorld = (*storage)->deviceList[c].maskImage;
                if (imageWorld != nil)
                {
                    SetGWorld(imageWorld, nil);
                    DrawPicture(maskPict, &theRect);
                }
            }
            SetPort((GrafPtr)oldPort);
            SetGDevice(oldGD);

            (*storage)->hotSpot = data->hotSpot;

            // Now tell Quickdraw we changed
            result = CursorComponentChanged((*storage)->self);
        }
        CloseComponentResFile(resFile);
    }
    // show the new cursor
    ShowCursor();

    return result;
}


CursorReconfigure

This function is called when the graphics environment has changed. The component should now rebuild its cursors (if required) to correspond to the new environment. A simple example implementation is below. This example calls DisposeDeviceRecords, also shown below, which is the de-allocation function for CreateDeviceRecords (shown above).



pascal ComponentResult
CursorReconfigure
(CursorGlobalsHnd storage)
{
    ComponentResult     result;

    if (storage != nil)
    {
        // dispose of old device records if they exist
        if ((*storage)->deviceCount > 0)
            DisposeDeviceRecords(storage);

        result = CreateDeviceRecords(storage);
    }
    return result;
}

static void
DisposeDeviceRecords
(CursorGlobalsHnd cursorGlobals)
{
    SInt16          index;
    SInt16          cursorIndex;

    for (index = 0; index < (*cursorGlobals)->deviceCount; index++)
    {
        (*cursorGlobals)->deviceList[index].device = nil;

        for (cursorIndex = 0; cursorIndex < kNumberOfAnimations; cursorIndex++)
        {
        if ((*cursorGlobals)->deviceList[index].cursorImage[cursorIndex] != nil)
                DisposeGWorld
                    ((*cursorGlobals)->deviceList[index].cursorImage[cursorIndex]);
        }
        if ((*cursorGlobals)->deviceList[index].saveUnder != nil)
        {
            DisposeGWorld((*cursorGlobals)->deviceList[index].saveUnder);
            (*cursorGlobals)->deviceList[index].saveUnder = nil;
        }
    }
    (*cursorGlobals)->deviceCount = 0;
}


CursorDraw

This is the core of the cursor component, and it will be called every time the cursor is drawn for every display that the cursor spans. Additionally, since cursors are drawn at interrupt time, this function must not move memory (call Memory Manager routines). The first parameter, as usual, is a handle to CursorGlobalsRecord. Following this is a GDHandle for the GDevice on which to draw the cursor. The component should use this information to find the correct cursor images to draw. This is handled in the example by the FindDeviceRecordIndex function. This function, like the others in this example, works together with FindDeviceRecordIndex locating the specific index that corresponds to the cursor image data for the device passed in. The component developer is free to implement this functionality any way you prefer. The last parameter is the point at which to draw the cursor (in locate coordinates). This can be negative if the origin of the cursor is on an adjacent display.

Once the cursor data is found and you have the display and point, the cursor needs to be drawn. It is key to note that this entire drawing sequence occurs at interrupt time; it should be quick and use only interrupt-safe routines. Additionally, the plotted cursor must be clipped to the appropriate device. Lastly, the plotting code absolutely must save the data underneath the cursor image prior to plotting the new one. This can be done all at once, or pixel-by-pixel, as shown in the example. PlotMaskedImageWithSave is shown below as an example of how to plot and save the cursor data. The last call in this routine, CursorFlush, is required to previously mentioned flicker condition that can occur in cursor components. See the Fighting Flicker section for a full explanation.



pascal ComponentResult
CursorDraw
(CursorGlobalsHnd storage, GDHandle device, Point *position)
{
    SInt16      index;

    index = FindDeviceRecordIndex(storage, device);
    if (index != -1)
    {
        PlotMaskedImageWithSave (device, *position,
            (*storage)->deviceList[index].cursorImage
                [(*storage)->animIndex]->portPixMap,
            (*storage)->deviceList[index].maskImage->portPixMap,
            (*storage)->deviceList[index].saveUnder->portPixMap);
    }
    return noErr;
}

static SInt16
FindDeviceRecordIndex
(CursorGlobalsHnd cursorGlobals, GDHandle findDevice)
{
    UInt16          index;

    for (index = 0; index < (*cursorGlobals)->deviceCount; index++)
    {
        if ((*cursorGlobals)->deviceList[index].device == findDevice)
            return index;
    }
    // didn't find device
    return -1;
}

static void
PlotMaskedImageWithSave
(GDHandle device, Point mouse, PixMapHandle srcPix,
          PixMapHandle mskPix, PixMapHandle savePix)
{
    Rect            theRect;
    UInt32          h;
    UInt32          v;
    UInt32          srcHStart;
    UInt32          srcVStart;
    UInt32          offRowBytes;
    UInt32          dstRowBytes;
    SInt32          srcHeight;
    SInt32          srcWidth;
    UInt32          deviceHeight;
    UInt32          deviceWidth;
    UInt32          offOffset;
    SInt32          xOffset;
    SInt32          yOffset;
    UInt16          depth;
    Ptr             srcBase;
    Ptr             mskBase;
    Ptr             dstBase;
    Ptr             saveBase;
    Ptr             srcData;
    Ptr             mskData;
    Ptr             dstData;
    Ptr             saveData;

    // get base of src, msk, and save
    srcBase = (*srcPix)->baseAddr;
    mskBase = (*mskPix)->baseAddr;
    saveBase = (*savePix)->baseAddr;

    offRowBytes = (*srcPix)->rowBytes & 0x7FFF;

    // get base and rowbytes of destination
    dstBase = (*(*device)->gdPMap)->baseAddr;
    dstRowBytes = (*(*device)->gdPMap)->rowBytes & 0x7FFF;
    depth = (*(*device)->gdPMap)->pixelSize;

    theRect = (*device)->gdRect;
    deviceWidth = theRect.right - theRect.left;
    deviceHeight = theRect.bottom - theRect.top;

    theRect = (*srcPix)->bounds;
    srcWidth = theRect.right - theRect.left;
    srcHeight = theRect.bottom - theRect.top;

    xOffset = mouse.h;
    yOffset = mouse.v;
    srcHStart = 0;
    srcVStart = 0;

    // trim h
    if (xOffset < 0)
    {
        srcWidth -= -xOffset;
        srcHStart = -xOffset;
        xOffset = 0;
    }
    else
    {
        if ((xOffset + srcWidth) > deviceWidth)
            srcWidth = deviceWidth - xOffset;
    }

    // trim y
    if (yOffset < 0)
    {
        srcHeight -= -yOffset;
        srcVStart = -yOffset;
        yOffset = 0;
    }
    else
    {
        if ((yOffset + srcHeight) > deviceHeight)
            srcHeight = deviceHeight - yOffset;
    }

    switch (depth)
    {
        case 1:
        case 2:
        case 4:
        case 8:
            DebugStr("\pDon't handle indexed yet");
            break;

        case 16:
            // need to align this stuff
            {
                UInt16          *srcPixel;
                UInt16          *mskPixel;
                UInt16          *dstPixel;
                UInt16          *savePixel;

                offOffset = (srcVStart * offRowBytes) + (srcHStart << 1);

                srcData = srcBase + offOffset;
                mskData = mskBase + offOffset;
                saveData = saveBase + offOffset;
                dstData = dstBase + (yOffset * dstRowBytes) + (xOffset << 1);

                for (v = 0; v < srcHeight; v++)
                {
                    srcPixel = (UInt16 *)srcData;
                    mskPixel = (UInt16 *)mskData;
                    dstPixel = (UInt16 *)dstData;
                    savePixel = (UInt16 *)saveData;

                    for (h = 0; h < srcWidth; h++)
                    {
                        if (*mskPixel++ == 0x00)
                        {
                            *savePixel = *dstPixel;
                            *dstPixel = *srcPixel;
                        }

                        dstPixel++;
                        srcPixel++;
                        savePixel++;
                    }

                    srcData += offRowBytes;
                    mskData += offRowBytes;
                    dstData += dstRowBytes;
                    saveData += offRowBytes;
                }
            }
            break;

        case 32:
            {
                UInt32          *srcPixel;
                UInt32          *mskPixel;
                UInt32          *dstPixel;
                UInt32          *savePixel;

                offOffset = (srcVStart * offRowBytes) + (srcHStart << 2);

                srcData = srcBase + offOffset;
                mskData = mskBase + offOffset;
                saveData = saveBase + offOffset;
                dstData = dstBase + (yOffset * dstRowBytes) + (xOffset << 2);
                for (v = 0; v < srcHeight; v++)
                {
                    srcPixel = (UInt32 *)srcData;
                    mskPixel = (UInt32 *)mskData;
                    dstPixel = (UInt32 *)dstData;
                    savePixel = (UInt32 *)saveData;

                    for (h = 0; h < srcWidth; h++)
                    {
                        if (*mskPixel++ == 0x00)
                        {
                            *savePixel = *dstPixel;
                            *dstPixel = *srcPixel;
                        }
                        srcPixel++;
                        dstPixel++;
                        savePixel++;
                    }

                    srcData += offRowBytes;
                    mskData += offRowBytes;
                    saveData += offRowBytes;
                    dstData += dstRowBytes;
                }
            }
            break;

        default:
            break;
    }
    CursorFlush(device);
}


CursorErase

This function really is a drawing function. It just undoes the work done by CursorDraw by restoring the save screen image data. The parameters are the same as for CursorDraw and have the same caveats. The applicable save data must be found based on the GDHandle parameter. RestoreMaskedImage (as shown below) restores the clipped data back to the screen. As with other component functions, this must be interrupt safe.



pascal ComponentResult
CursorErase
(CursorGlobalsHnd storage, GDHandle device, Point *position)
{
    SInt16          index;

    index = FindDeviceRecordIndex(storage, device);
    if (index != -1)
    {
        RestoreMaskedImage(device, *position,
            (*storage)->deviceList[index].saveUnder->portPixMap,
            (*storage)->deviceList[index].maskImage->portPixMap);
    }
    return noErr;
}

static void
RestoreMaskedImage
(GDHandle device, Point mouse, PixMapHandle srcPix, PixMapHandle mskPix)
{
    Rect            theRect;
    UInt32          h;
    UInt32          v;
    UInt32          srcHStart;
    UInt32          srcVStart;
    UInt32          offRowBytes;
    UInt32          dstRowBytes;
    UInt32          offOffset;
    SInt32          srcHeight;
    SInt32          srcWidth;
    UInt32          deviceHeight;
    UInt32          deviceWidth;
    SInt32          xOffset;
    SInt32          yOffset;
    UInt16          depth;
    Ptr             srcBase;
    Ptr             dstBase;
    Ptr             mskBase;
    Ptr             srcData;
    Ptr             dstData;
    Ptr             mskData;

    // get base of src and mask
    srcBase = (*srcPix)->baseAddr;
    mskBase = (*mskPix)->baseAddr;

    offRowBytes = (*srcPix)->rowBytes & 0x7FFF;

    // get base and rowbytes of destination
    dstBase = (*(*device)->gdPMap)->baseAddr;
    dstRowBytes = (*(*device)->gdPMap)->rowBytes & 0x7FFF;
    depth = (*(*device)->gdPMap)->pixelSize;

    theRect = (*device)->gdRect;
    deviceWidth = theRect.right - theRect.left;
    deviceHeight = theRect.bottom - theRect.top;

    theRect = (*srcPix)->bounds;
    srcWidth = theRect.right - theRect.left;
    srcHeight = theRect.bottom - theRect.top;

    xOffset = mouse.h;
    yOffset = mouse.v;
    srcHStart = 0;
    srcVStart = 0;

    // trim h
    if (xOffset < 0)
    {
        srcWidth -= -xOffset;
        srcHStart = -xOffset;
        xOffset = 0;
    }

    if ((xOffset + srcWidth) > deviceWidth)
        srcWidth = deviceWidth - xOffset;

    // trim y
    if (yOffset < 0)
    {
        srcHeight -= -yOffset;
        srcVStart = -yOffset;
        yOffset = 0;
    }

    if ((yOffset + srcHeight) > deviceHeight)
        srcHeight = deviceHeight - yOffset;

    switch (depth)
    {
        case 1:
        case 2:
        case 4:
        case 8:
            DebugStr("\pDon't handle indexed yet");
            break;

        case 16:
            // need to align this stuff
            {
                UInt16          *srcPixel;
                UInt16          *dstPixel;
                UInt16          *mskPixel;

                offOffset = (srcVStart * offRowBytes) + (srcHStart << 1);

                srcData = srcBase + offOffset;
                mskData = mskBase + offOffset;
                dstData = dstBase + (yOffset * dstRowBytes) + (xOffset << 1);

                for (v = 0; v < srcHeight; v++)
                {
                    srcPixel = (UInt16 *)srcData;
                    dstPixel = (UInt16 *)dstData;
                    mskPixel = (UInt16 *)mskData;

                    for (h = 0; h < srcWidth; h++)
                    {
                        if (*mskPixel == 0x00)
                        {
                            *dstPixel = *srcPixel;
                        }

                        dstPixel++;
                        mskPixel++;
                        srcPixel++;
                    }

                    srcData += offRowBytes;
                    mskData += offRowBytes;
                    dstData += dstRowBytes;
                }
            }
            break;

        case 32:
            {
                UInt32          *srcPixel;
                UInt32          *dstPixel;
                UInt32          *mskPixel;

                offOffset = (srcVStart * offRowBytes) + (srcHStart << 2);

                srcData = srcBase + offOffset;
                mskData = mskBase + offOffset;
                dstData = dstBase + (yOffset * dstRowBytes) + (xOffset << 2);

                for (v = 0; v < srcHeight; v++)
                {
                    srcPixel = (UInt32 *)srcData;
                    dstPixel = (UInt32 *)dstData;
                    mskPixel = (UInt32 *)mskData;

                    for (h = 0; h < srcWidth; h++)
                    {
                        if (*mskPixel == 0x00)
                        {
                            *dstPixel = *srcPixel;
                        }

                        dstPixel++;
                        mskPixel++;
                        srcPixel++;
                    }

                    srcData += offRowBytes;
                    mskData += offRowBytes;
                    dstData += dstRowBytes;
                }
            }
            break;

        default:
            break;
    }
}


CursorMove

This function must both erase and restore the last image, and draw a new image. To this end, both the old position and new position are provided. Again, this all happens at interrupt time; the GDHandle must be used to determine the cursor data to restore and draw the screen and cursor image. All the caveats for the previously described plotting functions apply.



pascal ComponentResult
CursorMove
(CursorGlobalsHnd storage, GDHandle device, Point *lastPosition, Point *newPosition)
{
    SInt16          index;

    index = FindDeviceRecordIndex(storage, device);
    if (index != -1)
    {
        RestoreMaskedImage(device, *lastPosition,
            (*storage)->deviceList[index].saveUnder->portPixMap,
            (*storage)->deviceList[index].maskImage->portPixMap);
        PlotMaskedImageWithSave(device, *newPosition,
            (*storage)->deviceList[index].cursorImage
                    [(*storage)->animIndex]->portPixMap,
            (*storage)->deviceList[index].maskImage->portPixMap,
            (*storage)->deviceList[index].saveUnder->portPixMap);
    }
    return noErr;
}


CursorAnimate

The last function in the component API is CursorAnimate. It is responsible for indexing through the cursor's various images. You will only need to provide this function if your component handles animation. The animate function is called once for each display. In the example case, we simply index through pre-rendered images so we care only about the first display. If your component renders each frame on the fly, you will most likely require notifications for each display. Once again, the specifics of the animate function are completely defined by your cursor implementation. Below is a simple example of a rotating animation.



pascal ComponentResult
CursorAnimate
(CursorGlobalsHnd storage, GDHandle device, Point *lastPosition)
{
#pragma unused (device, lastPosition)
    UInt32          index;

    if (device == GetMainDevice())
    {
        index = (*storage)->animIndex;
        index++;
        if (index > kNumberOfAnimations - 1)
            index = 0;
        (*storage)->animIndex = index;
    }
    return noErr;
}


Resources

The cursor component compiled as a code resource needs a 'thng' resource, which allows the application to identify the specific component it is accessing. Below is a simple example of a beach ball cursor's 'thng' resource. Note the kCursorSubType: this will be used later to identify this particular cursor code resource. Beyond this, you are free to use resources to store any data required to build the cursor.



#define kCursorResID            129
#define kCursorName             "Beachball Cursor"
#define kCursorType             'curs'
#define kCursorSubType          'bbcc'
#define kCursorManufacturer     'appl'


resource 'thng' (kCursorResID, kCursorName, purgeable)
{
    kCursorType,
    kCursorSubType,
    kCursorManufacturer,
    0x80000020,
    kAnyComponentFlagsMask,
    'crco',
    kCursorResID,
    'STR ',
    kCursorResID + 5000,
    'STR ',
    kCursorResID + 5050,
    'ICON',
    kCursorResID,
    0x0,
    8,
    0,
    {   /* array ComponentPlatformInfo: 1 elements */
        /* [1] */
        0x80000000, 'crco', kCursorResID, platformPowerPC
    }
};


Back to top


Using Components

Once the cursor component is created, an application must be created to instantiate it. The application interface for the component is fairly simple. The basic requirements are that the code resource is loaded, the cursor is instantiated by opening it, and then it is set. Once this occurs the application can pass data directly to the cursor. Finally, when it is done with it, close and unload it.

Registering Components

First, we look at handling the code resource. The code below registers the cursors code resource, allowing it to be opened.



OSErr
RegisterCursorComponents
(void)
{
    OSErr       err = -1;

    if (RegisterComponentResourceFile (CurResFile(), 0))
        err = noErr;

    return err;
}


Opening the Cursor

Once the component is registered, we can open the cursor. This is achieved by setting up a ComponentDescription to look for the cursor component. The keys here are the subtype and the manufacturer: ensure that these match your cursor's 'thng' resource. Once this it completed, call FindNextComponent to find the next component that fits the description. If this returns without error, the component found can be used in the new QuickDraw function OpenCursorComponent to actually open the cursor. OpenCursorComponent is defined as follows:



 OSErr OpenCursorComponent (Component c, ComponentInstance * ci); 


The ComponentInstance parameter should be set on return to a specific instance the cursor that can used with the other cursor component functions. This functionality can be seen in the example below, which takes a component subtype and attempts to open it, in turn setting the global ComponentInstance as applicable.



void
OpenTheCursor
(OSType subType)
{
    OSErr                       err;
    ComponentDescription        theDesc;
    Component                   comp = nil;
    Point                       hotspot;

    // set up the descriptor
    theDesc.componentType           = 'curs';
    theDesc.componentSubType        = subType;
    theDesc.componentManufacturer   = 'appl';
    theDesc.componentFlags          = 0L;
    theDesc.componentFlagsMask      = 0L;

    comp = FindNextComponent(0L, &theDesc);
    if (comp != nil)
    {
        err = OpenCursorComponent(comp, &gComp);
        if (err == noErr)
            gCurrentSubType = subType;

        else
            gComp = nil;

        if (err != noErr)
            HandleError(err, false);
    }
}


Setting the Cursor

After the cursor is opened it can be set. This should be done when the application wants to display the particular cursor. To achieve this SetCursorComponent is call. The function is defined in QuickDraw.h as:



 OSErr SetCursorComponent (ComponentInstance ci); 


This can be accomplished in the following simple manner.



void
SetTheCursor
(void)
{
    SetCursorComponent(gComp);
}


Closing the Cursor

Once the application is done with a particular cursor (and absolutely before exit), it should be close with CloseCursorComponent defined as:



 OSErr CloseCursorComponent (ComponentInstance ci); 


Again, this is simply implemented in the sample by just checking for NIL and passing the global ComponentInstance to the function as shown below.



void
CloseTheCursor
(void)
{
    if (gComp != nil)
    {
        CloseCursorComponent(gComp);
        gComp = nil;
    }
}


Modifying the Cursor

Cursor components possess the CursorComponentSetData function, which allows modification of the cursor. This can be used to change frame data, set a state, or just about anything else you can imagine. The set data function is defined as:



 OSErr CursorComponentSetData (ComponentInstance ci, long data); 


This allows passing either direct data or a pointer to the cursor itself. An example of utilizing can also be found in the example application's SetCursorPictureID function that sets the bitmap for the current cursor.



OSErr
SetCursorPictureID
(short pictureID, Point *hotspot)
{
    OSErr                       err = noErr;
    PrivateCursorData           data;
    static  short               lastID = -99;

    // return if image is already set
    if (lastID == pictureID) return err;

    // only set image if cursor is bitmap cursor...
    if (gCurrentSubType == kBitMapSubType)
    {
        data.pictureID = (long)pictureID;
        data.hotSpot = *hotspot;

        err = CursorComponentSetData((ComponentInstance)gComp, (long)&data);
        if (err == noErr)
            lastID = pictureID;
    }
    return err;
}


Basically CursorComponentSetData provides an application-defined conduit to the cursor component, but does not alter any outside characteristics. It is worthwhile to note if the call to CursorComponentSetData modifies that appearance of the cursor, CursorComponentSetData should call CursorComponentChanged. This will notify QuickDraw that the appearance and/or other info about the cursor has changed. QuickDraw will re-interrogate the cursor for hotspot, bounds, etc., and redraw it.

Miscellaneous

If you are changing cursors (not just changing images) you should ensure that you open the new cursor before closing the old one. This will prevent the cursor from reverting to the arrow during the change. Here's some example code to do this:



void
OpenNewCloseOld
(OSType subType)
{
    ComponentInstance   oldCursor;

    // save the current cursor
    oldCursor = gComp;

    // open the new one
    OpenTheCursor(subType);

    if (oldCursor != nil)
    {
        CloseCursorComponent(oldCursor);
    }
}



Back to top


Fighting Flicker

Cursor flicker can occur when using color cursors under some circumstances. RAGE 128-based systems may not flush their internal cache on every cursor redraw, causing some or all of the cursor to flicker. This problem can be corrected by utilizing the included CursorFlush library. The CursorFlush header file defines two routines, InitializeCursorFlush and CursorFlush. The former should be called in the CursorInitialize function to set up the flush library. The later should be called when drawing is complete to ensure the video card correctly displays the cursor image without flicker.

Back to top


Summary

Obviously, Cursor Components provide a useful and greatly expanded human interface element. At first, they may seem difficult to implement. By reading this Technote and studying the code provided in the SDK, you can see the implementation is straightforward, and the resources in the SDK provide a good start. Freeform color cursors are here today on the Mac OS; it is only your imagination that limits their implementation. Go forth and build some awesome cursors.

References

Cursor Component SDK

Technote 1104: Interrupt-Safe Routines

Inside Macintosh: Imaging With QuickDraw

Technote web site


Back to top


Downloadables

Acrobat

Acrobat version of this Note (92K).

Download




Back to top


Technical Notes by Date | Number | Technology | Title
Developer Documentation | Technical Q&As | Development Kits | Sample Code




Gray line

Contact ADC |  ADC Site Map |  ADC Advanced Search
For information about Apple Products, please visit Apple.com.
Contact Apple | Privacy Notice
Copyright © 2002 Apple Computer, Inc. All rights reserved.
1-800-MY-APPLE