Cursor (the pointer’s shape over a piece)

Authoring

use day::prelude::*;

link_row.cursor(Cursor::Pointer)
canvas.cursor(Cursor::Crosshair)
divider.cursor(Cursor::ColResize)

// Reactive: a tool palette, or a busy state. A constant, a Signal<Cursor>, or a closure.
let tool = Signal::new(Tool::Pen);
canvas.cursor(move || match tool.get() {
    Tool::Pen => Cursor::Crosshair,
    Tool::Pan => Cursor::Grab,
})

// A shape only one toolkit names, compiled only under that backend's feature.
#[cfg(feature = "appkit")]
trash.cursor(day::cursor::appkit::DISAPPEARING_ITEM)

// Anything a backend names that Day does not; ignored everywhere else.
row.cursor(Cursor::native("screenshot-cursor"))

Three rules:

  • The cursor applies to the piece’s realized widget and its descendants, until a descendant sets its own. The nearest ancestor wins.
  • Cursor::Default releases the piece to the platform rather than pinning an arrow, so a reactive cursor can hand back what it took.
  • A shape is a pointer affordance and nothing else. A touch screen never shows one, and that is correct; an affordance that only a cursor reveals (a resize handle with no visible grip) should check capability(Cap::Cursor) before it counts on being discovered.

The vocabulary

Cursor is the CSS cursor vocabulary as an enum, plus one open variant. CSS names are the largest set any toolkit accepts as they are (GTK and the DOM take them verbatim), and every other toolkit’s set is a subset of them or maps onto them, so the enum is the contract and the table below is the only mapping written down.

VariantCSS nameMeaning
Defaultdefaultthe platform’s arrow; also releases the piece
Pointerpointera pointing hand, for links and tap-to-activate content
Text, VerticalTexttext, vertical-textan I-beam
Crosshaircrosshair
Movemovefour-way move
Grab, Grabbinggrab, grabbingan open and a closed hand
NotAllowednot-allowed
Wait, Progresswait, progressbusy; Progress still takes input
Helphelp
ContextMenucontext-menu
Copy, Aliascopy, aliasa drop will copy or link
Cellcella table cell
ZoomIn, ZoomOutzoom-in, zoom-out
Nonenoneno pointer at all
NsResize, EwResizens-resize, ew-resizevertical and horizontal resize
NeswResize, NwseResizenesw-resize, nwse-resizediagonal resize
ColResize, RowResizecol-resize, row-resizea divider between columns or rows
Native(name)the nameone toolkit’s shape, see below

Cursor::css_name() answers the keyword, and Cursor::NAMED walks the named set in order, which is what the Showcase’s Cursors page draws.

Per toolkit

ToolkitMechanismCap::CursorWhat differs
macos-appkitan NSTrackingArea with cursorUpdate per view, owned by a small object that sets the NSCursorNativeno wait, progress, help, or move shape (the arrow, or the open hand for move); zoom, diagonal frame resize, and column/row resize need macOS 15 and fall back below it; None hides the pointer while inside
gtkwidget.set_cursor(gdk::Cursor::from_name(css))Nativethe theme’s own drawings; a name the theme lacks falls back to its arrow
qtQWidget::setCursor(Qt::CursorShape)Emulatedno zoom, cell, context-menu, or vertical-text shape; each takes its nearest (Help is What’s This, Progress is Busy)
xamlPointerEntered/PointerExited on the element record the shape; the host window answers WM_SETCURSOR with a Win32 IDC_* cursorEmulatedno grab, zoom, column/row resize, copy, alias, cell, or vertical-text shape; nearest of the classic set. UWP XAML in a Win32 host has no per-element cursor property, which is why the host owns it
web-doman inline cursor: style on the elementNativethe browser’s own set; Default removes the inline style so the stylesheet’s affordance rules apply again
android-mdcView.setPointerIcon(PointerIcon.getSystemIcon(…)), API 24Nativethe full vocabulary; visible only with a mouse or trackpad, on ChromeOS, or in desktop windowing
ios-uikita UIPointerInteraction whose delegate answers a UIPointerStyleEmulatediPadOS draws pointer effects, not arrows: Text and VerticalText become a beam, Pointer, Grab, Grabbing, Copy, Alias, and ContextMenu the highlight effect, Move the lift effect, None hides the pointer, everything else keeps the system pointer
harmony-arkuinone yetUnsupportedpointer.setPointerStyle exists only in ArkTS; wiring a hover hook through an ArkTS helper is the follow-up
mockrecords the last cursor per widgetNativeprobe-visible in tests

The duty and its implementors are in duty-matrix.md; the capability answers in coverage-matrix.md. Both are generated and diffed in CI.

Shapes only one toolkit names

The day::cursor module carries them, one submodule per toolkit, each gated on the facade’s backend feature:

ModuleConstants
day::cursor::appkitDISAPPEARING_ITEM, DRAG_COPY, DRAG_LINK, CONTEXTUAL_MENU, I_BEAM_VERTICAL
day::cursor::qtWHATS_THIS, BUSY, UP_ARROW, SPLIT_H, SPLIT_V
day::cursor::xamlPERSON, PIN, UP_ARROW, APP_STARTING
day::cursor::androidALL_SCROLL, NO_DROP, TOP_RIGHT_DIAGONAL, TOP_LEFT_DIAGONAL

Each is a Cursor::Native value, so the named backend resolves it by its own table and any other ignores it. The gate is the point: a name only AppKit has is a compile error in a GTK build rather than a silent arrow, and the app wraps the use in the same #[cfg(feature = …)]. Cursor::native("…") is the ungated form for a name Day has no constant for.

How it reaches the widget

Decorate::cursor pushes an op that, after the inner piece is built, calls TreeOps::set_node_cursor, which forwards to Toolkit::set_cursor on the node’s handle. A reactive source also binds a closure that calls the same setter on change. The setter is idempotent and cheap on every toolkit, so there is no patch enum and no extra native node; a layout-only inner piece has no widget of its own and the op resolves the nearest realized node the way .selectable() does.

Trying it

The Showcase’s Cursors page (Controls group): every named shape as a tile to hover, one box whose shape follows a picker, a nested pair that shows the nearest ancestor winning, and the toolkit’s extras.

day launch -p macos-appkit --script dayscript/cursors.yaml

The walkthrough asserts the support row and drives the picker. The shape itself is the acceptance test, and it needs a person with a mouse: on macOS, in a browser, on the Android emulator with a mouse attached, and on the iPad simulator with pointer support enabled.