appkit

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: BSD-3-Clause Imports: 5 Imported by: 0

README

appkit

Embed real AppKit controls — NSButton, NSTextField, NSSecureTextField, NSSlider and their kin — inside a host's own NSView, from pure Go with CGO_ENABLED=0.

import "github.com/go-macos/appkit"

pw, _ := appkit.NewSecureTextField("")
pw.OnAction(func() { unlock(pw.StringValue()) }) // fires on Return
pw.SetFrame(12, 12, 220, 24)
pw.AddTo(contentView) // an objc.ID for the view it should live in

Why this exists

It is the native counterpart to a pixel-drawn widget toolkit. A toolkit that paints its own buttons and fields into a framebuffer is portable and fast, but a small set of controls the operating system must own:

  • a secure text field the window server fills without the process ever seeing the keystrokes — a drawn password box cannot be that;
  • controls that carry the platform's exact focus ring, drag, and accessibility behaviour, which an imitation only approximates.

For those, this package hands the host a live AppKit object it can place where the toolkit laid out a region. The two compose: the toolkit does the layout, AppKit does the control.

The controls

Constructor AppKit class Value Action
NewButton NSButton (push) OnAction on click
NewLabel NSTextField (static) StringValue
NewTextField NSTextField StringValue OnAction on Return; OnChange per keystroke
NewSecureTextField NSSecureTextField StringValue as TextField
NewCheckbox NSButton (switch) Bool OnAction on toggle
NewRadioButton NSButton (radio) Bool OnAction; siblings in one superview group
NewSwitch NSSwitch Bool OnAction on toggle
NewSlider NSSlider Double OnChange while dragging
NewPopUpButton NSPopUpButton StringValue (selected title) OnAction on select
NewProgressIndicator NSProgressIndicator (bar) Double (in [min,max]) — (read-only)
NewSpinner NSProgressIndicator (spinning) Bool → start/stop animation
NewStepper NSStepper Double OnAction on step
NewSearchField NSSearchField StringValue OnAction on Return; OnChange per keystroke
NewComboBox NSComboBox (editable) StringValue (typed or picked) OnAction on Return/pick; OnChange per keystroke
NewSegmentedControl NSSegmentedControl StringValue (selected segment label) OnAction on select
NewTextView NSTextView (in NSScrollView) StringValue (multi-line) OnChange per edit
NewLinkButton NSButton (hyperlink) StringValue (title) OnAction on click
NewDatePicker NSDatePicker StringValue (ISO YYYY-MM-DD) OnAction on change
NewColorWell NSColorWell StringValue (#RRGGBB) OnAction/OnChange on change

Contract

  • Main thread only. Every method must be called on the thread that runs the AppKit event loop — where AppKit permits control creation and mutation, and where a windowing host already runs its layout and event loop. The action and change handlers are called back on that same thread.
  • It needs a host. A control is useful only once it is a subview of a view that is on screen in a running application. This package does not create the application, window, or event loop — those belong to the windowing library that owns the process (for example go-widgets/window).
  • Off macOS every constructor returns [ErrUnsupported] rather than being absent, so a consumer cross-compiles to every platform and finds out at run time, with one clean error, that this platform has no AppKit to embed.

How it binds

Every native call is an Objective-C message send over go-macos/objc (itself over ebitengine/purego), so the package links with no cgo and cross-compiles like any other Go code. One process-wide Objective-C target routes each control's action back to its Go handler by the control's tag — the same pattern go-macos/statusitem uses for menu rows.

Testing

The portable control model — kind and spec validation, the action registry, the closed-state bookkeeping — is 100% covered on Linux and on all six of Go's 64-bit architectures under qemu, through a fake control seam that needs no AppKit. On macOS a live suite builds the real controls and reads their values back through the actual selectors, skipping itself when the runner has no window server.

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package appkit embeds real AppKit controls — NSButton, NSTextField, NSSecureTextField, NSSlider and their kin — inside a host's own NSView, from pure Go with CGO_ENABLED=0.

It exists to be the NATIVE counterpart to a pixel-drawn widget toolkit. A toolkit that paints its own buttons and text fields into a framebuffer is portable and fast, but there is a small set of controls the operating system must own: a secure text field the window server fills without the process ever seeing the keystrokes, the system colour and font panels, a control that carries the platform's exact focus-ring, drag and accessibility behaviour. For those, a drawn imitation is not merely less faithful — it cannot be correct. This package hands the host a live AppKit object it can place where the toolkit laid out a region, so the two compose: the toolkit does the layout, AppKit does the control.

The shape of the API

A Control wraps one native control. The host creates it with a constructor (NewButton, NewSecureTextField, …), positions it with Control.SetFrame in the coordinate system of the superview it is added to with Control.AddTo, reads and writes its value (Control.StringValue, Control.SetBool, …), and is told when the person changes it (Control.OnAction, Control.OnChange). Every method must be called on the process main thread — where AppKit permits control creation and mutation, and where a windowing host already runs its layout and event loop. The action and change handlers are called back on that same thread.

It binds through go-macos/objc

Every native call is an Objective-C message send over github.com/go-macos/objc (itself over github.com/ebitengine/purego), so the package links with no cgo and cross-compiles like any other Go code. Off macOS every constructor reports ErrUnsupported rather than being absent, so a consumer builds and runs on every platform and finds out at run time — with one clean error — that this platform has no AppKit to embed.

It needs a running application and a host view

A control is only useful once it is a subview of a view that is on screen and whose application is running an event loop — otherwise it draws nowhere and no action ever fires. This package does not create the application, the window or the event loop; those belong to whoever owns the process (a windowing library such as github.com/go-widgets/window). A control created before there is an application is still a valid object; it simply shows and reacts to nothing until it is placed and the loop runs.

Index

Constants

This section is empty.

Variables

View Source
var ErrClosed = errors.New("appkit: control is closed")

ErrClosed is returned when a method is called on a control that has already been closed. A closed control's native object is gone; touching it would message freed memory.

View Source
var ErrUnsupported = errors.New("appkit: native controls are only available on macOS")

ErrUnsupported is returned by every constructor away from macOS: the controls this package embeds are AppKit's, and there is no AppKit to embed them in.

Functions

This section is empty.

Types

type Control

type Control struct {
	// contains filtered or unexported fields
}

Control is one live native AppKit control. Its zero value is not usable; get one from a constructor.

Every method must be called on the process main thread — the thread that runs the AppKit event loop — because AppKit permits control creation and mutation only there. A host such as github.com/go-widgets/window drives its layout and event handling on that thread already, which is where it places and updates these controls. Calling from another goroutine is a bug this package does not guard against; the action and change handlers, in turn, are invoked on the main thread because that is where AppKit delivers them.

func New

func New(spec Spec) (*Control, error)

New builds a control from a full Spec. The constructors below are thin wrappers over it and are what most callers want.

func NewButton

func NewButton(title string) (*Control, error)

NewButton makes a push button with the given title.

func NewCheckbox

func NewCheckbox(title string) (*Control, error)

NewCheckbox makes a labelled checkbox.

func NewColorWell added in v0.2.0

func NewColorWell() (*Control, error)

NewColorWell makes a colour well. Its value is Control.StringValue as a #RRGGBB hex string; Control.OnAction (and Control.OnChange) fire when the colour changes.

func NewComboBox added in v0.2.0

func NewComboBox(items []string) (*Control, error)

NewComboBox makes an editable combo box with the given drop-down items. The typed-or-picked text is Control.StringValue; Control.OnChange fires on a keystroke, Control.OnAction on Return or a pick.

func NewDatePicker added in v0.2.0

func NewDatePicker() (*Control, error)

NewDatePicker makes a date picker. Its value is Control.StringValue as an ISO-8601 YYYY-MM-DD string; Control.OnAction fires when the date changes.

func NewLabel

func NewLabel(text string) (*Control, error)

NewLabel makes a non-editable text label.

func NewLinkButton added in v0.2.0

func NewLinkButton(text string) (*Control, error)

NewLinkButton makes a hyperlink-styled button with the given title. Its title is Control.StringValue; Control.OnAction fires when it is clicked.

func NewPopUpButton

func NewPopUpButton(items []string) (*Control, error)

NewPopUpButton makes a pop-up list of the given items.

func NewProgressIndicator added in v0.2.0

func NewProgressIndicator(min, max float64) (*Control, error)

NewProgressIndicator makes a determinate progress bar over [min,max], starting at min. Its value is read and written with Control.Double; it has no action.

func NewRadioButton

func NewRadioButton(title string) (*Control, error)

NewRadioButton makes a radio button. Give a set of them the same superview and they behave as one group.

func NewSearchField added in v0.2.0

func NewSearchField(text string) (*Control, error)

NewSearchField makes a search field with the given initial text. Its text is Control.StringValue; Control.OnChange fires on every keystroke and Control.OnAction on Return.

func NewSecureTextField

func NewSecureTextField(text string) (*Control, error)

NewSecureTextField makes a secure (bulleted) text field. This is the control a drawn toolkit cannot substitute for a password box: the window server fills it without the process seeing the keystrokes.

func NewSegmentedControl added in v0.2.0

func NewSegmentedControl(items []string) (*Control, error)

NewSegmentedControl makes a segmented control with the given segment labels. The selected segment's label is Control.StringValue; Control.OnAction fires when the selection changes.

func NewSlider

func NewSlider(min, max, value float64) (*Control, error)

NewSlider makes a slider over [min,max] positioned at value.

func NewSpinner added in v0.2.0

func NewSpinner() (*Control, error)

NewSpinner makes an indeterminate spinning progress indicator. Control.SetBool(true) starts its animation and (false) stops it.

func NewStepper added in v0.2.0

func NewStepper(min, max, value float64) (*Control, error)

NewStepper makes a stepper over [min,max] starting at value. Its value is Control.Double; Control.OnAction fires on each step.

func NewSwitch

func NewSwitch() (*Control, error)

NewSwitch makes an on/off switch (NSSwitch, macOS 10.15+).

func NewTableView added in v0.2.0

func NewTableView(items []string) (*Control, error)

NewTableView makes a list of items: an NSTableView of one text column inside an NSScrollView, with the system's scrolling, keyboard navigation and accessibility.

The chosen row is Control.Double as a zero-based index, -1 when none is; OnChange fires when it moves. Control.SetItems replaces the rows.

func NewTextField

func NewTextField(text string) (*Control, error)

NewTextField makes an editable text field with the given initial text.

func NewTextView added in v0.2.0

func NewTextView(text string) (*Control, error)

NewTextView makes a multi-line, editable text view with the given initial text. Its text is Control.StringValue; Control.OnChange fires as it is edited.

func (*Control) AddTo

func (c *Control) AddTo(parent objc.ID) error

AddTo makes the control a subview of parent. Adding it to a view that is on screen is what makes it appear; a control is never visible on its own.

func (*Control) Bool

func (c *Control) Bool() bool

Bool reads the on/off state of a Checkbox or Switch. On a closed control it returns false.

func (*Control) Close

func (c *Control) Close()

Close removes the control from its superview and releases the native object. After Close the control is inert: mutating methods return ErrClosed and readers return their zero value. Closing twice is safe.

func (*Control) Double

func (c *Control) Double() float64

Double reads a slider's position. On a closed control it returns 0.

func (*Control) Kind

func (c *Control) Kind() Kind

Kind reports which control this is.

func (*Control) OnAction

func (c *Control) OnAction(fn func())

OnAction registers the handler called when the control fires its primary action: a button clicked, a checkbox or switch toggled, a pop-up selection made, or editing ended in a text field. The handler runs on the main thread. Passing nil clears it.

func (*Control) OnChange

func (c *Control) OnChange(fn func())

OnChange registers the handler called as a control's value changes continuously: every keystroke in a text field, every step of a dragged slider. The handler runs on the main thread. Passing nil clears it.

func (*Control) Remove

func (c *Control) Remove() error

Remove takes the control out of its superview without closing it, so it can be added elsewhere. To dispose of it for good, use Control.Close.

func (*Control) SetBool

func (c *Control) SetBool(on bool) error

SetBool sets the on/off state of a Checkbox or Switch.

func (*Control) SetDouble

func (c *Control) SetDouble(v float64) error

SetDouble sets a slider's position (clamped to its range).

func (*Control) SetFrame

func (c *Control) SetFrame(x, y, w, h float64) error

SetFrame positions and sizes the control in the coordinate system of the view it is (or will be) added to. The host converts from its own layout space; a control's frame means nothing until it has a superview.

func (*Control) SetHidden

func (c *Control) SetHidden(hidden bool) error

SetHidden shows or hides the control in place, keeping its frame and its place in the view tree.

func (*Control) SetImage added in v0.4.0

func (c *Control) SetImage(png []byte) error

SetImage puts a picture on a control -- a button, in practice -- from PNG (or any other format NSImage reads) bytes.

A toolbar is icons. Transmission's is eleven of them and not one word, and it is legible at a glance because a picture of a pause sign is read faster than the word "pause". A button that can only carry a title cannot make one.

Passing no bytes removes the image and leaves the title.

func (*Control) SetImageOnly added in v0.4.0

func (c *Control) SetImageOnly(only bool) error

SetImageOnly says the control shows its picture and not its title.

The title stays SET even so: it is what a screen reader announces and what the tooltip shows, so an icon-only button that dropped its title would be a button nobody using assistive technology could name.

func (*Control) SetItems added in v0.2.0

func (c *Control) SetItems(items []string) error

SetItems replaces the rows of a TableView (or the entries of a PopUpButton or ComboBox), and reloads it.

A list whose contents are fixed at creation is not a list anybody needs: the queue this was built for gains and loses entries while the window is open.

func (*Control) SetMenu added in v0.3.0

func (c *Control) SetMenu(items []MenuItem) error

SetMenu gives a control the menu it shows on a right-click (or a Control click, or the trackpad's secondary gesture -- the system decides, which is the point of asking it).

Buttons along the bottom of a window are a dialogue's shape: a fixed row of verbs that must all fit, all the time, whether or not any of them applies to what is selected. A context menu is the other shape -- the verbs that apply to THIS row, where the row is, named in full rather than abbreviated to fit.

Passing no items removes the menu.

func (*Control) SetStringValue

func (c *Control) SetStringValue(s string) error

SetStringValue replaces a text control's contents (Label, TextField, SecureTextField), or a pop-up's selected title.

func (*Control) StringValue

func (c *Control) StringValue() string

StringValue reads a text control's contents, or a pop-up's selected title. On a closed control it returns "".

type Kind

type Kind int

Kind is which native control a Control wraps. It is fixed at construction: AppKit has no single control that becomes a button or a slider after the fact, so neither does this package.

const (
	// Button is an NSButton with a push-button style — a momentary control that
	// fires its action when clicked.
	Button Kind = iota
	// Label is a non-editable, non-selectable NSTextField: text the host places
	// but the person cannot change. It has no action.
	Label
	// TextField is an editable NSTextField. Its action fires when editing ends
	// (Return, or focus leaving the field); OnChange fires on every keystroke.
	TextField
	// SecureTextField is an NSSecureTextField: an editable field whose glyphs
	// are bullets and whose contents the window server fills without this
	// process seeing the keystrokes. This is the control that cannot be drawn.
	SecureTextField
	// Checkbox is an NSButton with a switch (checkbox) style: an on/off control
	// with a label. Its state is [Control.Bool].
	Checkbox
	// RadioButton is an NSButton with a radio style. AppKit groups radio buttons
	// that share a superview and action, so that selecting one clears its
	// siblings; give a group of RadioButtons the same parent to get that.
	RadioButton
	// Switch is an NSSwitch, the sliding on/off control introduced in macOS
	// 10.15. Its state is [Control.Bool].
	Switch
	// Slider is an NSSlider over a [Spec.Min],[Spec.Max] range. Its value is
	// [Control.Double]; OnChange fires as it is dragged.
	Slider
	// PopUpButton is an NSPopUpButton: a pull-down list of [Spec.Items]. The
	// selected title is [Control.StringValue]; its action fires on selection.
	PopUpButton

	// ProgressIndicator is a determinate NSProgressIndicator (a bar). Its value
	// is [Control.Double], clamped to the [Spec.Min],[Spec.Max] range fixed at
	// construction. It is read-only: it has no action, because a progress bar
	// reports, it is not operated.
	ProgressIndicator
	// Spinner is an indeterminate, spinning NSProgressIndicator. It carries no
	// value; [Control.SetBool] starts (true) and stops (false) its animation.
	Spinner
	// Stepper is an NSStepper over [Spec.Min],[Spec.Max] starting at
	// [Spec.Value]. Its value is [Control.Double]; its action fires on each step.
	Stepper
	// SearchField is an NSSearchField: an editable field styled for search. Its
	// text is [Control.StringValue]; OnChange fires on every keystroke and its
	// action fires on Return.
	SearchField
	// ComboBox is an editable NSComboBox: a text field with a drop-down list of
	// [Spec.Items]. The typed-or-picked text is [Control.StringValue]; OnChange
	// fires on a keystroke, its action on Return or a pick.
	ComboBox
	// SegmentedControl is an NSSegmentedControl over [Spec.Items]. The selected
	// segment's label is [Control.StringValue] (the binding maps label to index);
	// its action fires when the selection changes.
	SegmentedControl
	// TextView is a multi-line, editable NSTextView (inside an NSScrollView). Its
	// text is [Control.StringValue]; OnChange fires as it is edited. It has no
	// action.
	TextView
	// LinkButton is an NSButton styled as a hyperlink. Its title is
	// [Control.StringValue]; its action fires when it is clicked, which is where
	// a host opens the link.
	LinkButton
	// DatePicker is an NSDatePicker. Its value is [Control.StringValue] as an
	// ISO-8601 YYYY-MM-DD date string (the binding parses and formats at the
	// boundary); its action fires when the date changes.
	DatePicker
	// ColorWell is an NSColorWell. Its value is [Control.StringValue] as a
	// #RRGGBB hex string (the binding converts to and from NSColor); its action
	// fires when the colour changes.
	ColorWell
	// TableView is an NSTableView of one text column inside an NSScrollView:
	// a list of [Spec.Items] a person picks a row from, with the system's own
	// scrolling, keyboard navigation and accessibility.
	//
	// The selected row is [Control.Double] as a zero-based index, and -1 when
	// nothing is selected; OnChange fires when the selection moves. The rows
	// themselves are replaced with [Control.SetItems], because a list whose
	// contents never change is not a list anybody needs.
	//
	// It is the one kind here that needs a DATA SOURCE rather than a value:
	// AppKit asks how many rows there are and what is in each, so this package
	// answers those two questions from the items it was given.
	TableView
)

func (Kind) String

func (k Kind) String() string

String names the kind for error messages and logs.

type MenuItem struct {
	Title string
	// OnPick runs when the item is chosen. Nil makes the item inert, which is
	// how a menu shows a verb that does not apply right now rather than hiding
	// it and moving everything else.
	OnPick func()
}

MenuItem is one line of a control's context menu.

An empty Title is a separator: a menu is a list of verbs with rules between the groups, and giving the rule its own type would make every caller name something that has no name.

type Spec

type Spec struct {
	Kind Kind

	// Title is the label of a Button, Checkbox or RadioButton, the initial text
	// of a Label, TextField, SecureTextField, SearchField or TextView, and the
	// title of a LinkButton. It is ignored for the valueless controls (Switch,
	// Slider, PopUpButton, and the rest).
	Title string

	// Items are the entries of a PopUpButton or ComboBox drop-down, or the
	// segments of a SegmentedControl, in order. A PopUpButton or SegmentedControl
	// with no items is refused: neither can be operated empty. A ComboBox may be
	// empty — it is still a typeable text field.
	Items []string

	// Min, Max and Value are the bounds and initial position of a Slider or
	// Stepper, and the bounds of a ProgressIndicator (whose initial value is
	// Min). Min must be < Max. Value is clamped into the range. Ignored
	// otherwise.
	Min, Max, Value float64
}

Spec describes a control to build. Most callers reach for a constructor (NewButton, NewSlider, …) rather than filling this in by hand; it is exported so a host that maps a laid-out region to a control kind at run time can build one from data.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL