Building the app
Windows
Creating and arranging windows of both kinds, their events, and what windows showing web pages do with them: navigation, downloads, permissions, printing.
A window is a native window that shows a web page, loaded from its URL,
or native UI that MyGo draws, from its Content. Create windows
with mygo.NewWindow once the app is ready:
mygo.App.WhenReady(func() {
win := mygo.NewWindow(mygo.WindowOptions{
Title: "Editor",
URL: "/",
Width: 1200,
Height: 800,
MinWidth: 600,
MinHeight: 400,
StateKey: "editor",
})
win.OnClosed(func() { log.Println("closed") })
})A window of native UI takes Content in place of URL:
mygo.NewWindow(mygo.WindowOptions{Title: "Inspector", Content: ui.View(app.view)})Everything in this guide applies to windows of both kinds, apart from pages, which only windows showing web pages have, and the options about them. Like every MyGo API, windows can be created and changed from any goroutine; calls hop to the main thread, which drives the UI.
Options
The zero WindowOptions is a visible, resizable window of 800×600 centered
on the screen. Sizes and positions are in device-independent pixels.
| Option | |
|---|---|
Title |
the window title; defaults to the app name, and a page's <title> replaces it |
URL |
loaded once the window exists; / and other URLs without a scheme are pages of the frontend |
Content |
shows native UI that MyGo draws, instead of a page |
Width, Height |
the size of the window, or of what it shows with UseContentSize |
X, Y |
the top-left corner; the window is centered when both are 0 |
MinWidth, MinHeight, MaxWidth, MaxHeight |
limits of resizing; 0 means none |
FitContent |
sizes the page area to its page's content, and follows it, within the limits above, as a flyout's |
StateKey |
remembers the window's bounds and state under this key, see below |
Hidden |
creates the window without showing it |
Maximized, FullScreen |
creates it maximized or in full screen |
Frameless |
no title bar and borders: the page draws them, see custom title bars, or the native UI |
TitleBarStyle |
hides the title bar but keeps the window buttons over the page, see custom title bars, or over native UI |
TrafficLightPosition |
moves the window buttons of a hidden title bar (macOS) |
TitleBarHeight |
the height of the title bar the page draws under the window buttons (Linux, Windows) |
HideWindowButtons |
hides the window buttons of a hidden title bar, which keeps its resizing top edge, shadow and corners: the page draws its own and calls Minimize, Maximize, Unmaximize and Close |
Transparent, Vibrancy |
a transparent window, and the material behind a transparent page or native UI (macOS, Windows 11 22H2): VibrancyGlass and VibrancyGlassClear are macOS 26's Liquid Glass. On Windows, a transparent window of native UI shows what is behind it where it draws nothing, and a window with a material has no menu bar: Alt and F10 open its menus in a popup |
VibrancyTint |
tints Liquid Glass, in CSS syntax such as "rgba(0, 122, 255, 0.3)" or "light-dark(...)"; other materials and platforms ignore it (macOS 26). SetVibrancyTint changes it later |
CornerRadius |
rounds a frameless window's corners: its content and material are cut to them on macOS, and Windows 11 gives it its own rounded corners |
BackgroundColor |
fills the window until the page paints, in CSS syntax such as "#1e1e1e", or "light-dark(#f5f5f7, #1e1e1e)" to follow the appearance |
Opacity |
between 0 and 1 |
DisableResize, DisableMove, DisableMinimize, DisableMaximize, DisableClose, DisableFullScreen, DisableShadow |
take abilities away. On Linux under Wayland, DisableShadow keeps a frameless window or a hidden title bar without the shadow and border GTK draws around them |
AlwaysOnTop |
keeps the window above others |
SkipTaskbar |
leaves the window out of the taskbar (Linux, Windows) |
AutoHideMenuBar |
shows the menu bar only while the keyboard is in it, from Alt or F10 (Linux, Windows), see the menu bar |
Parent, Modal |
a child window, modal to its parent, a flyout too; for one placed next to a part of it, see flyouts |
Panel |
the window of a launcher or a quick entry, floating above all apps, see panels |
Page |
the options of the window's page: its preload script, trusted origins, web inspector, zoom, user agent and its suffix, swipes and pinches |
Show windows without flashing
A window showing a web page shows its BackgroundColor, white by
default, until the page paints, so make it the page's background. When the page follows dark mode,
give both colors with light-dark(<light>, <dark>): the window switches
with the appearance. To show windows only once their page is ready, create
them hidden and show them from OnReadyToShow:
win := mygo.NewWindow(mygo.WindowOptions{URL: "/", Hidden: true, BackgroundColor: "#1e1e1e"})
win.OnReadyToShow(win.Show)Windows of native UI need neither: they have no page to load, and show
with their first frame (on Windows, the window stays out of sight until
it drew it). One created Hidden is ready to show once it drew its first
frame, which it draws while hidden, so OnReadyToShow(win.Show) shows it
as it does a page's.
The view of native UI first runs once NewWindow returned, so it can use
the window NewWindow returns:
app.win = mygo.NewWindow(mygo.WindowOptions{Content: ui.View(func(c *ui.Context) {
ui.Text(c, app.win.Title()) // never nil
})})Called on the main thread, as in App.WhenReady or an event handler,
NewWindow leaves showing the window to the main loop's next turn, when
the window draws its first frame, or to the first method that acts on it,
such as IsVisible or Maximize, which finds it shown. OnShow added
right after NewWindow hears that show, and Hide keeps it from
happening. Called from another goroutine, NewWindow shows the window and
returns while the main thread goes on drawing it: guard the variable
there, or set it with Window.Update.
Remember where windows were
With a StateKey, a window remembers its position, size, and maximized and
full screen state in window-state.json in the user data directory, which
is written when the window closes and when the app quits. The next window
created with the same key, at the next launch for example, gets them back,
as long as it would show on a connected display. They override X, Y,
Width, Height, UseContentSize, Maximized and FullScreen, which
stay the defaults of the first launch. Give each kind of window its own key:
"main", "preferences".
On Windows, a state also keeps the window's bounds in the screen's pixels
(PixelX, PixelY, PixelWidth, PixelHeight): where displays have
different scales, its position in DIPs changes with the scales, and
earlier versions' cannot tell the displays apart.
An app that keeps its data elsewhere, such as in its own database, keeps
the states there with a WindowStateStore, set before the first window
with a StateKey, and no window-state.json is written. It keeps every
field of a WindowState, as JSON does: on Windows, a window whose state
lost its pixels is centered when the displays have different scales.
type windowStates struct{ db *sql.DB }
func (s windowStates) LoadWindowStates() (map[string]mygo.WindowState, error) {
var data []byte
err := s.db.QueryRow(`SELECT value FROM settings WHERE key = 'windows'`).Scan(&data)
if err != nil {
return nil, err
}
var states map[string]mygo.WindowState
return states, json.Unmarshal(data, &states)
}
func (s windowStates) SaveWindowStates(states map[string]mygo.WindowState) error {
data, err := json.Marshal(states)
if err != nil {
return err
}
_, err = s.db.Exec(`INSERT OR REPLACE INTO settings (key, value) VALUES ('windows', ?)`, data)
return err
}
mygo.App.SetWindowStateStore(windowStates{db})MyGo loads the states once, when the first window with a StateKey is
created, and saves them all when such a window closes and when the app
quits, on the main thread. Windows open with their options when there are
no states or loading them fails; a save that fails is tried again the next
time.
In a quit, the states are saved after the OnBeforeQuit listeners have
run, as each window closes, and once more after the event loop has
stopped, right before the OnQuit listeners run. A "Reset settings"
action that removed window-state.json, or the states from its own store,
would see them written again. App.ClearWindowStates() forgets them for
good instead: it saves no states, an empty map for a WindowStateStore,
and the windows save theirs no more until the app exits. The windows open
keep their bounds, and those created afterwards, as at the next launch,
get their options. It can be called at any time, from a quit listener
too:
mygo.App.OnBeforeQuit(func(*mygo.QuitEvent) {
if resetRequested {
_ = mygo.App.ClearWindowStates()
}
})Bring a page back
A window's position comes back with its StateKey; its page comes back
with its session state: Page.SessionState saves the back/forward list,
with the scroll positions and form fields the engine saved, and
PageOptions.SessionState brings it back in a new window or web view, in
place of its URL, Back included. An app keeps the state where it keeps
its data, to bring the page back at the next launch, or to close a page it
isn't showing and bring it back later, as a browser's sleeping tabs:
win.OnClose(func(e *mygo.CloseEvent) {
if state, err := win.Page().SessionState(); err == nil {
os.WriteFile(sessionFile, state, 0o600)
}
})
// At the next launch:
state, _ := os.ReadFile(sessionFile)
win := mygo.NewWindow(mygo.WindowOptions{StateKey: "main", URL: "/", Page: mygo.PageOptions{SessionState: state}})The state is opaque bytes in a format MyGo versions: the page's URL and the
engine's own state. A state the platform can't read, saved on another
platform or by a web engine that encodes it otherwise, brings back the page
it was at alone, and one that is no state at all (a missing file) loads
URL. It may hold what the user typed into forms: keep it as the user's
data. The HTML that LoadHTML loaded is not in it.
WebKit saves the scroll positions and form fields as it goes: the scroll position of the page it shows a moment after the user stops scrolling (WebKitGTK 2.38 and Safari 16's WebKit or later), its form fields only as it goes out of view (its window hidden, for one), and the other entries' scroll positions as the page leaves them. A state saved since has them, so a page saved while it shows comes back at its scroll position, but with the form fields it had when it last went out of view.
| Platform | Session state |
|---|---|
| macOS | WKWebView's interactionState, macOS 12 or later; the page's URL alone before |
| Linux | webkit_web_view_get_session_state, serialized, and webkit_web_view_restore_session_state |
| Windows | the page's URL alone: WebView2 has no API for the back/forward list |
Size, position and state
win.SetSize(1024, 768)
win.SetContentSize(1024, 700) // the page area
win.SetPosition(100, 100)
win.SetBounds(mygo.Rectangle{X: 100, Y: 100, Width: 1024, Height: 768})
win.Center()
win.Maximize() // Unmaximize, ToggleMaximize, IsMaximized
win.Minimize() // Restore, IsMinimized
win.SetFullScreen(true) // ToggleFullScreen, IsFullScreen
win.Hide() // Show, ShowInactive (without focusing), IsVisible
win.Focus() // Blur, IsFocusedIsVisible tells whether Show showed the window and Hide did not hide
it since: a minimized window is still visible, on every platform, as is one
covered by others or on another desktop. IsMinimized tells that it is
minimized, and in a window of native UI,
Context.WindowHidden whether any of it shows on screen.
Sizes and positions are in the window's DIPs, whatever the zoom of its
page (Page.SetZoomFactor) or of its native UI:
at a zoom of 1.25, a page area of 1000 DIPs shows 800 of the page's CSS
pixels, or of the view's DIPs. Bounds, ContentBounds, Size,
ContentSize and Position read them back;
mygo.Screen tells where the displays are.
Bounds are the whole window's, title bar and borders included (on Linux
X11, the window manager's frame), and ContentBounds those of the area the
page or native UI shows in, below a menu bar. On Linux under Wayland the
compositor decides where windows go and ignores the positions apps set,
and its decorations are not part of Bounds.
Other properties:
-
SetTitle,SetResizable,SetMovable,SetMinimizable,SetMaximizable,SetClosable,SetMinimumSize,SetMaximumSize,SetAlwaysOnTop,SetOpacity,SetHasShadow,SetBackgroundColorandSetVibrancychange what the options set. On Windows,SetVibrancychanges the material of a window created with one, and shows none in other windows. On Linux, a window keeps the shadow it was created with (DisableShadow). -
SetIcon(png)gives the window its own icon in its title bar and taskbar button (Linux, Windows). -
SetSkipTaskbarhides the window from the taskbar (Linux, Windows). -
SetAutoHideMenuBarhides the menu bar until Alt or F10, or shows it for good again (Linux, Windows). -
SetVisibleOnAllWorkspacesshows it on every Space or virtual desktop (macOS, Linux); on macOS also over other apps in full screen, as an overlay does, even withDisableFullScreen, if the app is an accessory (ActivationPolicyAccessory,App.Dock.Hide): macOS shows no windows of regular apps there, but panels. -
SetContentProtection(true)keeps it out of screenshots and screen recordings. -
SetIgnoreMouseEvents(true)lets clicks through to what is behind it, andSetInputRegionall of them but those on parts of it (see click-through windows). -
SetActiveAppearance(true)keeps the window drawn as active while another app is in front (macOS), so that the clear Liquid Glass (VibrancyGlassClear) of a panel floating over it stays clear. It uses a private AppKit call, which a later macOS may drop: apps for the Mac App Store should leave it off. Its material stays active then as well, and its pages and native UI too, on every platform (see inactive windows). -
SetWindowButtonsTheme(mygo.ThemeDark)draws the window buttons of a hidden title bar for a dark background in a light app, as over a dark view at the top of the page, andThemeLightfor a light one;ThemeSystemfollows the app again. The page and the menus keep the app's appearance (Windows; the buttons of macOS and Linux keep it). -
SetProgressBarshows the progress of a task on the taskbar button (Windows), the Dock icon (macOS) or the launcher entry (Linux docks that support it):win.SetProgressBar(mygo.ProgressBar{Value: 0.4}) win.SetProgressBar(mygo.ProgressBar{State: mygo.ProgressIndeterminate}) win.SetProgressBar(mygo.ProgressBar{}) // done -
FlashFrame(true)asks for attention: the taskbar button flashes (Windows), the window is marked urgent (Linux) or the Dock icon bounces (macOS).
Active and inactive windows
A window is drawn as active while the user's input goes to it: while it, or one of its flyouts, has the keyboard. Native apps draw the others apart so that users tell at a glance which window has it, and MyGo's do too:
- The material (
Vibrancy) of a window fades to an opaque gray on macOS while it is inactive, as AppKit's sidebars do, and Windows 11 draws its Mica or Acrylic as a solid fill. Panels and flyouts keep theirs active, floating over whatever app is in front, as does a windowSetActiveAppearance(true)keeps active. - Pages get the
data-mygo-window-inactiveattribute on<html>while their window is inactive, for CSS, andwindowActive()ofmygo-runtime(see inactive windows). - Native UI reads
c.WindowActive(), and its built-in widgets fade their accent to gray (see native UI).
A window's focusable flyout taking the keyboard leaves the window active:
on Windows its title bar and Mica stay active too. A window that never had
the keyboard counts as active, so that the first frame of a window about
to take it is drawn active; one shown with ShowInactive is not. Panels,
and flyouts without a parent, are always active.
Child and modal windows
A window with a Parent stays above it and closes with it. It can go to
any display, and stays where it is as its parent moves, on every platform.
On macOS it is ordered above its parent whenever the parent comes in
front, stays above a parent that is AlwaysOnTop, goes with it into
full screen, and hides while the parent is minimized or hidden. With Modal,
it also blocks its parent until it closes, like a dialog:
prefs := mygo.NewWindow(mygo.WindowOptions{
URL: "/preferences",
Parent: win,
Modal: true,
Width: 500,
Height: 400,
})On macOS a modal window is a sheet of its parent. Its parent may be a flyout too: see a dialog over a flyout.
Flyouts
A flyout is a window without a frame, a background or a taskbar button, owned by its parent and placed next to a rectangle of it, its anchor: the list of a combo box, a hover card, a popover with a form. Unlike an overlay of native UI or a popover of the page, it may extend beyond the parent's edges, and the work area of the display decides where it goes, not the parent's content. Its page or native UI draws all of it, its background included:
fl := mygo.NewFlyout(mygo.FlyoutOptions{
Parent: win,
Anchor: mygo.Rectangle{X: 20, Y: 40, Width: 120, Height: 28}, // in win's content area
Placement: mygo.PlacementBottomStart,
Gap: 4,
Width: 240,
Height: 300,
Content: ui.View(app.suggestions),
})The anchor is in DIPs relative to the parent's content area, which is
what ui.Element.Bounds gives for the element that opens the flyout; a
rectangle of zero size anchors it to a point. Placement names the side
of the anchor the flyout goes to and how it lines up with it along that
side, as "bottom-start", "right" or "top-end". Where the work area
has no room for it there, the flyout flips to the other side, or the other
end, if that has room, then slides back into the work area, then shrinks
to fit it, as GTK places menus and Wayland compositors place popups. It
stays above its parent, follows it as it moves and closes with it.
SetAnchor places it against another rectangle, and SetSize resizes it
where it goes; IsFlyout tells a flyout from other windows.
With Focusable, a flyout takes the keyboard as it shows and closes as a
menu does when the user presses outside it, another window takes the
keyboard or the app is deactivated. OnClose listeners hear it and may
keep it open, except on Wayland, whose compositor dismisses it. On Linux
and Windows the press only closes the flyout, as it closes their menus;
on macOS it goes on to what is under the pointer, but on the anchor, as
AppKit's popovers. So a click on the button that opened it closes it, and
does not open it again. Menus, lists to choose from and popovers are
focusable, and handle their own keys: the arrows, Enter.
Without it, a flyout never takes the keyboard, which stays in the parent, and it stays until it is closed: a tooltip, a hover card, or the suggestions of a text field in the parent, whose view drives them and closes them as it sees fit, as when the field loses the keyboard. (On Wayland, only a popup that takes the keyboard hears of presses outside it.)
Either closes, as the user closing it, on an Escape its content leaves:
one no element or shortcut of native UI takes, or no handler of the page
cancels with preventDefault(), as the web's popovers.
Shadow gives it the system's window shadow on macOS, where it follows
the shape of what the flyout draws and outlines it with a hairline (so
draw no border of your own there), and on Windows, where Windows 11 rounds
the flyout's corners as it rounds its menus and outlines it (so draw
neither a border nor rounded corners there).
On macOS, Popover gives it the look of the system's popovers instead: it
shows in an NSPopover, which draws its material, rounded corners and an
arrow pointing at the middle of the anchor, and animates. The material and
arrow follow the application's theme, including changes while open; a tray's
menu-bar contrast appearance does not override them. AppKit places it
on the side Placement names, or the opposite one; the rest of the
placement, and Gap, do not apply. As AppKit's popovers do, it has the
keyboard while it shows, and with Focusable it closes as the user clicks
elsewhere. Its content draws no background over the material: in native
UI, c.Vibrancy() reports it, so a view draws its own panel elsewhere:
func (app *App) about(c *ui.Context) {
c.Root().Background(ui.Transparent)
box := ui.Column(c).Fill() // over the popover's material
if !c.Vibrancy() {
box = ui.Column(c).Fill().Background(c.Theme().Surface).Radius(10)
}
box.Padding(12).Children(func() { ui.Text(c, "MyGo") })
}A flyout can hang from a tray icon instead of a window, as the panel of
a menu bar app: give it the Tray in place of a Parent. It goes next to
the icon, on the side Placement names, and floats over the windows of
other apps. Below the macOS menu bar, PlacementBottom centers it under
the icon (with Popover, the arrow of the system's popover points at
it); on Windows, the same placement flips above an icon of a taskbar at
the bottom of the screen. On macOS, which shows a menu bar item on each
display, it goes under the copy of the icon the user clicked. A press on
the icon does not dismiss a focusable flyout, so the icon's click can
toggle it; with Popover, the click closes it, as AppKit's popovers
close on their anchor, and does not reach OnClick. Either way, a click
on the icon closes the open flyout with the code below, and does not
open it again. Keep the app running as the flyout, its only window,
closes:
mygo.App.OnWindowAllClosed(func() {})
tray.OnClick(func() {
if panel != nil {
panel.Close()
return
}
panel = mygo.NewFlyout(mygo.FlyoutOptions{
Tray: tray,
Placement: mygo.PlacementBottom,
Width: 280,
Height: 230,
Focusable: true,
Popover: true,
Content: ui.View(app.panel),
})
panel.OnClosed(func() { panel = nil })
})Linux's tray icons tell neither clicks nor where they are: open the flyout
from a menu item of the icon there, and it shows in the middle of the
primary display's work area. examples/menubar-flyout is such an app.
A dialog over a flyout
A dialog of native UI draws in its window, so a flyout's is
cut to the flyout, and a tray's flyout may have no other window that shows.
Ask from a window modal to the flyout instead: give it the flyout as its
Parent, with Modal. Without X and Y, it is centered over the
flyout, moved into the work area of its display, and it stays above the
flyout, over other apps' windows too for a tray's. The flyout takes no
input while it shows, and stays open wherever the keyboard goes: a
focusable flyout gets the keyboard back as the window closes, and from
then on closes as it loses it again. Closing the flyout, as a click on
the tray icon may, closes the window.
if ui.Button(c, "Quit…").Clicked() {
mygo.NewWindow(mygo.WindowOptions{
Title: "Quit?",
Parent: panel,
Modal: true,
Width: 360,
Height: 140,
UseContentSize: true,
DisableResize: true,
Content: ui.View(app.confirmQuit),
})
}On macOS the window is no sheet of the flyout, which a sheet would hang
from, but a window attached to it, at its level. On Wayland, where a
flyout with a Parent is a popup, the compositor decides whether the
popup stays as the window takes the keyboard; a tray's flyout is a
window there, and stays.
Size a flyout to its content
With FitContent, a flyout takes the size of its page's content and
follows it as it changes, as browsers size the popups of their
extensions: a menu bar popover with a list, a hover card, a launcher's
results. Width and Height are its size only until the page tells its
own, which it does before its DOM is ready, so a flyout created Hidden
and shown from OnReadyToShow shows at its size. MinWidth, MinHeight,
MaxWidth and MaxHeight bound it, and the work area, as any flyout:
fl := mygo.NewFlyout(mygo.FlyoutOptions{
Tray: tray,
Placement: mygo.PlacementBottom,
FitContent: true,
MaxWidth: 400,
MaxHeight: 600,
URL: "/panel",
})MyGo's runtime in the page lays its root element (html) out at the
width of its content (width: max-content), within the limits, so that
its text wraps at MaxWidth, and watches its size with a
ResizeObserver: its height is its content's, the body's margins
included, beyond MaxHeight the page scrolls, and the room its
scrollbar takes then is part of the size. A page that gives html a size of its own,
as height: 100%, keeps the flyout at that size: let the root and the
body take their content's. The size is the page's CSS pixels times its
zoom, in DIPs, and a size that changes nothing resizes nothing.
A flyout of native UI (Content) fits the same way, a tray panel whose
list of nodes, form or error line changes as the app runs, with no size
to guess and no SetSize to call:
fl := mygo.NewFlyout(mygo.FlyoutOptions{
Tray: tray,
Placement: mygo.PlacementBottom,
FitContent: true,
MinWidth: 260,
MaxHeight: 480,
Content: ui.View(func(c *ui.Context) {
ui.Column(c).Fill().Padding(12).Gap(8).Children(func() {
ui.Text(c, "Nodes")
ui.Scroll(c).Grow(1).Children(func() { app.nodes(c) })
if app.err != nil {
ui.Text(c, app.err.Error())
}
})
}),
})Each frame measures its root's content as it lays it out, as CSS sizes
width: fit-content; height: auto: its widest line of children
(max-content), within MinWidth and MaxWidth and the work area, and
its height at that width, within MinHeight and MaxHeight, beyond
which a Scroll in it scrolls: Fill and Grow take the flyout's size
as it lays out, and measure as their content. Elements placed Absolute, as menus and
toasts, take none of it. A frame that measures another size resizes the
flyout, placed again against its anchor (flipping where the work area
has no room), and the next frame lays the content out at that size: the
first frame after a change shows it at the size the flyout had. A
hidden flyout draws no frames: content changed while it was hidden
(with Update or Invalidate) lays out as Show or ShowInactive
shows it, and the flyout shows at the size that content takes. A frame
that measures what the last did resizes nothing, so a size SetSize
gives holds until the content's changes. A view that builds its content
after the window's size (Context.Size) settles at the size it took
rather than resize back and forth.
WindowOptions.FitContent does the same for the page area of a window,
a panel for one, within its MinWidth, MinHeight,
MaxWidth and MaxHeight and the work area of its display. It follows
the content only when the content's size changes, and only in the
dimension that changed, so a size the user gives a resizable window holds
until then, and not while the window is maximized or in full screen.
How each platform shows them:
| macOS | Linux | Windows | |
|---|---|---|---|
| window | a borderless, nonactivating NSPanel, a child window of its parent while it shows, or at the level of menus for a tray |
a GTK_WINDOW_POPUP: override-redirect on X11, an xdg_popup of its parent on Wayland; for a tray, an undecorated utility window kept above |
an owned WS_POPUP, WS_EX_TOOLWINDOW, without a redirection bitmap; topmost and unowned for a tray |
| placement | in the screen's visible frame | in the monitor's work area on X11; the compositor's positioner (gdk_window_move_to_rect) on Wayland |
in the monitor's work area (MonitorFromRect) |
| keyboard | the key window when focusable; the parent stays the main window | a grab of the seat and the app's input when focusable, as GTK's menus | activated when focusable, else WS_EX_NOACTIVATE |
On Wayland, a popup shows only over the topmost one: showing a flyout
closes the others that show, but its own ancestors, as the user would.
examples/flyout opens a list of colors, a note editor and an About box,
a popover on macOS.
Panels
A panel is the window of a launcher, a command palette or a quick entry, as Spotlight's and Raycast's are: summoned from anywhere by a global shortcut, it floats above the windows of every app, on whichever Space or virtual desktop the user is on, over apps in full screen too, and stays out of the taskbar, the Dock's windows and the app switcher. It cannot be minimized, maximized or go full screen, and has no menu bar on Linux and Windows. On macOS, showing it gives it the keyboard without activating the app: the app the user was in stays the active one, as its menu bar shows, and has the keyboard back as the panel hides. Clicks in the panel leave it active too.
mygo.App.SetActivationPolicy(mygo.ActivationPolicyAccessory) // no Dock icon (macOS)
mygo.App.OnWindowAllClosed(func() {}) // keep running with no window
mygo.App.WhenReady(func() {
panel := mygo.NewWindow(mygo.WindowOptions{
Panel: true,
Hidden: true,
Frameless: true,
Transparent: true,
Vibrancy: mygo.VibrancyHUD,
Width: 680,
Height: 420,
URL: "/launcher",
})
// Hide rather than close, so that it shows at once the next time:
// on Escape, Cmd-W or Alt-F4, and as the user goes elsewhere.
panel.OnClose(func(e *mygo.CloseEvent) {
e.PreventDefault()
panel.Hide()
})
panel.OnBlur(panel.Hide)
mygo.GlobalShortcut.Register("Alt+Shift+Space", func() {
if panel.IsVisible() {
panel.Hide()
return
}
// On the display with the pointer, a third of the way down.
d := mygo.Screen.DisplayNearestPoint(mygo.Screen.CursorScreenPoint())
width, height := panel.Size()
panel.SetPosition(d.WorkArea.X+(d.WorkArea.Width-width)/2, d.WorkArea.Y+(d.WorkArea.Height-height)/3)
panel.Show()
})
})A panel whose results grow and shrink, as a launcher's, takes its page's
size with FitContent, as a flyout
does: give it MaxHeight rather than a Height to pick.
An Escape the page does not handle (preventDefault()) or native UI
leaves closes a panel, as the user closing it, which OnClose listeners
may turn into hiding it, as above. A launcher is usually an accessory app,
without a Dock icon, from the start: give it LSUIElement in
macos.infoPlist of mygo.config.ts, or set its
activation policy before App.Run, as above. A regular app is activated
as it launches, as any app the user opens: the first window it shows,
a panel too, brings it to the front.
How each platform shows them:
| macOS | Linux | Windows | |
|---|---|---|---|
| window | an NSPanel with the nonactivating style, never the main window |
a utility window (_NET_WM_WINDOW_TYPE_UTILITY), kept above, out of the taskbar and the pager; a surface of the overlay layer under Wayland (gtk-layer-shell) |
a topmost tool window (WS_EX_TOOLWINDOW), owned by a hidden window, so it never has a taskbar button, with Windows 11's rounded corners |
| workspaces | every Space, as a full-screen auxiliary (CanJoinAllSpaces, FullScreenAuxiliary), out of Mission Control and the window cycle |
every workspace (sticky) | the virtual desktop it shows on |
| keyboard | the key window, the app staying inactive | the active window; on a layer surface, exclusively while it shows | the foreground window, which the key press of a global shortcut lets it take |
Under Wayland, where apps neither place nor raise their windows, a panel
is a surface of the layer shell where the compositor speaks the protocol
(Sway, Hyprland, KDE Plasma, COSMIC…) and libgtk-layer-shell.so.0 is
installed, as launchers there are: it shows on the overlay layer, above
windows in full screen too, out of the taskbar, centered on the output
the user is on, or where X and Y or SetPosition put it. It keeps the
keyboard while it shows, as other launchers there do, so that it gets the
keys from the start: it is blurred only as it hides, so close it on
Escape, which the user presses there. Elsewhere, as on GNOME, which has no
layer shell, it is a window as others: the compositor decides where it
shows and what is above it.
Click-through windows
A desktop pet is a frameless, transparent window always on top, which
shows a character, and a speech bubble now and then: clicks on them reach
the app, and clicks anywhere else the window or the desktop under it, as
if the pet's window were not there. SetInputRegion gives the window the
rectangles that take the mouse, in DIPs from the top-left corner of its
content; presses, moves and the wheel elsewhere go through to what is
behind it:
pet := mygo.NewWindow(mygo.WindowOptions{
Frameless: true,
Transparent: true,
AlwaysOnTop: true,
SkipTaskbar: true,
Width: 320,
Height: 240,
Content: ui.View(app.pet),
})
character := mygo.Rectangle{X: 200, Y: 120, Width: 110, Height: 110}
pet.SetInputRegion([]mygo.Rectangle{character})
// The bubble shows: it takes clicks too, until it goes.
pet.SetInputRegion([]mygo.Rectangle{character, {X: 10, Y: 10, Width: 220, Height: 90}})Set the region again as what takes clicks moves or changes size: the
rectangles stay where they were in the window, which may move as a whole
(SetPosition, or StartDrag from the character). A page finds its
rectangles with getBoundingClientRect and sends them through a bound
method; native UI with an element's Bounds. SetInputRegion(nil) gives
the window the mouse everywhere again, as it starts, and an empty slice
lets it all through, as SetIgnoreMouseEvents(true) does, which wins
over the region while it is set. PassThrough in native UI is another
thing: it lets the pointer through an element to what the window shows
under it, never to another window.
How each platform does it:
| macOS | Linux | Windows | |
|---|---|---|---|
| region | the window lets the mouse through (ignoresMouseEvents) while the pointer is outside the rectangles, switched as it moves, from monitors of the pointer's moves in the app and in other apps |
the window's input shape: X11's SHAPE extension, Wayland's input region | the window lets the mouse through (layered, WS_EX_TRANSPARENT) while the cursor is outside the rectangles, which a timer asks at each tick of the system's clock (15.6 ms) while the window shows |
On macOS and Windows a button held keeps the window as it is: a drag that started on the character ends there, as one that started on what is behind ends there, and the release settles which takes the mouse next.
Events
Listeners run on the main thread: keep them short, and start goroutines for
slow work. Each On… method returns a function that removes the listener.
| Event | When |
|---|---|
OnClose(func(e *mygo.CloseEvent)) |
the window is about to close, by the user or Close; e.PreventDefault() keeps it open |
OnClosed |
the window closed |
OnFocus, OnBlur |
it gained or lost the keyboard focus |
OnShow, OnHide |
it was shown or hidden |
OnResize, OnMove |
it was resized or moved |
OnMaximize, OnUnmaximize, OnMinimize, OnRestore |
its state changed |
OnEnterFullScreen, OnLeaveFullScreen |
it entered or left full screen |
OnReadyToShow |
its first page is ready to be displayed, or its native UI drew its first frame |
OnFileDrop |
files were dropped on its page, see dropped files, or on native UI that did not take them, see dropped files |
Ask before closing a window with unsaved changes:
win.OnClose(func(e *mygo.CloseEvent) {
if !doc.Dirty() {
return
}
e.PreventDefault()
go func() {
res, _ := mygo.Dialog.Message(mygo.MessageOptions{
Parent: win,
Type: mygo.MessageQuestion,
Message: "Save changes before closing?",
Buttons: []string{"Save", "Don't Save", "Cancel"},
})
switch res.Button {
case 0:
doc.Save()
win.Destroy()
case 1:
win.Destroy()
}
}()
})Close closes a window as the user would, asking OnClose listeners;
Destroy closes it at once.
Pages
A window shows one page at a time and has a history, like a browser tab.
win.Page() returns it, or nil for a window that shows
native UI:
page := win.Page()
page.LoadURL("/settings") // LoadHTML, LoadFile
page.Reload() // ReloadIgnoringCache, Stop
page.GoBack() // GoForward, CanGoBack, CanGoForward
page.URL() // the current page
page.SetZoomFactor(1.25) // ZoomFactorWindowOptions.Page sets up the page:
mygo.NewWindow(mygo.WindowOptions{
URL: "/",
Page: mygo.PageOptions{ZoomFactor: 1.25, DevTools: mygo.DevToolsEnabled},
})| Option | |
|---|---|
PreloadScript |
JavaScript run before every page's own scripts |
PreloadAllFrames |
runs PreloadScript in the page's iframes too, of every origin, where window.mygo isn't |
TrustedOrigins |
other origins whose pages may call Go, see who may call |
DevTools |
DevToolsAuto (the inspector in development builds), DevToolsEnabled or DevToolsDisabled, which also turns WebView2's own keys, as F5 reloading, on or off (Windows); for a window of native UI, its inspector |
ZoomFactor, UserAgent |
the page's zoom (1 is 100%) and user agent |
SafariTabFocus |
leaves Tab to text fields and elements with a tabindex, as Safari does unless the system's keyboard navigation is on; by default Tab reaches links, buttons, checkboxes and selects too (macOS) |
PictureInPicture |
lets the page's videos go into picture in picture, see picture in picture (macOS; WebView2 lets them already) |
SwipeNavigation |
a horizontal swipe goes back and forward through the page's history, as in browsers, see gestures |
PinchZoom |
a pinch magnifies the page, as in browsers, see gestures |
UserAgentSuffix |
appended to the user agent instead of replacing it, which UserAgent does, such as "MyApp/1.2"; it appends to UserAgent too, and SetUserAgent keeps it, and leading and trailing spaces are trimmed |
ContentRules |
rules that block what the page loads and hide parts of it, see blocking content |
BackgroundThrottling |
ThrottlingOn (the web view's own) or ThrottlingOff, which keeps the page running while its window is hidden, minimized or covered, see background pages |
Autoplay |
AutoplayUserGesture (the web view's own policy) or AutoplayAllow, which lets the page play audible media before the user clicked or typed in it, in a hidden window too, see autoplay |
Session |
the store of the page's cookies, storage and caches, apart from other sessions', see sessions; nil is the default session |
UserAgentSuffix is for the sites that want to see the app, and for the ones
that serve a bare web view older pages because its user agent names no browser:
"Version/17.0 Safari/605.1.15" makes the page look like Safari.
Page events, on win.Page():
| Event | When |
|---|---|
OnWillNavigate(func(e *mygo.NavigateEvent)) |
the page is about to navigate, for example after a click on a link; e.PreventDefault() cancels it. e.UserInitiated says whether a user's gesture started it, a click on a link or a click whose handler sets location.href, or the page's script did on its own. Not called for LoadURL and its siblings, nor for the page's iframes, except on Linux before WebKitGTK 2.56, which tells no navigation's frame: there it is called for an iframe's too, but a srcdoc one's. |
OnDidNavigate(func(url string)) |
a navigation committed |
OnDOMReady |
the page's DOM is ready (DOMContentLoaded) |
OnDidFinishLoad |
the page finished loading |
OnDidFailLoad(func(err *mygo.LoadError)) |
the page failed to load |
OnPageTitleUpdated(func(e *mygo.TitleEvent)) |
the page's <title> changed; preventing the event keeps the window title as it is |
OnThemeColorChanged(func(c color.NRGBA, ok bool)) |
the page's theme color changed; ok is false when it has none any more |
OnRenderProcessGone(func(reason string)) |
the web content process crashed or was killed; Reload recovers |
OnAudibleChanged(func(audible bool)) |
the page started or stopped making sound |
OnCaptureStateChanged(func(kind mygo.CaptureKind, s mygo.CaptureState)) |
the page started or stopped capturing the camera, the microphone or the display, or the capture was muted or unmuted |
Keep a window on the app's own pages and open other links in the browser:
win.Page().OnWillNavigate(func(e *mygo.NavigateEvent) {
if u, err := url.Parse(e.URL); err == nil && (u.Scheme == "http" || u.Scheme == "https") && e.UserInitiated {
e.PreventDefault()
go mygo.Shell.OpenExternal(e.URL)
}
})During development the app's pages come from the dev server, an http URL:
check the host, or mygo.IsDev(), when your rule depends on the scheme.
Links that open new windows go through the window's open handler, see links and new windows.
Swipes and pinches
A page neither goes back on a swipe nor zooms on a pinch, as desktop apps
don't: SwipeNavigation and PinchZoom turn them on, for a window's page
and a web view's alike.
mygo.NewWindow(mygo.WindowOptions{
URL: "https://example.com/",
Page: mygo.PageOptions{SwipeNavigation: true, PinchZoom: true},
})A pinch magnifies what the page shows without laying it out again, as
browsers do, and leaves ZoomFactor, which lays the page out at another
size, as it is.
SwipeNavigation |
PinchZoom |
|
|---|---|---|
| macOS | a trackpad's or a Magic Mouse's swipe between pages, as System Settings sets it up (allowsBackForwardNavigationGestures) |
a trackpad's pinch and smart zoom (allowsMagnification) |
| Linux | two fingers across a touchpad, one across a touchscreen (enable-back-forward-navigation-gestures) |
a touchscreen's pinch, and a touchpad's, which GTK reports on Wayland only. WebKitGTK zooms with no setting to stop it: MyGo turns its zoom gesture off from WebKitGTK 2.38, and older versions always zoom |
| Windows | a swipe across a touchscreen (IsSwipeNavigationEnabled) |
a touchscreen's pinch (IsPinchZoomEnabled); a WebView2 Runtime without ICoreWebView2Settings5 always zooms |
WebKitGTK and WebView2 have pinches, and WebView2 swipes, on by default: MyGo turns them off unless the page asks for them.
The theme color
ThemeColor returns the page's theme color, its <meta name="theme-color">,
so that the app can tint its title bar, toolbar or window background to
match the page, as Safari does; OnThemeColorChanged tells when it
changes:
page := win.Page()
page.OnThemeColorChanged(func(c color.NRGBA, ok bool) {
if !ok {
c = defaultToolbarColor // the page has none
}
toolbar.SetBackground(ui.RGB(c.R, c.G, c.B))
})A page may have one color for light appearance and another for dark, with
the metas' media:
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#111111" media="(prefers-color-scheme: dark)">The color is the content of the first meta named theme-color in the
page's <head> whose media is absent or matches, and whose content is a
color, as browsers pick it. The page's engine computes it, so any CSS color
works (dodgerblue, rgb(30 144 255 / 50%), oklch(…)), and it comes as
8-bit sRGB with straight alpha. The event fires only when the color
changes: when the page adds, removes or changes its metas, when the app's
appearance (Theme) changes and another meta's
media matches, and when another page has another color. A page that just started loading
keeps the color of the one before until its DOM is ready, so going between
pages of the same color fires nothing.
MyGo's runtime in the page watches the metas, the same way on every platform, in pages of any origin, trusted or not. A page the runtime does not run in, as an image or a PDF the web view shows, has no theme color.
Sound
IsAudible tells whether a page is making sound, from any of its frames,
a video of another site included, and OnAudibleChanged when that
changes, for a speaker on a tab or a window, or a media app's state.
SetAudioMuted mutes the whole page, as a browser mutes a tab, without the
page seeing its elements muted; it lasts as the page navigates:
page := win.Page()
page.OnAudibleChanged(func(audible bool) {
log.Println("playing:", audible) // show a speaker on the window's tab
})
// A menu item that mutes or unmutes the window it was chosen for.
mute := &mygo.MenuItem{Label: "Mute or Unmute Window", Click: func(_ *mygo.MenuItem, win *mygo.Window) {
if win == nil || win.Page() == nil {
return // no window, or one showing native UI
}
win.Page().SetAudioMuted(!win.Page().IsAudioMuted())
}}A page muted right after NewWindow starts muted, even where its web view
is not made yet. A muted page can still be audible, as a tab shows a
crossed-out speaker; the engine decides when a paused or silent source stops
counting, which can take a moment after the sound stops.
| Platform | Making sound | Muting |
|---|---|---|
| macOS | WebKit's private _isPlayingAudio, observed; false if WebKit stops answering it |
WebKit's private _setPageMuted:, as Safari mutes a tab; does nothing if WebKit stops answering it |
| Windows | IsDocumentPlayingAudio of WebView2 (ICoreWebView2_8); false on runtimes older than it |
IsMuted of the same; does nothing on older runtimes |
| Linux | webkit_web_view_is_playing_audio |
webkit_web_view_set_is_muted, WebKitGTK 2.30 or later; does nothing on older versions |
Where a platform can't mute, IsAudioMuted is false. Both are false for a
window showing native UI and after the window closed. A web view's
page has them too.
Background pages
Web views slow down a page nobody can see, to save power: once its window is
hidden, minimized or behind others, its timers fire once a second at most,
requestAnimationFrame waits, and macOS may suspend it. A page that plays,
syncs or computes in the background keeps running with ThrottlingOff, as
Electron's backgroundThrottling: false:
mygo.NewWindow(mygo.WindowOptions{
URL: "/player",
Page: mygo.PageOptions{BackgroundThrottling: mygo.ThrottlingOff},
})Its timers keep their rate and its process the priority of a visible page's,
in windows and in web views alike. A covered window's page keeps
its animation frames too on macOS and Windows, but the page of a hidden or
minimized window still goes hidden (document.visibilityState) on macOS and
Linux, and so does a web view of a hidden window on Windows, which draws
nothing then: run what must go on with timers.
| macOS | WKPreferences.inactiveSchedulingPolicy none (macOS 14+), WebKit's private hidden-page timer throttling, CSS animation suspension and process suppression off, and window occlusion detection off, so that a covered window counts as visible |
| Linux | WebKitGTK's HiddenPageDOMTimerThrottling and HiddenPageCSSAnimationSuspension features off (WebKitGTK 2.42+; nothing before) |
| Windows | its browser runs with --disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows |
On Windows those arguments are the WebView2 environment's, which all of an app's pages share and which the first window or web view that shows a page creates: give the option to that one. Then no page of the app has its timers throttled; a page that asks later is too late, and the log says so. Two processes of the app share the browser too, so they must agree: WebView2 refuses web views to a second one whose first page asks otherwise than the running one's did. A web view in a hidden window's native UI still goes hidden, as it must for its window to leave the screen.
Autoplay
WebKitGTK and WebView2 let a page start audible media only once the user
clicked or typed in it: before, play() of an unmuted <audio> or <video>
rejects with NotAllowedError, and muted media that plays pauses again when
it is unmuted. The page of a hidden window never gets such a gesture, so
there it can't play sound at all. An app whose interface is native UI, and
which plays its media in a page of a hidden window, lets that page play with
AutoplayAllow:
mygo.NewWindow(mygo.WindowOptions{
URL: "/player",
Hidden: true,
Page: mygo.PageOptions{Autoplay: mygo.AutoplayAllow},
})Then play() resolves and autoplay elements start with their sound, in
windows and in web views alike. AutoplayUserGesture, the
default, leaves the page to its web view's own policy.
| macOS | WKWebViewConfiguration.mediaTypesRequiringUserActionForPlayback none |
| Linux | the website policies of the page's main-frame navigations allow autoplay (WEBKIT_AUTOPLAY_ALLOW, WebKitGTK 2.30+; nothing before) |
| Windows | its browser runs with --autoplay-policy=no-user-gesture-required |
On Windows, as with ThrottlingOff, that argument is
the WebView2 environment's, which all of an app's pages share and which the
first window or web view that shows a page creates: give the option to that
one. Then every page of the app may play without a gesture, those that keep
AutoplayUserGesture too; a page that asks later is too late, and the log
says so. In an app whose first window is native UI, the window of the page
that plays is often the first to show a page, and its option is enough. One
whose first window shows a page gives that window the option too, or creates
the page that plays first. Two processes of the app must agree, as with
ThrottlingOff.
Camera and microphone
CaptureState tells whether a page captures the camera, the microphone or
the display (getUserMedia(), getDisplayMedia()), from any of its frames,
and whether the capture is muted: CaptureNone, CaptureActive or
CaptureMuted. OnCaptureStateChanged tells when that changes, for a
recording indicator in the window or the menu bar, as Safari shows.
SetCaptureState mutes, unmutes or stops a capture without the page's
help, as the mute button of a call app: a muted capture keeps the page's
tracks, which get silence or black frames, and a stopped one ends them.
page := win.Page()
page.OnCaptureStateChanged(func(kind mygo.CaptureKind, s mygo.CaptureState) {
if kind == mygo.CaptureMicrophone {
log.Println("microphone live:", s == mygo.CaptureActive) // show a recording indicator
}
})
// A menu item that mutes or unmutes the microphone of the window it was chosen for.
mute := &mygo.MenuItem{Label: "Mute or Unmute Microphone", Accelerator: "CmdOrCtrl+Shift+M", Click: func(_ *mygo.MenuItem, win *mygo.Window) {
if win == nil || win.Page() == nil {
return
}
p := win.Page()
switch p.CaptureState(mygo.CaptureMicrophone) {
case mygo.CaptureActive:
p.SetCaptureState(mygo.CaptureMicrophone, mygo.CaptureMuted)
case mygo.CaptureMuted:
p.SetCaptureState(mygo.CaptureMicrophone, mygo.CaptureActive)
}
}}The page asks for the devices first, which permissions
decide. A change shows in CaptureState and the event once the page took
it, a moment after SetCaptureState, which does nothing while the page
captures no such device. The page can't unmute a capture itself: asking for
the device again goes to the permission handler, as a new capture does.
| Platform | Capture state | Muting and stopping |
|---|---|---|
| macOS | cameraCaptureState and microphoneCaptureState of WKWebView (macOS 12+), and WebKit's private _displayCaptureState (macOS 13+), observed; CaptureNone if WebKit doesn't answer them |
setCameraCaptureState:completionHandler:, setMicrophoneCaptureState:completionHandler: and the private _setDisplayCaptureState:completionHandler:; while only one of the camera and the microphone is muted, muting or unmuting the page's sound (SetAudioMuted) mutes the other for a moment, as WebKit mutes them together |
| Linux | webkit_web_view_get_camera_capture_state and its microphone and display siblings, WebKitGTK 2.34 or later, and their notify signals |
their setters; muting or unmuting the page's sound (SetAudioMuted) unmutes every capture in WebKitGTK, which MyGo mutes again at once, so a muted capture may be live for a moment |
| Windows | always CaptureNone: WebView2 tells no capture |
does nothing |
CaptureState is CaptureNone for a window showing native UI and after
the window closed. A web view's page has them too.
Picture in picture
PageOptions.PictureInPicture lets a page's videos float out into picture
in picture, a small window over every app's, as they do in Safari: the
button of a video's controls, the item of its context menu, and
requestPictureInPicture() in the page's script. WKWebView keeps it to
Safari, so a page on macOS needs the option; WebView2 lets videos go
whatever it says. TogglePictureInPicture moves a video out, or the one
out back into the page, without a click in the page, and
IsPictureInPictureActive tells whether one is out, whoever put it there:
win := mygo.NewWindow(mygo.WindowOptions{
URL: "/player",
Page: mygo.PageOptions{PictureInPicture: true},
})
// A menu item that floats the video of the window it was chosen for.
pip := &mygo.MenuItem{Label: "Picture in Picture", Accelerator: "CmdOrCtrl+Shift+P", Click: func(_ *mygo.MenuItem, win *mygo.Window) {
if win != nil && win.Page() != nil {
win.Page().TogglePictureInPicture()
}
}}| Platform | |
|---|---|
| macOS | WebKit's private _allowsPictureInPictureMediaPlayback preference, and _togglePictureInPicture, _canTogglePictureInPicture and _isPictureInPictureActive for the video WebKit picks for its media controls; all do nothing, or false, if WebKit stops answering them. A page that navigates with its video out is left out of the back/forward cache (private _setUsesPageCache:), from which WebKit would bring it back believing its video still out |
| Windows | Chromium's, on in every page. TogglePictureInPicture calls requestPictureInPicture() of the video playing, else of the largest, in the page's document, not its frames, as a user's gesture, and IsPictureInPictureActive asks the page for document.pictureInPictureElement, which a video in a frame leaves null |
| Linux | none: WebKitGTK has no picture in picture, so TogglePictureInPicture does nothing and IsPictureInPictureActive is false |
Both do nothing, or false, for a window showing native UI and after the window closed. A web view's page has them too.
Find in page
FindInPage highlights the matches of a text and reports how many there
are, like the find bar of a browser; call it as the user types, and with
FindNext to go to the next match:
res, err := win.Page().FindInPage("mygo", mygo.FindOptions{FindNext: true})
fmt.Printf("%d of %d\n", res.Active, res.Matches)
win.Page().StopFindInPage()Printing, PDF and screenshots
win.Page().Print() // the print dialog
pdf, err := win.Page().PrintToPDF(mygo.PDFOptions{
PageSize: mygo.PageA4,
Landscape: false,
Margins: &mygo.Margins{Top: 0.5, Right: 0.5, Bottom: 0.5, Left: 0.5}, // inches
Background: true, // print background colors and images
})
png, err := win.CapturePage() // what the window shows, as a PNGPrintToPDF lays the page out for printing, so its print style sheets
apply. The zero PDFOptions prints Letter pages in portrait with margins of
0.4 inch and no backgrounds, like a browser's print dialog.
The PDF's text copies and searches as the page's. On macOS, the PDFs that
WebKit prints name radicals for many Chinese characters, such as ⼈
(U+2F08) for 人, which the system's fonts draw with the same glyph.
PrintToPDF asks the installed fonts which characters their glyphs are
and appends corrected text maps to the PDF, which leaves its pages as they
were. Text in a font that is not installed, such as a web font, keeps the
radicals.
Downloads
A page downloads a file when it follows a link with the download
attribute, or gets a response it cannot show, such as an attachment. By
default the file goes to the Downloads directory, under the name the page
or the server suggests. OnWillDownload chooses another path or cancels
the download, and OnDownloadDone reports how it ended:
win.Page().OnWillDownload(func(e *mygo.DownloadEvent) {
if strings.HasSuffix(e.SuggestedName, ".exe") {
e.PreventDefault()
return
}
dir, _ := mygo.App.Path(mygo.PathDocuments)
e.Path = filepath.Join(dir, e.SuggestedName)
})
win.Page().OnDownloadDone(func(d *mygo.Download) {
if d.Err == nil {
mygo.Shell.ShowItemInFolder(d.Path)
}
})Permissions
Pages ask for the camera, the microphone, the location or notifications.
By default the app's own pages, those of custom schemes included (see
trust), get what they ask for and other pages get
nothing. Page.SetPermissionHandler decides instead; it runs on the main
thread:
win.Page().SetPermissionHandler(func(req mygo.PermissionRequest) bool {
return req.Origin == "https://meet.example.com" &&
!slices.Contains(req.Permissions, mygo.PermissionGeolocation)
})The operating system may ask the user too. macOS does, once per app, for
the camera and the microphone, and ends apps that use them without
explaining why in Info.plist: add usage descriptions to macos.infoPlist
in mygo.config.ts:
export default defineConfig({
macos: {
infoPlist: {
NSCameraUsageDescription: "Scan documents with the camera.",
NSMicrophoneUsageDescription: "Record voice notes.",
},
},
});Sign-in and certificates
A page behind HTTP authentication (Basic, Digest, NTLM), of its server or
of a proxy, asks for a user name and a password. Page.SetAuthHandler
answers, on the main thread, with the credentials, or with an empty
AuthResponse to cancel, which shows the server's refusal:
win.Page().SetAuthHandler(func(req mygo.AuthRequest) mygo.AuthResponse {
if req.Host == "router.lan" && !req.IsProxy {
return mygo.AuthResponse{Username: "admin", Password: password}
}
return mygo.AuthResponse{}
})The web view keeps the credentials a server accepted for the rest of the
session. Without a handler, WebKitGTK and WebView2 show their own sign-in
dialog, and macOS none. To ask the user in the app's own way, cancel,
prompt, then Reload once the handler has the credentials.
A server whose certificate does not verify, such as a dev server with a
self-signed certificate, fails to load, as in a browser.
Page.SetCertificateErrorHandler can load it anyway: allow only the
certificate you expect, which CertificateError.Certificate holds
(DER-encoded), never a host whatever its certificate:
win.Page().SetCertificateErrorHandler(func(e mygo.CertificateError) bool {
sum := sha256.Sum256(e.Certificate)
return e.Host == "dev.example" && sum == devCertificateSHA256
})On Linux only the page itself is asked about, not what it loads, and on Linux and Windows a certificate allowed for a host stays allowed in every window until the app quits.
A server, or a proxy, can also ask for the client's certificate before it
serves the page (mutual TLS). Page.SetClientCertificateHandler answers
with one of the certificates the platform offers, or with an empty
ClientCertificateResponse to refuse, which fails the load as when a
user dismisses a browser's prompt:
win.Page().SetClientCertificateHandler(func(req mygo.ClientCertificateRequest) mygo.ClientCertificateResponse {
for _, c := range req.Certificates {
if c.Subject == "CN=alice" {
return mygo.ClientCertificateResponse{Certificate: c}
}
}
return mygo.ClientCertificateResponse{}
})What is offered is the platform's: WebView2's mutually trusted
certificates on Windows, the keychain's identities on macOS. WebKitGTK
lists none, so on Linux Certificates is empty and a handler can only
refuse; without a handler each platform shows its own choice (WebView2's
dialog, WebKitGTK's chooser; macOS never asks). A refusal on Linux ends
the load as a cancellation, which OnDidFinishLoad reports and
OnDidFailLoad does not.
Blocking content
Content rules block what pages load and hide parts of them, as ad and
tracker blockers do. They are written in WebKit's
content blocker format:
a JSON array of rules, each a trigger that matches loads by a regular
expression of their URL, and an action. mygo.CompileContentRules
compiles them, and a page applies them through PageOptions.ContentRules
or Page.SetContentRules:
rules, err := mygo.CompileContentRules("ads", `[
{"trigger": {"url-filter": "^[^:]+://+([^:/]+\\.)?ads\\.example\\.com[:/]"}, "action": {"type": "block"}},
{"trigger": {"url-filter": ".*", "if-domain": ["*news.example"]},
"action": {"type": "css-display-none", "selector": ".sponsored"}}]`)
if err != nil {
return err
}
win := mygo.NewWindow(mygo.WindowOptions{URL: "https://news.example/",
Page: mygo.PageOptions{ContentRules: []*mygo.ContentRules{rules}}})Triggers take url-filter, url-filter-is-case-sensitive, one of
if-domain, unless-domain, if-top-url and unless-top-url,
resource-type and load-type (first-party or third-party).
Actions are block, block-cookies, make-https, css-display-none with
a selector, and ignore-previous-rules, which undoes what the rules
before it did to a load. Regular expressions are WebKit's: no alternatives
(|), counted repetitions or classes such as \d, and ASCII only.
Domains are lowercase, in punycode. A rule whose selector the engine cannot
parse is skipped; any other mistake fails the whole list.
CompileContentRules takes a while for long lists, so call it off the
main thread. The engine keeps what it compiled under the id: the same
rules load right away on later launches, and other rules compiled under
the id replace the earlier ones. WebKit limits the rules of a list (to
150,000, or 50,000 in older versions), so split a long list into several
and apply them together; a page applies its lists in order, each list's
ignore-previous-rules undoing only its own rules. SetContentRules
replaces a page's lists, and with none removes them: the loads from then
on go through them, and the page shows what it loaded until it reloads.
macOS and Linux (WebKitGTK 2.24 or newer) apply the rules with WebKit's
content blockers. WebView2 has none, so on Windows MyGo sees every request
of a page applying rules and fails the blocked ones: a blocked fetch
rejects and an XMLHttpRequest errs, as in WebKit, while a blocked page
shows WebView2's error page for a 403 response. It redirects what
make-https matches, and hides elements with a style sheet each document
adopts. There:
- WebSockets are not blocked: WebView2 does not show MyGo their requests.
- Popups (
resource-typepopup) are not matched. block-cookiesremoves the cookies of the request, not those of the response.- An element whose own
stylesaysdisplay: ... !importantstays: the style sheet is the page's, where WebKit's is the user's, which wins.
The web inspector
OpenDevTools, CloseDevTools, ToggleDevTools and IsDevToolsOpened
of a page control its inspector, when it has one, see
the web inspector.
The page's process
ProcessID returns the ID of the operating system's process that runs a
page, for an app's own task manager, memory reports or telling which window
a busy process belongs to; 0 when it is unknown:
pid := win.Page().ProcessID()| Platform | The process |
|---|---|
| macOS | the page's web content process (com.apple.WebKit.WebContent), from WebKit's private _webProcessIdentifier; 0 if WebKit stops answering it. A page may move to another process when it navigates to another site or after its process crashed, so ask again rather than keep the ID |
| Windows | WebView2's browser process (msedgewebview2.exe), which the app's web views share and whose children render the pages |
| Linux | none: WebKitGTK has no API for the process of a page, so it is always 0 |
It is 0 too before the web view has started a process, after the window closed, and for a window showing native UI.
GPUProcessID returns the ID of the process that does the page's graphics
and media. On macOS that is where the page's sound comes out, so an app
that shows what a page plays (a music player's visualizer, say) can capture
it with a Core Audio process tap (CATapDescription, macOS 14.2 and
later), which needs the process's ID:
pid := win.Page().GPUProcessID()| Platform | The process |
|---|---|
| macOS | WebKit's GPU process (com.apple.WebKit.GPU), which plays the page's audio and video, from WebKit's private _gpuProcessIdentifier; 0 if WebKit doesn't answer it or hasn't started the process yet, which it does once a page first needs it, to play media or draw with the GPU |
| Windows | WebView2's GPU process (msedgewebview2.exe --type=gpu-process), a child of ProcessID's browser process, from ICoreWebView2Environment8's GetProcessInfos; 0 on runtimes without it. WebView2 plays sound from its audio service, a utility process, not this one: capture the tree of ProcessID's process for it (process loopback) |
| Linux | none: WebKitGTK has no API for its GPU process, so it is always 0 |
| Browsers | none: always 0 |
It is 0 too before the web view has started a process, after the window closed, and for a window showing native UI. The app's web views may share the process, and it gets a new ID each time the app runs and whenever the web view starts it again, after it crashed or exited, so ask again rather than keep the ID.
Finding windows
mygo.Windows()lists the open windows,mygo.FocusedWindow()returns the focused one andmygo.WindowByID(id)the one with anID(), which pages know asruntime().windowId.mygo.CallerWindow(ctx)returns the window whose page called a bound method, andmygo.CallerPage(ctx)the page: the window's, or that of a web view in a window of native UI (Web view). Given ther.Context()of a request to a custom scheme, they return the window and the page that made it.mygo.App.OnWindowCreatedis called for every new window.
GTK's style on Linux
On Linux, GTK draws part of a window's frame itself, in the desktop's
GTK 3 theme: the title bar and decorations of a window the desktop leaves
to decorate itself (GNOME on Wayland), the shadow and border GTK puts
around a frameless window or a hidden title bar on Wayland, and the title
buttons of a hidden title bar; its menus and dialogs too.
mygo.Theme.SetGTKCSS styles them with a GTK 3 style sheet of the app's,
for a frame that matches the rest of the desktop, such as the flat header
bars and round title buttons of GTK 4 and libadwaita apps:
const gtk4Frame = `
decoration { border-radius: 12px; box-shadow: 0 3px 9px 1px rgba(0, 0, 0, .28); }
.maximized decoration, .fullscreen decoration, .tiled decoration { border-radius: 0; }
.csd.popup decoration, tooltip.csd decoration { border-radius: 5px; }
headerbar { min-height: 46px; padding: 0 6px; background-color: #f9fafb;
background-image: none; border-width: 0; box-shadow: none; }
button.titlebutton { min-width: 24px; min-height: 24px; margin: 0 3px; padding: 0;
border: none; border-radius: 9999px; background-color: transparent; }
`
if err := mygo.Theme.SetGTKCSS(gtk4Frame); err != nil {
log.Print(err) // the sheet does not parse
}- GTK takes the sheet at the user's priority
(
GTK_STYLE_PROVIDER_PRIORITY_USER): its rules win over the theme's, MyGo's own and those of the user's~/.config/gtk-3.0/gtk.css. - Another call replaces the sheet, as a switch of the app's theme does, and
""removes it. A sheet that does not parse returns GTK's error, with where it is, and the sheet set before stays. - It may be called from any goroutine, before
App.Runtoo: other goroutines wait for Run to load the sheet, and main's sheet waits for Run, which logs its error, as no call is left to return it. - Pages and native UI never see it: MyGo draws them, and pages have their own CSS. On macOS, Windows and in browsers it does nothing and returns nil.
GTK 3 rounds only the top corners of a window it decorates, whatever the
border-radius of decoration: it clips the decorations to the radius but
never the window's content, so the page or the native UI covers the
bottom corners, which stay square. A window that needs four rounded
corners draws them itself: a Frameless, Transparent window, which GTK
leaves undecorated, without its shadow, whose page or native UI rounds its
own background, under a compositing window manager or on Wayland.
Native access
win.NativeHandle() returns the native window, an NSWindow* on macOS, a
GtkWindow* on Linux and an HWND on Windows, for APIs MyGo does not
cover. Call native APIs on the main thread with mygo.RunOnMain.