Skip to content
Haiku OSDeep Dive Published Updated 8 min readViews unavailable

Haiku BCursor: Bitmap Hotspots, Application Scope, and Legacy Data

Build Haiku BCursor objects from bitmaps and hotspots, choose application or view scope, and avoid legacy 16-by-16 data and persistence traps.

Haiku’s BCursor is an Interface Kit object that names cursor data managed by app_server. It is not just a BBitmap, and changing the pointer image is not the same operation as tracking mouse movement. A robust implementation chooses between a system cursor and a custom bitmap, puts the hotspot on the pixel that should receive clicks, and installs the result at the right scope: the active application or one view.

There are two easily confused custom-cursor APIs. The older BCursor(const void*) constructor accepts a fixed BeOS-format byte array, while also recognizing the predefined B_HAND_CURSOR and B_I_BEAM_CURSOR pointer constants. Haiku also provides BCursor(const BBitmap*, const BPoint&), which takes a bitmap and a hotspot. The second overload is the practical choice for Haiku-specific artwork; feeding BBitmap::Bits() to the first overload is not a shortcut because the byte layouts are different.

The Haiku API reference lists the data-pointer overload as available since BeOS R5 and the bitmap-and-hotspot overload since Haiku R1. Treat that as a real compatibility boundary: code that must also build against BeOS-compatible headers cannot assume the Haiku bitmap overload exists.

Choose a stock cursor before building artwork

Cursor.h exposes BCursorID values for common meanings such as crosshair, help, progress, move, resize, and text insertion. Prefer one of those when it accurately describes the interaction. A standard cursor communicates familiar behavior and avoids maintaining bitmap data, alpha handling, hotspot coordinates, and a custom object lifetime.

For application-wide defaults, BApplication::SetCursor() sets the cursor used while the application is active. For a local affordance, BView::SetViewCursor() assigns a cursor that is displayed when the pointer is inside that view. For example, an application can keep the normal system pointer globally and use a crosshair over a drawing canvas. A view-specific choice should remain tied to that view’s actual interaction; it should not become an accidental global state change when the pointer leaves.

Both methods take a sync argument on their BCursor overloads. With true, the API synchronizes with app_server immediately; with false, the request is queued for later processing. The synchronous form is useful when subsequent logic depends on the change having been processed, but it introduces a wait across the client/server boundary. Avoid turning it into a per-motion synchronization loop when a normal queued update is sufficient.

Build from a bitmap and put the hotspot on purpose

The Haiku-specific constructor accepts a BBitmap and a BPoint hotspot. The hotspot is the point in the image that corresponds to the actual pointer location and click target. If a pointer image is drawn like an arrow, that is usually its tip; for a crosshair, it is usually the intersection. A visually centered hotspot on an asymmetric arrow can make clicks land away from what the user sees as the active point.

The current Haiku implementation sends the bitmap bounds, hotspot, color space, row stride, and pixel bytes to app_server. That is a different contract from the historic 16-by-16 monochrome array. Haiku’s ServerCursor implementation is designed to accommodate cursor bitmaps of different sizes and color spaces, but that implementation detail is not a promise that every display driver or rendering path has identical practical limits. Check the constructor result and test the output on the target Haiku release and graphics configuration.

Treat the hotspot as data that needs validation, not a decorative offset. Check the bitmap’s initialization and bits before constructing the cursor, and keep the point within the bitmap bounds. The current server implementation constrains an out-of-bounds hotspot to the cursor bounds; do not rely on that silent correction to hide a coordinate bug. Also use the bitmap’s declared bounds and format rather than assuming a tightly packed RGBA array. The BBitmap API’s row stride and color-space contract still applies.

Here is a view-owned example. The image is assumed to be a successfully loaded BBitmap whose lifetime covers construction. The member keeps the BCursor available for later reuse; install it after the view is attached, and restore a defined cursor rather than passing NULL to clear it.

#include <Bitmap.h>
#include <Cursor.h>
#include <View.h>

class CanvasView : public BView {
public:
    CanvasView(BRect frame, const BBitmap* pointerImage)
        : BView(frame, "canvas", B_FOLLOW_ALL, B_WILL_DRAW),
          fCursor(pointerImage, BPoint(2, 2))
    {
    }

    void AttachedToWindow() override
    {
        BView::AttachedToWindow();
        if (fCursor.InitCheck() == B_OK)
            SetViewCursor(&fCursor, false);
    }

    void RestoreSystemCursor()
    {
        SetViewCursor(B_CURSOR_SYSTEM_DEFAULT, false);
    }

private:
    BCursor fCursor;
};

The caller still needs to validate pointerImage and ensure (2, 2) is inside its bounds before creating the view. In production code, make the hotspot a named value derived from the artwork, check BBitmap::InitCheck(), IsValid(), Bits(), and BitsLength(), and check BCursor::InitCheck() before installing it. SetViewCursor() returns void, so there is no status value from that call to substitute for checking cursor construction. Call view APIs under the owning window’s normal locking and looper rules.

SetViewCursor(NULL) is not a reset operation in the current client implementation: it returns without sending a change. Use a known cursor such as B_CURSOR_SYSTEM_DEFAULT when the view should stop displaying its custom affordance. The public API reference describes the view cursor as applying when the pointer is positioned inside that view; test the transitions at the view edges, over child views, and when the window loses and regains focus.

Keep the old byte-array format separate

The older constructor takes a pointer but no length. Its input must therefore already be a valid legacy cursor block. Haiku’s current client implementation validates a size byte of 16, a depth byte of 1, and two hotspot coordinates below 16, then sends 68 bytes to app_server. The historical Be Book specifies the remaining layout: four header bytes, 32 bytes of 16-by-16 one-bit pixel data, and a 32-byte transparency mask. In that format, the two hotspot bytes are stored as Y followed by X, whereas BPoint expresses X and Y as coordinates. Copying the bytes into a BPoint in the same order swaps the axes.

The server source still supports this compatibility representation, but its comment calls the API problematic and says it should be deprecated, while remaining supported, in R2. That is an implementation comment, not a version-independent promise about when deprecation will occur. Keep legacy arrays at exactly the expected size and format, and never pass arbitrary memory, a short buffer, or the raw bits of a BBitmap to BCursor(const void*). For new Haiku code, the bitmap constructor makes bounds, color space, stride, and hotspot explicit.

Do not treat BArchivable as a persistence guarantee

BCursor inherits from BArchivable, but the current API documentation marks cursor archiving and instantiation as unimplemented. The current implementation’s Archive() returns B_OK without serializing cursor data, while Instantiate() returns NULL. A success status from Archive() is consequently not evidence that a cursor can be reconstructed later. If a preference or document must preserve cursor artwork, persist the source bitmap or a documented application-owned representation plus the hotspot, then construct a new BCursor at runtime.

This also keeps implementation tokens out of persistent state. The client implementation registers cursor data with app_server and uses a server token when setting the application or view cursor. The token is part of the running system’s object relationship, not a stable file format, asset identifier, or cross-run cache key.

Test the image, scope, and fallback independently

Use a small target-system test matrix rather than treating a successful constructor as a complete visual check:

  • Verify the bitmap loads, BCursor::InitCheck() returns B_OK, and the image renders with the intended transparency and colors.
  • Click targets near each edge and at the hotspot. The expected model coordinate should match the pixel under the intended active point.
  • Move from the custom view to adjacent content, a child view, another application, and the desktop. Confirm that application-wide defaults and view-specific changes have the intended scope.
  • Exercise both queued and synchronous updates where timing matters. Do not assume sync=false means the app server has already applied the change when the call returns.
  • Test invalid image data and out-of-range hotspot inputs. Reject bad application input explicitly instead of relying on server-side clamping.
  • If compatibility arrays are still required, test header bytes, both hotspot axes, the monochrome image plane, and transparency mask separately. Do not test that path using a BBitmap’s pixel buffer.

The source tree and documentation establish the API and current implementation details, but they do not replace a visual test on the actual Haiku version, display stack, and input configuration shipped by an application. The narrow rule is straightforward: use a stock BCursorID where possible, use the bitmap constructor for Haiku custom art, keep its hotspot and scope explicit, and regard the old 68-byte format as a separate compatibility boundary.

Related:

Sources:

Comments