gfx/window_cocoa.h
The Cocoa window, so the native UI runs on the third platform.
#include <sonore/gfx/window_cocoa.h>- 2types
- 58other declarations
- Guides: The native editor
On this page
Design notes
Written, compiled, never run
Say it plainly at the top rather than let a green build imply more than it earned: no machine in this project has executed a line of this file. macOS CI is the only one that could and it has not started a job since the account's spending limit was reached. What HAS happened is that a compiler has read it, against the shim in tests/au_shim -- so every name exists, every call has the right argument count, and the file is internally consistent. That is a real class of error caught, and it is not the same as working.
The Win32 and X11 peers are checked against real windows with real events. This one is not, and the feature map says so.
Written against the runtime, not in Objective-C
Same reason as webview_cocoa.h: the SDK stays plain C++, and a generated project needs no .mm file, no mixed-language build and no Xcode-specific CMake. Every Cocoa call here is objc_msgSend on a class looked up by name, which is exactly what the Objective-C compiler emits anyway.
The cost is that an NSView subclass has to be REGISTERED at runtime, with C functions as its method implementations. There is no way around that: drawing and input on macOS both arrive as overridden methods, and a view that overrides nothing draws nothing and receives nothing.
What macOS does differently
- Its y axis points UP, and a view's coordinates start at the bottom left. Every other surface in this SDK -- the rasteriser, the component tree, every peer -- is y-down. isFlipped is overridden to return YES, which makes one view y-down and leaves the conversion in one place instead of scattering
height - ythrough the input handlers.
- There is no pointer grab. X11 has XGrabPointer and Windows has SetCapture; macOS has neither, and a popup dismisses because its window stops being key. That is a genuinely different mechanism, not a translation of the same one.
- Coordinates for a click arrive in WINDOW space and have to be converted into the view. Using them unconverted is the classic Cocoa bug and it looks like an editor whose controls respond a title-bar's height away from where they are.
Screen readers and input methods (2026-10-14, compiled, not run)
The view is also the way in for VoiceOver and for input methods, and both are as unrun as the rest of this file:
- Accessibility: the view is an AXGroup whose children are the elements of gfx/accessibility_mac.h, the NSAccessibility bridge over the same model the Windows UIA bridge reads. Retaken and announced on this view's clock.
- Input methods: the view adopts NSTextInputClient. When a text field has the focus, keyDown: goes through interpretKeyEvents:, so the system's input method sees the key first and answers with insertText: (typed or committed text), setMarkedText: (a composition, drawn by the field, see gfx/text_input.h) or doCommandBySelector: (an editing key, delivered as the KeyPress it always was). Every range arithmetic step lives in TextInputSession and is tested on Windows; what is here is the NSString and NSRange at the edge. Without a text field focused, keys take the path they always took -- a knob has nothing to compose into, and an input method that ate its keys would swallow the host's shortcuts.
tests/mac_input_selftest.cpp is what a Mac developer runs to see what both expose; until somebody has, treat both as written and not working.
Types 2
The bit of CoreGraphics needed to put a rectangle of pixels on screen. Resolved with dlsym rather than linked, so nothing here adds a framework a generated project would have to name.
A component tree in an NSView, with the same surface as the other two peers.
Identical API on purpose: NativeEditor holds whichever one the platform has and knows nothing about which, so a difference between platforms has to be written deliberately rather than arrived at.
Functions 56
- fn
template <typename Ret, typename... Args> inline Ret msg(id target, SEL selector, Args... args)objc_msgSend must be called through a correctly-typed pointer: it is a trampoline, and on arm64 variadic and regular calls use different registers, so a wrongly typed call corrupts arguments. Same helper the webview host uses, duplicated rather than shared so this file has no dependency on the webview backend being compiled in.gfx::cocoa::msg · window_cocoa.h:108 - fn
inline id cls(const char* name)gfx::cocoa::cls · window_cocoa.h:113 - fn
template <typename Ret, typename... Args> inline Ret msgRect(id target, SEL selector, Args... args)A message whose answer is a struct LARGER than 16 bytes -- an NSRect.gfx::cocoa::msgRect · window_cocoa.h:125More
On x86_64 such a struct comes back through a hidden pointer, and the message has to go through objc_msgSend_stret, which expects that pointer first. Sent through plain objc_msgSend, the compiler still passes it, in the register objc_msgSend reads
selffrom: every frame and rectangle conversion on an Intel Mac messaged a stack address. arm64 has no _stret and returns structs through objc_msgSend itself. (An NSPoint, two doubles, comes back in registers on both and needs none of this.) - fn
inline void keepTickingInEveryMode(id timer)Add a scheduled timer to the run loop's COMMON modes as well, so it keeps firing while a host runs the loop in a modal or event-tracking mode. NSRunLoopCommonModes is the string "kCFRunLoopCommonModes"; modes compare by value, so an NSString of it is the constant.gfx::cocoa::keepTickingInEveryMode · window_cocoa.h:138 - fn
inline TextRange16 rangeFromNS(NSRange2 r)gfx::cocoa::rangeFromNS · window_cocoa.h:158 - fn
inline NSRange2 rangeToNS(TextRange16 r)gfx::cocoa::rangeToNS · window_cocoa.h:163 - fn
inline std::string textOf(id value)An NSString or NSAttributedString as UTF-8 -- insertText: and setMarkedText: are documented to take either.gfx::cocoa::textOf · window_cocoa.h:170 - fn
inline unsigned long long nowMs()Milliseconds on a steady clock, for the accessibility bridge's rate limits. Never 0 in practice; the bridge reads 0 as "no clock".gfx::cocoa::nowMs · window_cocoa.h:181 - fn
inline const CoreGraphicsApi& coreGraphics()gfx::cocoa::coreGraphics · window_cocoa.h:206 - fn
inline NativeWindowCocoa* peerOfView(id view)Every registered method needs a C function, and every one of them needs to find the peer that owns the view. It is stored in an instance variable, set once when the view is made.gfx::peerOfView · window_cocoa.h:252 - fn
inline BOOL sonoreViewIsFlipped(id, SEL)Y-DOWN. Every other surface in this SDK is, and overriding this puts the conversion in one place instead of scatteringgfx::sonoreViewIsFlipped · window_cocoa.h:1239height - ythrough five input handlers -- where forgetting one gives an editor whose clicks land correctly and whose drags go the wrong way. - fn
inline BOOL sonoreViewAcceptsFirstMouse(id, SEL, id)So the first click into an unfocused plugin window does something, rather than being swallowed to focus the window. In a DAW the plugin window is usually not the focused one, so without this every first click is lost.gfx::sonoreViewAcceptsFirstMouse · window_cocoa.h:1244 - fn
inline void sonoreViewDrawRect(id self, SEL, cocoa::CGRect2)gfx::sonoreViewDrawRect · window_cocoa.h:1246 - fn
inline Point sonorePointOf(id self, id event)An event's location is in WINDOW coordinates and has to be converted into the view. Using it unconverted is the classic Cocoa mistake, and it shows up as an editor whose controls respond a title-bar's height from where they are.gfx::sonorePointOf · window_cocoa.h:1259 - fn
inline Point sonoreDragPoint(id self, id sender)Where a DRAG is, in the view's coordinates.gfx::sonoreDragPoint · window_cocoa.h:1274More
draggingLocation is in window space like an event's, and needs the same conversion -- using it unconverted drops a file a title-bar's height from where it was let go.
- fn
inline void sonoreModifiers(id event, bool* shift, bool* ctrl, bool* alt)NSEventModifierFlags: shift 1<<17, control 1<<18, option 1<<19.gfx::sonoreModifiers · window_cocoa.h:1286 - fn
inline void sonoreViewMouseDown(id self, SEL, id event)gfx::sonoreViewMouseDown · window_cocoa.h:1294 - fn
inline void sonoreViewRightMouseDown(id self, SEL, id event)The right button, and on the Mac also Control-click.gfx::sonoreViewRightMouseDown · window_cocoa.h:1321More
Cocoa routes a Control-click to rightMouseDown: itself, so honouring one selector honours both -- which matters because Control-click IS the right click on a Mac with a one-button mouse, and a plugin where it did nothing would be a plugin whose parameters cannot be automated by the usual gesture.
Compiled on macOS since 2026-09-01, and unrun like the rest of this file: no macOS test opens a window, and there is no Mac in this loop to click one.
- fn
inline void sonoreViewMouseUp(id self, SEL, id event)gfx::sonoreViewMouseUp · window_cocoa.h:1331 - fn
inline void sonoreViewMouseMoved(id self, SEL, id event)Dragged and moved are separate methods on macOS and both are needed. A view that handles only mouseMoved: loses every drag; one that handles only mouseDragged: never highlights on hover.gfx::sonoreViewMouseMoved · window_cocoa.h:1344 - fn
inline void sonoreViewMouseExited(id self, SEL, id)gfx::sonoreViewMouseExited · window_cocoa.h:1354 - fn
inline void sonoreViewScrollWheel(id self, SEL, id event)gfx::sonoreViewScrollWheel · window_cocoa.h:1359 - fn
inline BOOL sonoreViewAcceptsFirstResponder(id, SEL)So the view can be first responder at all. A view that answers NO to this never receives keyDown:, which is the quiet way a text field ends up unable to be typed into with nothing in the code looking wrong.gfx::sonoreViewAcceptsFirstResponder · window_cocoa.h:1397 - fn
inline KeyPress sonoreKeyPressOf(id event)A key.gfx::sonoreKeyPressOf · window_cocoa.h:1412More
Cocoa puts both halves on one event, in two properties. charactersIgnoringModifiers is the KEY -- what the physical key means before anything is held -- and characters is what the layout actually produced. A named key is read from the first; text from the second.
The named keys arrive as private-use code points (NSUpArrowFunctionKey and friends, 0xF700 upward) rather than as separate constants, which is the one genuinely odd thing about Cocoa's keyboard and the reason this switch is on a character value rather than a key code.
- fn
inline void sonorePassKeyOn(id self, id event)A key nobody here used goes on up the responder chain -- to the host's window, where the spacebar is transport. Swallowed, every host shortcut stopped working the moment the editor was first responder.gfx::sonorePassKeyOn · window_cocoa.h:1498 - fn
inline void sonoreViewKeyDown(id self, SEL, id event)gfx::sonoreViewKeyDown · window_cocoa.h:1503 - fn
inline void sonoreViewInsertText(id self, SEL, id text, cocoa::NSRange2 replacement)gfx::sonoreViewInsertText · window_cocoa.h:1539 - fn
inline void sonoreViewSetMarkedText(id self, SEL, id text, cocoa::NSRange2 selected, cocoa::NSRange2 replacement)gfx::sonoreViewSetMarkedText · window_cocoa.h:1544 - fn
inline void sonoreViewUnmarkText(id self, SEL)gfx::sonoreViewUnmarkText · window_cocoa.h:1551 - fn
inline cocoa::NSRange2 sonoreViewSelectedRange(id self, SEL)gfx::sonoreViewSelectedRange · window_cocoa.h:1555 - fn
inline cocoa::NSRange2 sonoreViewMarkedRange(id self, SEL)gfx::sonoreViewMarkedRange · window_cocoa.h:1560 - fn
inline BOOL sonoreViewHasMarkedText(id self, SEL)gfx::sonoreViewHasMarkedText · window_cocoa.h:1565 - fn
inline id sonoreViewAttributedSubstring(id self, SEL, cocoa::NSRange2 range, cocoa::NSRange2* actual)gfx::sonoreViewAttributedSubstring · window_cocoa.h:1570 - fn
inline id sonoreViewValidAttributes(id, SEL)None: the composition is drawn with the field's own underline, and an input method's attributes (clause segments, colours) would be read by nobody. An empty list is the documented way to say so.gfx::sonoreViewValidAttributes · window_cocoa.h:1587 - fn
inline cocoa::CGRect2 sonoreViewFirstRect(id self, SEL, cocoa::NSRange2 range, cocoa::NSRange2* actual)gfx::sonoreViewFirstRect · window_cocoa.h:1591 - fn
inline unsigned long sonoreViewCharacterIndex(id self, SEL, cocoa::CGPoint2 point)gfx::sonoreViewCharacterIndex · window_cocoa.h:1600 - fn
inline bool sonoreKeyPressForCommand(const char* name, KeyPress* key)An editing command from the key bindings: deleteBackward:, insertNewline:, moveLeft:, cancelOperation: and the rest.gfx::sonoreKeyPressForCommand · window_cocoa.h:1616More
During a keyDown: the command IS that key, and the key is delivered as the KeyPress it has always been -- so Return, Tab, Escape and the arrows behave exactly as they did before keys went through the input method, and a key the editor does not use still reaches the host. Outside one (an input method or a menu sending a command on its own), the commonest commands are named; the rest are ignored rather than guessed at.
- fn
inline void sonoreViewDoCommand(id self, SEL, SEL command)gfx::sonoreViewDoCommand · window_cocoa.h:1646 - fn
inline id sonoreViewAxChildren(id self, SEL)gfx::sonoreViewAxChildren · window_cocoa.h:1662 - fn
inline BOOL sonoreViewAxIsElement(id, SEL)gfx::sonoreViewAxIsElement · window_cocoa.h:1668 - fn
inline id sonoreViewAxRole(id, SEL)gfx::sonoreViewAxRole · window_cocoa.h:1670 - fn
inline id sonoreViewAxLabel(id self, SEL)gfx::sonoreViewAxLabel · window_cocoa.h:1672 - fn
inline id sonoreViewAxHitTest(id self, SEL, nsax::AxPoint point)gfx::sonoreViewAxHitTest · window_cocoa.h:1678 - fn
inline id sonoreViewAxFocused(id self, SEL)gfx::sonoreViewAxFocused · window_cocoa.h:1684 - fn
inline std::vector<std::string> sonoreDraggedFiles(id sender)The paths on a dragging pasteboard.gfx::sonoreDraggedFiles · window_cocoa.h:1698More
NSFilenamesPboardType is the old type and NSPasteboardTypeFileURL is the current one; both are still sent depending on what dragged the files. Reading the old one first is deliberate -- it gives an array of plain paths, where the new one gives URLs that each need converting.
- fn
inline unsigned long sonoreViewDraggingEntered(id self, SEL, id sender)NSDragOperationCopy(1) if anything will take them, NSDragOperationNone(0) otherwise. Cocoa asks on entry and on every move, like XDND and unlike WM_DROPFILES -- so this peer can light a target on hover.gfx::sonoreViewDraggingEntered · window_cocoa.h:1722 - fn
inline unsigned long sonoreViewDraggingUpdated(id self, SEL, id sender)gfx::sonoreViewDraggingUpdated · window_cocoa.h:1734 - fn
inline void sonoreViewDraggingExited(id self, SEL, id)gfx::sonoreViewDraggingExited · window_cocoa.h:1738 - fn
inline BOOL sonoreViewPerformDrag(id self, SEL, id sender)gfx::sonoreViewPerformDrag · window_cocoa.h:1742 - fn
inline void sonoreViewTabletProximity(id, SEL, id event)Which end of the pen is down -- Cocoa says so only here.gfx::sonoreViewTabletProximity · window_cocoa.h:1754 - fn
inline void sonoreViewTick(id self, SEL, id)gfx::sonoreViewTick · window_cocoa.h:1758 - fn
inline void sonoreViewResignedKey(id self, SEL, id)gfx::sonoreViewResignedKey · window_cocoa.h:1763 - fn
inline void sonoreViewFollowFrame(id self)The view's frame, in points, as its size now is.gfx::sonoreViewFollowFrame · window_cocoa.h:1769 - fn
inline void sonoreViewFrameChanged(id self, SEL, id)NSViewFrameDidChangeNotification: the host resized the view (autoresizing, or setFrame: from an AU host).gfx::sonoreViewFrameChanged · window_cocoa.h:1778 - fn
inline void sonoreViewBackingChanged(id self, SEL)The backing scale changed -- the window moved between a Retina display and one that is not. Nothing told the editor, which went on rendering at the old scale: blurred on the Retina side, twice the size on the other. NSView's own implementation does nothing, so there is no super to call.gfx::sonoreViewBackingChanged · window_cocoa.h:1784 - fn
inline BOOL sonoreMenuWindowCanBecomeKey(id, SEL)canBecomeKeyWindow for the menu window class: YES, so a borderless menu can be key and resign it -- its only way to learn of a click elsewhere.gfx::sonoreMenuWindowCanBecomeKey · window_cocoa.h:1788
Constants 2
- const
constexpr unsigned long kNSNotFound = ~0ul >> 1NSNotFound is NSIntegerMax: the largest SIGNED long, as an NSUInteger.gfx::cocoa::kNSNotFound · window_cocoa.h:156 - const
constexpr uint32_t kAlphaPremultipliedLast = 1ukCGImageAlphaPremultipliedLast | kCGBitmapByteOrderDefault. The rasteriser produces premultiplied RGBA, which is exactly this layout -- so unlike the Win32 and X11 peers, macOS needs NO channel swap at the blit. Stating the constant numerically rather than including CGImage.h keeps this file to the three runtime headers.gfx::cocoa::kAlphaPremultipliedLast · window_cocoa.h:243