diff --git a/app/os.go b/app/os.go index 75d0e03d..a5f2489d 100644 --- a/app/os.go +++ b/app/os.go @@ -6,6 +6,7 @@ package app import ( "errors" + "image" "image/color" "gioui.org/io/key" @@ -21,35 +22,76 @@ type size struct { Height unit.Value } -type config struct { - Size *size - MinSize *size - MaxSize *size - Title *string - WindowMode *windowMode - StatusColor *color.NRGBA - NavigationColor *color.NRGBA - Orientation *orientation - CustomRenderer bool +// Config describes a Window configuration. +type Config struct { + // Size is the window dimensions (Width, Height). + Size image.Point + // MaxSize is the window maximum allowed dimensions. + MaxSize image.Point + // MinSize is the window minimum allowed dimensions. + MinSize image.Point + // Title is the window title displayed in its decoration bar. + Title string + // WindowMode is the window mode. + Mode WindowMode + // StatusColor is the color of the Android status bar. + StatusColor color.NRGBA + // NavigationColor is the color of the navigation bar + // on Android, or the address bar in browsers. + NavigationColor color.NRGBA + // Orientation is the current window orientation. + Orientation Orientation + // CustomRenderer is true when the window content is rendered by the + // client. + CustomRenderer bool +} + +func (c *Config) apply(m unit.Metric, options []Option) { + for _, o := range options { + o(m, c) + } } type wakeupEvent struct{} -type windowMode uint8 +// WindowMode is the window mode (WindowMode.Option sets it). +// +// Supported platforms are macOS, X11, Windows, Android and JS. +type WindowMode uint8 const ( - windowed windowMode = iota - fullscreen + // Windowed is the normal window mode with OS specific window decorations. + Windowed WindowMode = iota + // Fullscreen is the full screen window mode. + Fullscreen ) -type orientation uint8 +func (m WindowMode) Option() Option { + return func(_ unit.Metric, cnf *Config) { + cnf.Mode = m + } +} + +// Orientation is the orientation of the app (Orientation.Option sets it). +// +// Supported platforms are Android and JS. +type Orientation uint8 const ( - anyOrientation orientation = iota - landscapeOrientation - portraitOrientation + // AnyOrientation allows the window to be freely orientated. + AnyOrientation Orientation = iota + // LandscapeOrientation constrains the window to landscape orientations. + LandscapeOrientation + // PortraitOrientation constrains the window to portrait orientations. + PortraitOrientation ) +func (o Orientation) Option() Option { + return func(_ unit.Metric, cnf *Config) { + cnf.Orientation = o + } +} + type frameEvent struct { system.FrameEvent @@ -91,7 +133,10 @@ type driver interface { WriteClipboard(s string) // Configure the window. - Configure(cnf *config) + Configure([]Option) + + // Config returns the current configuration. + Config() Config // SetCursor updates the current cursor to name. SetCursor(name pointer.CursorName) @@ -109,8 +154,8 @@ type windowRendezvous struct { } type windowAndConfig struct { - window *callbacks - cnf *config + window *callbacks + options []Option } func newWindowRendezvous() *windowRendezvous { diff --git a/app/os_android.go b/app/os_android.go index 932c09c5..832e88b0 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -144,7 +144,8 @@ type window struct { started bool animating bool - win *C.ANativeWindow + win *C.ANativeWindow + config Config } // gioView hold cached JNI methods for GioView. @@ -337,7 +338,7 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j handle := C.jlong(view) views[handle] = w w.loadConfig(env, class) - w.Configure(wopts.cnf) + w.Configure(wopts.options) w.setStage(system.StagePaused) w.callbacks.Event(ViewEvent{View: uintptr(view)}) return handle @@ -762,8 +763,8 @@ func goString(env *C.JNIEnv, str C.jstring) string { func osMain() { } -func newWindow(window *callbacks, cnf *config) error { - mainWindow.in <- windowAndConfig{window, cnf} +func newWindow(window *callbacks, options []Option) error { + mainWindow.in <- windowAndConfig{window, options} return <-mainWindow.errs } @@ -787,23 +788,40 @@ func (w *window) ReadClipboard() { }) } -func (w *window) Configure(cnf *config) { +func (w *window) Configure(options []Option) { runInJVM(javaVM(), func(env *C.JNIEnv) { - if o := cnf.Orientation; o != nil { - setOrientation(env, w.view, *o) + prev := w.config + cnf := w.config + cnf.apply(unit.Metric{}, options) + if prev.Orientation != cnf.Orientation { + w.config.Orientation = cnf.Orientation + setOrientation(env, w.view, cnf.Orientation) } - if o := cnf.NavigationColor; o != nil { - setNavigationColor(env, w.view, *o) + if prev.NavigationColor != cnf.NavigationColor { + w.config.NavigationColor = cnf.NavigationColor + setNavigationColor(env, w.view, cnf.NavigationColor) } - if o := cnf.StatusColor; o != nil { - setStatusColor(env, w.view, *o) + if prev.StatusColor != cnf.StatusColor { + w.config.StatusColor = cnf.StatusColor + setStatusColor(env, w.view, cnf.StatusColor) } - if o := cnf.WindowMode; o != nil { - setWindowMode(env, w.view, *o) + if prev.Mode != cnf.Mode { + switch cnf.Mode { + case Fullscreen: + callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_TRUE) + w.config.Mode = Fullscreen + case Windowed: + callVoidMethod(env, w.view, gioView.setFullscreen, C.JNI_FALSE) + w.config.Mode = Windowed + } } }) } +func (w *window) Config() Config { + return w.config +} + func (w *window) SetCursor(name pointer.CursorName) { runInJVM(javaVM(), func(env *C.JNIEnv) { setCursor(env, w.view, name) @@ -839,18 +857,18 @@ func setCursor(env *C.JNIEnv, view C.jobject, name pointer.CursorName) { callVoidMethod(env, view, gioView.setCursor, jvalue(curID)) } -func setOrientation(env *C.JNIEnv, view C.jobject, mode orientation) { +func setOrientation(env *C.JNIEnv, view C.jobject, mode Orientation) { var ( id int idFallback int // Used only for SDK 17 or older. ) // Constants defined at https://developer.android.com/reference/android/content/pm/ActivityInfo. switch mode { - case anyOrientation: + case AnyOrientation: id, idFallback = 2, 2 // SCREEN_ORIENTATION_USER - case landscapeOrientation: + case LandscapeOrientation: id, idFallback = 11, 0 // SCREEN_ORIENTATION_USER_LANDSCAPE (or SCREEN_ORIENTATION_LANDSCAPE) - case portraitOrientation: + case PortraitOrientation: id, idFallback = 12, 1 // SCREEN_ORIENTATION_USER_PORTRAIT (or SCREEN_ORIENTATION_PORTRAIT) } callVoidMethod(env, view, gioView.setOrientation, jvalue(id), jvalue(idFallback)) @@ -870,15 +888,6 @@ func setNavigationColor(env *C.JNIEnv, view C.jobject, color color.NRGBA) { ) } -func setWindowMode(env *C.JNIEnv, view C.jobject, mode windowMode) { - switch mode { - case fullscreen: - callVoidMethod(env, view, gioView.setFullscreen, C.JNI_TRUE) - default: - callVoidMethod(env, view, gioView.setFullscreen, C.JNI_FALSE) - } -} - // Close the window. Not implemented for Android. func (w *window) Close() {} diff --git a/app/os_ios.go b/app/os_ios.go index a6a2bb3e..ab1589f3 100644 --- a/app/os_ios.go +++ b/app/os_ios.go @@ -95,6 +95,7 @@ type window struct { visible bool cursor pointer.CursorName + config Config pointerMap []C.CFTypeRef } @@ -268,7 +269,11 @@ func (w *window) WriteClipboard(s string) { C.writeClipboard(chars, C.NSUInteger(len(u16))) } -func (w *window) Configure(cnf *config) {} +func (w *window) Configure([]Option) {} + +func (w *window) Config() Config { + return w.config +} func (w *window) SetAnimating(anim bool) { v := w.view @@ -329,8 +334,8 @@ func (w *window) SetInputHint(_ key.InputHint) {} // Close the window. Not implemented for iOS. func (w *window) Close() {} -func newWindow(win *callbacks, cnf *config) error { - mainWindow.in <- windowAndConfig{win, cnf} +func newWindow(win *callbacks, options []Option) error { + mainWindow.in <- windowAndConfig{win, options} return <-mainWindow.errs } diff --git a/app/os_js.go b/app/os_js.go index 64aa7b1c..61b2948a 100644 --- a/app/os_js.go +++ b/app/os_js.go @@ -46,7 +46,7 @@ type window struct { chanAnimation chan struct{} chanRedraw chan struct{} - size f32.Point + config Config inset f32.Point scale float32 animating bool @@ -56,7 +56,7 @@ type window struct { wakeups chan struct{} } -func newWindow(win *callbacks, cnf *config) error { +func newWindow(win *callbacks, options []Option) error { doc := js.Global().Get("document") cont := getContainer(doc) cnv := createCanvas(doc) @@ -94,7 +94,7 @@ func newWindow(win *callbacks, cnf *config) error { }) w.addEventListeners() w.addHistory() - w.Configure(cnf) + w.Configure(options) w.w = win go func() { @@ -509,21 +509,31 @@ func (w *window) WriteClipboard(s string) { w.clipboard.Call("writeText", s) } -func (w *window) Configure(cnf *config) { - if o := cnf.Title; o != nil { - w.document.Set("title", *o) +func (w *window) Configure(options []Option) { + prev := w.config + cnf := w.config + cnf.apply(unit.Metric{}, options) + if prev.Title != cnf.Title { + w.config.Title = cnf.Title + w.document.Set("title", cnf.Title) } - if o := cnf.WindowMode; o != nil { - w.windowMode(*o) + if prev.Mode != cnf.Mode { + w.windowMode(cnf.Mode) } - if o := cnf.NavigationColor; o != nil { - w.navigationColor(*o) + if prev.NavigationColor != cnf.NavigationColor { + w.config.NavigationColor = cnf.NavigationColor + w.navigationColor(cnf.NavigationColor) } - if o := cnf.Orientation; o != nil { - w.orientation(*o) + if prev.Orientation != cnf.Orientation { + w.config.Orientation = cnf.Orientation + w.orientation(cnf.Orientation) } } +func (w *window) Config() Config { + return w.config +} + func (w *window) SetCursor(name pointer.CursorName) { style := w.cnv.Get("style") style.Set("cursor", string(name)) @@ -559,24 +569,24 @@ func (w *window) resize() { w.scale = float32(w.window.Get("devicePixelRatio").Float()) rect := w.cnv.Call("getBoundingClientRect") - w.size.X = float32(rect.Get("width").Float()) * w.scale - w.size.Y = float32(rect.Get("height").Float()) * w.scale + w.config.Size.X = int(rect.Get("width").Float()) * int(w.scale) + w.config.Size.Y = int(rect.Get("height").Float()) * int(w.scale) if vx, vy := w.visualViewport.Get("width"), w.visualViewport.Get("height"); !vx.IsUndefined() && !vy.IsUndefined() { - w.inset.X = w.size.X - float32(vx.Float())*w.scale - w.inset.Y = w.size.Y - float32(vy.Float())*w.scale + w.inset.X = float32(w.config.Size.X) - float32(vx.Float())*w.scale + w.inset.Y = float32(w.config.Size.Y) - float32(vy.Float())*w.scale } - if w.size.X == 0 || w.size.Y == 0 { + if w.config.Size.X == 0 || w.config.Size.Y == 0 { return } - w.cnv.Set("width", int(w.size.X+.5)) - w.cnv.Set("height", int(w.size.Y+.5)) + w.cnv.Set("width", w.config.Size.X) + w.cnv.Set("height", w.config.Size.Y) } func (w *window) draw(sync bool) { - width, height, insets, metric := w.config() + width, height, insets, metric := w.getConfig() if metric == (unit.Metric{}) || width == 0 || height == 0 { return } @@ -595,8 +605,8 @@ func (w *window) draw(sync bool) { }) } -func (w *window) config() (int, int, system.Insets, unit.Metric) { - return int(w.size.X + .5), int(w.size.Y + .5), system.Insets{ +func (w *window) getConfig() (int, int, system.Insets, unit.Metric) { + return w.config.Size.X, w.config.Size.Y, system.Insets{ Bottom: unit.Px(w.inset.Y), Right: unit.Px(w.inset.X), }, unit.Metric{ @@ -605,9 +615,9 @@ func (w *window) config() (int, int, system.Insets, unit.Metric) { } } -func (w *window) windowMode(mode windowMode) { +func (w *window) windowMode(mode WindowMode) { switch mode { - case windowed: + case Windowed: if !w.document.Get("fullscreenElement").Truthy() { return // Browser is already Windowed. } @@ -615,26 +625,28 @@ func (w *window) windowMode(mode windowMode) { return // Browser doesn't support such feature. } w.document.Call("exitFullscreen") - case fullscreen: + w.config.Mode = Windowed + case Fullscreen: elem := w.document.Get("documentElement") if !elem.Get("requestFullscreen").Truthy() { return // Browser doesn't support such feature. } elem.Call("requestFullscreen") + w.config.Mode = Fullscreen } } -func (w *window) orientation(mode orientation) { +func (w *window) orientation(mode Orientation) { if j := w.screenOrientation; !j.Truthy() || !j.Get("unlock").Truthy() || !j.Get("lock").Truthy() { return // Browser don't support Screen Orientation API. } switch mode { - case anyOrientation: + case AnyOrientation: w.screenOrientation.Call("unlock") - case landscapeOrientation: + case LandscapeOrientation: w.screenOrientation.Call("lock", "landscape").Call("then", w.redraw) - case portraitOrientation: + case PortraitOrientation: w.screenOrientation.Call("lock", "portrait").Call("then", w.redraw) } } diff --git a/app/os_macos.go b/app/os_macos.go index b9e4893e..a80e8a5b 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -152,8 +152,8 @@ type window struct { displayLink *displayLink cursor pointer.CursorName - scale float32 - mode windowMode + scale float32 + config Config } // viewMap is the mapping from Cocoa NSViews to Go windows. @@ -210,50 +210,46 @@ func (w *window) WriteClipboard(s string) { C.writeClipboard(chars, C.NSUInteger(len(u16))) } -func (w *window) Configure(cnf *config) { +func (w *window) Configure(options []Option) { screenScale := float32(C.getScreenBackingScale()) cfg := configFor(screenScale) - val := func(v unit.Value) float32 { - return float32(cfg.Px(v)) / screenScale + prev := w.config + cnf := w.config + cnf.apply(cfg, options) + cnf.Size = cnf.Size.Div(int(screenScale)) + cnf.MinSize = cnf.MinSize.Div(int(screenScale)) + cnf.MaxSize = cnf.MaxSize.Div(int(screenScale)) + + if prev.Size != cnf.Size { + w.config.Size = cnf.Size + C.setSize(w.window, C.CGFloat(cnf.Size.X), C.CGFloat(cnf.Size.Y)) } - if o := cnf.Size; o != nil { - width := val(o.Width) - height := val(o.Height) - if width > 0 || height > 0 { - C.setSize(w.window, C.CGFloat(width), C.CGFloat(height)) - } + if prev.MinSize != cnf.MinSize { + w.config.MinSize = cnf.MinSize + C.setMinSize(w.window, C.CGFloat(cnf.MinSize.X), C.CGFloat(cnf.MinSize.Y)) } - if o := cnf.MinSize; o != nil { - width := val(o.Width) - height := val(o.Height) - if width > 0 || height > 0 { - C.setMinSize(w.window, C.CGFloat(width), C.CGFloat(height)) - } + if prev.MaxSize != cnf.MaxSize { + w.config.MaxSize = cnf.MaxSize + C.setMaxSize(w.window, C.CGFloat(cnf.MaxSize.X), C.CGFloat(cnf.MaxSize.Y)) } - if o := cnf.MaxSize; o != nil { - width := val(o.Width) - height := val(o.Height) - if width > 0 || height > 0 { - C.setMaxSize(w.window, C.CGFloat(width), C.CGFloat(height)) - } - } - if o := cnf.Title; o != nil { - title := C.CString(*o) + + if prev.Title != cnf.Title { + w.config.Title = cnf.Title + title := C.CString(cnf.Title) defer C.free(unsafe.Pointer(title)) C.setTitle(w.window, title) } - if o := cnf.WindowMode; o != nil { - w.SetWindowMode(*o) + if prev.Mode != cnf.Mode { + switch cnf.Mode { + case Windowed, Fullscreen: + w.config.Mode = cnf.Mode + C.toggleFullScreen(w.window) + } } } -func (w *window) SetWindowMode(mode windowMode) { - switch mode { - case w.mode: - case windowed, fullscreen: - C.toggleFullScreen(w.window) - w.mode = mode - } +func (w *window) Config() Config { + return w.config } func (w *window) SetCursor(name pointer.CursorName) { @@ -455,7 +451,7 @@ func gio_onFinishLaunching() { close(launched) } -func newWindow(win *callbacks, cnf *config) error { +func newWindow(win *callbacks, options []Option) error { <-launched errch := make(chan error) runOnMain(func() { @@ -468,7 +464,7 @@ func newWindow(win *callbacks, cnf *config) error { w.w = win w.window = C.gio_createWindow(w.view, nil, 0, 0, 0, 0, 0, 0) win.SetDriver(w) - w.Configure(cnf) + w.Configure(options) if nextTopLeft.x == 0 && nextTopLeft.y == 0 { // cascadeTopLeftFromPoint treats (0, 0) as a no-op, // and just returns the offset we need for the first window. diff --git a/app/os_unix.go b/app/os_unix.go index 518f912c..ee831b30 100644 --- a/app/os_unix.go +++ b/app/os_unix.go @@ -21,19 +21,19 @@ func osMain() { select {} } -type windowDriver func(*callbacks, *config) error +type windowDriver func(*callbacks, []Option) error // Instead of creating files with build tags for each combination of wayland +/- x11 // let each driver initialize these variables with their own version of createWindow. var wlDriver, x11Driver windowDriver -func newWindow(window *callbacks, cnf *config) error { +func newWindow(window *callbacks, options []Option) error { var errFirst error for _, d := range []windowDriver{x11Driver, wlDriver} { if d == nil { continue } - err := d(window, cnf) + err := d(window, options) if err == nil { return nil } diff --git a/app/os_wayland.go b/app/os_wayland.go index 0f857ea1..8ab930cb 100644 --- a/app/os_wayland.go +++ b/app/os_wayland.go @@ -185,10 +185,9 @@ type window struct { needAck bool // The most recent configure serial waiting to be ack'ed. serial C.uint32_t - width int - height int newScale bool scale int + config Config wakeups chan struct{} } @@ -223,12 +222,12 @@ func init() { wlDriver = newWLWindow } -func newWLWindow(window *callbacks, cnf *config) error { +func newWLWindow(window *callbacks, options []Option) error { d, err := newWLDisplay() if err != nil { return err } - w, err := d.createNativeWindow(cnf) + w, err := d.createNativeWindow(options) if err != nil { d.destroy() return err @@ -290,7 +289,7 @@ func (d *wlDisplay) readClipboard() (io.ReadCloser, error) { return r, nil } -func (d *wlDisplay) createNativeWindow(cnf *config) (*window, error) { +func (d *wlDisplay) createNativeWindow(options []Option) (*window, error) { if d.compositor == nil { return nil, errors.New("wayland: no compositor available") } @@ -357,7 +356,7 @@ func (d *wlDisplay) createNativeWindow(cnf *config) (*window, error) { C.xdg_surface_add_listener(w.wmSurf, &C.gio_xdg_surface_listener, unsafe.Pointer(w.surf)) C.xdg_toplevel_add_listener(w.topLvl, &C.gio_xdg_toplevel_listener, unsafe.Pointer(w.surf)) - w.Configure(cnf) + w.Configure(options) if d.decor != nil { // Request server side decorations. @@ -488,8 +487,8 @@ func gio_onToplevelClose(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel) { func gio_onToplevelConfigure(data unsafe.Pointer, topLvl *C.struct_xdg_toplevel, width, height C.int32_t, states *C.struct_wl_array) { w := callbackLoad(data).(*window) if width != 0 && height != 0 { - w.width = int(width) - w.height = int(height) + w.config.Size.X = int(width) + w.config.Size.Y = int(height) w.updateOpaqueRegion() } } @@ -856,7 +855,7 @@ func (w *window) flushFling() { w.fling.xExtrapolation = fling.Extrapolation{} w.fling.yExtrapolation = fling.Extrapolation{} vel := float32(math.Sqrt(float64(estx.Velocity*estx.Velocity + esty.Velocity*esty.Velocity))) - _, _, c := w.config() + _, _, c := w.getConfig() if !w.fling.anim.Start(c, time.Now(), vel) { return } @@ -908,19 +907,26 @@ func (w *window) WriteClipboard(s string) { w.disp.writeClipboard([]byte(s)) } -func (w *window) Configure(cnf *config) { - _, _, cfg := w.config() - if o := cnf.Size; o != nil { - w.width = cfg.Px(o.Width) - w.height = cfg.Px(o.Height) +func (w *window) Configure(options []Option) { + _, _, cfg := w.getConfig() + prev := w.config + cnf := w.config + cnf.apply(cfg, options) + if prev.Size != cnf.Size { + w.config.Size = cnf.Size } - if o := cnf.Title; o != nil { - title := C.CString(*o) + if prev.Title != cnf.Title { + w.config.Title = cnf.Title + title := C.CString(cnf.Title) C.xdg_toplevel_set_title(w.topLvl, title) C.free(unsafe.Pointer(title)) } } +func (w *window) Config() Config { + return w.config +} + func (w *window) SetCursor(name pointer.CursorName) { if name == pointer.CursorNone { C.wl_pointer_set_cursor(w.disp.seat.pointer, w.serial, nil, 0, 0) @@ -1366,7 +1372,7 @@ func (w *window) onPointerMotion(x, y C.wl_fixed_t, t C.uint32_t) { func (w *window) updateOpaqueRegion() { reg := C.wl_compositor_create_region(w.disp.compositor) - C.wl_region_add(reg, 0, 0, C.int32_t(w.width), C.int32_t(w.height)) + C.wl_region_add(reg, 0, 0, C.int32_t(w.config.Size.X), C.int32_t(w.config.Size.Y)) C.wl_surface_set_opaque_region(w.surf, reg) C.wl_region_destroy(reg) } @@ -1396,8 +1402,8 @@ func (w *window) updateOutputs() { } } -func (w *window) config() (int, int, unit.Metric) { - width, height := w.width*w.scale, w.height*w.scale +func (w *window) getConfig() (int, int, unit.Metric) { + width, height := w.config.Size.X*w.scale, w.config.Size.Y*w.scale return width, height, unit.Metric{ PxPerDp: w.ppdp * float32(w.scale), PxPerSp: w.ppsp * float32(w.scale), @@ -1411,7 +1417,7 @@ func (w *window) draw(sync bool) { if dead || (!anim && !sync) { return } - width, height, cfg := w.config() + width, height, cfg := w.getConfig() if cfg == (unit.Metric{}) { return } @@ -1450,7 +1456,7 @@ func (w *window) surface() (*C.struct_wl_surface, int, int) { C.xdg_surface_ack_configure(w.wmSurf, w.serial) w.needAck = false } - width, height, scale := w.width, w.height, w.scale + width, height, scale := w.config.Size.X, w.config.Size.Y, w.scale if w.newScale { C.wl_surface_set_buffer_scale(w.surf, C.int32_t(scale)) w.newScale = false diff --git a/app/os_windows.go b/app/os_windows.go index 46b1acb0..640d7321 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -32,11 +32,6 @@ type ViewEvent struct { HWND uintptr } -type winConstraints struct { - minWidth, minHeight int32 - maxWidth, maxHeight int32 -} - type winDeltas struct { width int32 height int32 @@ -46,8 +41,6 @@ type window struct { hwnd syscall.Handle hdc syscall.Handle w *callbacks - width int - height int stage system.Stage pointerBtns pointer.Buttons @@ -61,9 +54,8 @@ type window struct { animating bool - minmax winConstraints deltas winDeltas - cnf *config + config Config } const _WM_WAKEUP = windows.WM_USER + iota @@ -96,7 +88,7 @@ func osMain() { select {} } -func newWindow(window *callbacks, cnf *config) error { +func newWindow(window *callbacks, options []Option) error { cerr := make(chan error) go func() { // GetMessage and PeekMessage can filter on a window HWND, but @@ -104,7 +96,7 @@ func newWindow(window *callbacks, cnf *config) error { // Instead lock the thread so window messages arrive through // unfiltered GetMessage calls. runtime.LockOSThread() - w, err := createNativeWindow(cnf) + w, err := createNativeWindow() if err != nil { cerr <- err return @@ -115,7 +107,7 @@ func newWindow(window *callbacks, cnf *config) error { w.w = window w.w.SetDriver(w) w.w.Event(ViewEvent{HWND: uintptr(w.hwnd)}) - w.Configure(cnf) + w.Configure(options) windows.ShowWindow(w.hwnd, windows.SW_SHOWDEFAULT) windows.SetForegroundWindow(w.hwnd) windows.SetFocus(w.hwnd) @@ -159,20 +151,7 @@ func initResources() error { return nil } -func getWindowConstraints(cfg unit.Metric, cnf *config) winConstraints { - var minmax winConstraints - if o := cnf.MinSize; o != nil { - minmax.minWidth = int32(cfg.Px(o.Width)) - minmax.minHeight = int32(cfg.Px(o.Height)) - } - if o := cnf.MaxSize; o != nil { - minmax.maxWidth = int32(cfg.Px(o.Width)) - minmax.maxHeight = int32(cfg.Px(o.Height)) - } - return minmax -} - -func createNativeWindow(cnf *config) (*window, error) { +func createNativeWindow() (*window, error) { var resErr error resources.once.Do(func() { resErr = initResources() @@ -180,8 +159,6 @@ func createNativeWindow(cnf *config) (*window, error) { if resErr != nil { return nil, resErr } - dpi := windows.GetSystemDPI() - cfg := configForDPI(dpi) dwStyle := uint32(windows.WS_OVERLAPPEDWINDOW) dwExStyle := uint32(windows.WS_EX_APPWINDOW | windows.WS_EX_WINDOWEDGE) @@ -199,9 +176,7 @@ func createNativeWindow(cnf *config) (*window, error) { return nil, err } w := &window{ - hwnd: hwnd, - minmax: getWindowConstraints(cfg, cnf), - cnf: cnf, + hwnd: hwnd, } w.hdc, err = windows.GetDC(hwnd) if err != nil { @@ -311,16 +286,16 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr } case windows.WM_GETMINMAXINFO: mm := (*windows.MinMaxInfo)(unsafe.Pointer(uintptr(lParam))) - if w.minmax.minWidth > 0 || w.minmax.minHeight > 0 { + if p := w.config.MinSize; p.X > 0 || p.Y > 0 { mm.PtMinTrackSize = windows.Point{ - X: w.minmax.minWidth + w.deltas.width, - Y: w.minmax.minHeight + w.deltas.height, + X: int32(p.X) + w.deltas.width, + Y: int32(p.Y) + w.deltas.height, } } - if w.minmax.maxWidth > 0 || w.minmax.maxHeight > 0 { + if p := w.config.MaxSize; p.X > 0 || p.Y > 0 { mm.PtMaxTrackSize = windows.Point{ - X: w.minmax.maxWidth + w.deltas.width, - Y: w.minmax.maxHeight + w.deltas.height, + X: int32(p.X) + w.deltas.width, + Y: int32(p.Y) + w.deltas.height, } } case windows.WM_SETCURSOR: @@ -453,20 +428,19 @@ func (w *window) setStage(s system.Stage) { func (w *window) draw(sync bool) { var r windows.Rect windows.GetClientRect(w.hwnd, &r) - w.width = int(r.Right - r.Left) - w.height = int(r.Bottom - r.Top) - if w.width == 0 || w.height == 0 { + w.config.Size.X = int(r.Right - r.Left) + w.config.Size.Y = int(r.Bottom - r.Top) + if w.config.Size.X == 0 || w.config.Size.Y == 0 { return } dpi := windows.GetWindowDPI(w.hwnd) cfg := configForDPI(dpi) - w.minmax = getWindowConstraints(cfg, w.cnf) w.w.Event(frameEvent{ FrameEvent: system.FrameEvent{ Now: time.Now(), Size: image.Point{ - X: w.width, - Y: w.height, + X: w.config.Size.X, + Y: w.config.Size.Y, }, Metric: cfg, }, @@ -515,13 +489,17 @@ func (w *window) readClipboard() error { return nil } -func (w *window) Configure(cnf *config) { - w.cnf = cnf - if o := cnf.Size; o != nil { - dpi := windows.GetSystemDPI() - cfg := configForDPI(dpi) - width := int32(cfg.Px(o.Width)) - height := int32(cfg.Px(o.Height)) +func (w *window) Configure(options []Option) { + dpi := windows.GetSystemDPI() + cfg := configForDPI(dpi) + prev := w.config + cnf := w.config + cnf.apply(cfg, options) + + if prev.Size != cnf.Size { + width := int32(cnf.Size.X) + height := int32(cnf.Size.Y) + w.config.Size = cnf.Size // Include the window decorations. wr := windows.Rect{ @@ -538,30 +516,35 @@ func (w *window) Configure(cnf *config) { w.deltas.width = width - dw w.deltas.height = height - dh - w.cnf.Size = o windows.MoveWindow(w.hwnd, 0, 0, width, height, true) } - if o := cnf.MinSize; o != nil { - w.cnf.MinSize = o + if prev.MinSize != cnf.MinSize { + w.config.MinSize = cnf.MinSize } - if o := cnf.MaxSize; o != nil { - w.cnf.MaxSize = o + if prev.MaxSize != cnf.MaxSize { + w.config.MaxSize = cnf.MaxSize } - if o := cnf.Title; o != nil { - windows.SetWindowText(w.hwnd, *cnf.Title) + if prev.Title != cnf.Title { + w.config.Title = cnf.Title + windows.SetWindowText(w.hwnd, cnf.Title) } - if o := cnf.WindowMode; o != nil { - w.SetWindowMode(*o) + if prev.Mode != cnf.Mode { + w.SetWindowMode(cnf.Mode) } } -func (w *window) SetWindowMode(mode windowMode) { +func (w *window) Config() Config { + return w.config +} + +func (w *window) SetWindowMode(mode WindowMode) { // https://devblogs.microsoft.com/oldnewthing/20100412-00/?p=14353 switch mode { - case windowed: + case Windowed: if w.placement == nil { return } + w.config.Mode = Windowed windows.SetWindowPlacement(w.hwnd, w.placement) w.placement = nil style := windows.GetWindowLong(w.hwnd) @@ -570,10 +553,11 @@ func (w *window) SetWindowMode(mode windowMode) { 0, 0, 0, 0, windows.SWP_NOOWNERZORDER|windows.SWP_FRAMECHANGED, ) - case fullscreen: + case Fullscreen: if w.placement != nil { return } + w.config.Mode = Fullscreen w.placement = windows.GetWindowPlacement(w.hwnd) style := windows.GetWindowLong(w.hwnd) windows.SetWindowLong(w.hwnd, windows.GWL_STYLE, style&^windows.WS_OVERLAPPEDWINDOW) @@ -672,7 +656,7 @@ func (w *window) HDC() syscall.Handle { } func (w *window) HWND() (syscall.Handle, int, int) { - return w.hwnd, w.width, w.height + return w.hwnd, w.config.Size.X, w.config.Size.Y } func (w *window) Close() { diff --git a/app/os_x11.go b/app/os_x11.go index 0e55b796..a7b6e469 100644 --- a/app/os_x11.go +++ b/app/os_x11.go @@ -81,9 +81,7 @@ type x11Window struct { wmStateFullscreen C.Atom } stage system.Stage - cfg unit.Metric - width int - height int + metric unit.Metric notify struct { read, write int } @@ -97,7 +95,7 @@ type x11Window struct { content []byte } cursor pointer.CursorName - mode windowMode + config Config wakeups chan struct{} } @@ -116,48 +114,57 @@ func (w *x11Window) WriteClipboard(s string) { C.XSetSelectionOwner(w.x, w.atoms.clipboard, w.xw, C.CurrentTime) } -func (w *x11Window) Configure(cnf *config) { +func (w *x11Window) Configure(options []Option) { var shints C.XSizeHints - if o := cnf.MinSize; o != nil { - shints.min_width = C.int(w.cfg.Px(o.Width)) - shints.min_height = C.int(w.cfg.Px(o.Height)) + prev := w.config + cnf := w.config + cnf.apply(w.metric, options) + if prev.MinSize != cnf.MinSize { + w.config.MinSize = cnf.MinSize + shints.min_width = C.int(cnf.MinSize.X) + shints.min_height = C.int(cnf.MinSize.Y) shints.flags = C.PMinSize } - if o := cnf.MaxSize; o != nil { - shints.max_width = C.int(w.cfg.Px(o.Width)) - shints.max_height = C.int(w.cfg.Px(o.Height)) + if prev.MaxSize != cnf.MaxSize { + w.config.MaxSize = cnf.MaxSize + shints.max_width = C.int(cnf.MaxSize.X) + shints.max_height = C.int(cnf.MaxSize.Y) shints.flags = shints.flags | C.PMaxSize } if shints.flags != 0 { C.XSetWMNormalHints(w.x, w.xw, &shints) } - if o := cnf.Size; o != nil { - C.XResizeWindow(w.x, w.xw, C.uint(w.cfg.Px(o.Width)), C.uint(w.cfg.Px(o.Height))) + if prev.Size != cnf.Size { + w.config.Size = cnf.Size + C.XResizeWindow(w.x, w.xw, C.uint(cnf.Size.X), C.uint(cnf.Size.Y)) } - var title string - if o := cnf.Title; o != nil { - title = *o + if prev.Title != cnf.Title { + title := cnf.Title + ctitle := C.CString(title) + defer C.free(unsafe.Pointer(ctitle)) + C.XStoreName(w.x, w.xw, ctitle) + // set _NET_WM_NAME as well for UTF-8 support in window title. + C.XSetTextProperty(w.x, w.xw, + &C.XTextProperty{ + value: (*C.uchar)(unsafe.Pointer(ctitle)), + encoding: w.atoms.utf8string, + format: 8, + nitems: C.ulong(len(title)), + }, + w.atoms.wmName) } - ctitle := C.CString(title) - defer C.free(unsafe.Pointer(ctitle)) - C.XStoreName(w.x, w.xw, ctitle) - // set _NET_WM_NAME as well for UTF-8 support in window title. - C.XSetTextProperty(w.x, w.xw, - &C.XTextProperty{ - value: (*C.uchar)(unsafe.Pointer(ctitle)), - encoding: w.atoms.utf8string, - format: 8, - nitems: C.ulong(len(title)), - }, - w.atoms.wmName) - if o := cnf.WindowMode; o != nil { - w.SetWindowMode(*o) + if prev.Mode != cnf.Mode { + w.SetWindowMode(cnf.Mode) } } +func (w *x11Window) Config() Config { + return w.config +} + func (w *x11Window) SetCursor(name pointer.CursorName) { switch name { case pointer.CursorNone: @@ -182,13 +189,11 @@ func (w *x11Window) SetCursor(name pointer.CursorName) { C.XDefineCursor(w.x, w.xw, c) } -func (w *x11Window) SetWindowMode(mode windowMode) { +func (w *x11Window) SetWindowMode(mode WindowMode) { switch mode { - case w.mode: - return - case windowed: + case Windowed: C.XDeleteProperty(w.x, w.xw, w.atoms.wmStateFullscreen) - case fullscreen: + case Fullscreen: C.XChangeProperty(w.x, w.xw, w.atoms.wmState, C.XA_ATOM, 32, C.PropModeReplace, (*C.uchar)(unsafe.Pointer(&w.atoms.wmStateFullscreen)), 1, @@ -196,7 +201,7 @@ func (w *x11Window) SetWindowMode(mode windowMode) { default: return } - w.mode = mode + w.config.Mode = mode // "A Client wishing to change the state of a window MUST send // a _NET_WM_STATE client message to the root window (see below)." var xev C.XEvent @@ -261,7 +266,7 @@ func (w *x11Window) display() *C.Display { } func (w *x11Window) window() (C.Window, int, int) { - return w.xw, w.width, w.height + return w.xw, w.config.Size.X, w.config.Size.Y } func (w *x11Window) setStage(s system.Stage) { @@ -327,15 +332,15 @@ loop: default: } - if (anim || syn) && w.width != 0 && w.height != 0 { + if (anim || syn) && w.config.Size.X != 0 && w.config.Size.Y != 0 { w.w.Event(frameEvent{ FrameEvent: system.FrameEvent{ Now: time.Now(), Size: image.Point{ - X: w.width, - Y: w.height, + X: w.config.Size.X, + Y: w.config.Size.Y, }, - Metric: w.cfg, + Metric: w.metric, }, Sync: syn, }) @@ -491,8 +496,7 @@ func (h *x11EventHandler) handleEvents() bool { w.w.Event(key.FocusEvent{Focus: false}) case C.ConfigureNotify: // window configuration change cevt := (*C.XConfigureEvent)(unsafe.Pointer(xev)) - w.width = int(cevt.width) - w.height = int(cevt.height) + w.config.Size = image.Pt(int(cevt.width), int(cevt.height)) // redraw will be done by a later expose event case C.SelectionNotify: cevt := (*C.XSelectionEvent)(unsafe.Pointer(xev)) @@ -583,7 +587,7 @@ func init() { x11Driver = newX11Window } -func newX11Window(gioWin *callbacks, cnf *config) error { +func newX11Window(gioWin *callbacks, options []Option) error { var err error pipe := make([]int, 2) @@ -623,6 +627,9 @@ func newX11Window(gioWin *callbacks, cnf *config) error { ppsp := x11DetectUIScale(dpy) cfg := unit.Metric{PxPerDp: ppsp, PxPerSp: ppsp} + var cnf Config + cnf.apply(cfg, options) + swa := C.XSetWindowAttributes{ event_mask: C.ExposureMask | C.FocusChangeMask | // update C.KeyPressMask | C.KeyReleaseMask | // keyboard @@ -632,24 +639,18 @@ func newX11Window(gioWin *callbacks, cnf *config) error { background_pixmap: C.None, override_redirect: C.False, } - var width, height int - if o := cnf.Size; o != nil { - width = cfg.Px(o.Width) - height = cfg.Px(o.Height) - } win := C.XCreateWindow(dpy, C.XDefaultRootWindow(dpy), - 0, 0, C.uint(width), C.uint(height), + 0, 0, C.uint(cnf.Size.X), C.uint(cnf.Size.Y), 0, C.CopyFromParent, C.InputOutput, nil, C.CWEventMask|C.CWBackPixmap|C.CWOverrideRedirect, &swa) w := &x11Window{ w: gioWin, x: dpy, xw: win, - width: width, - height: height, - cfg: cfg, + metric: cfg, xkb: xkb, xkbEventBase: xkbEventBase, wakeups: make(chan struct{}, 1), + config: cnf, } w.notify.read = pipe[0] w.notify.write = pipe[1] @@ -684,7 +685,7 @@ func newX11Window(gioWin *callbacks, cnf *config) error { // extensions C.XSetWMProtocols(dpy, win, &w.atoms.evDelWindow, 1) - w.Configure(cnf) + w.Configure(options) // make the window visible on the screen C.XMapWindow(dpy, win) diff --git a/app/window.go b/app/window.go index 08b05edb..9405e4a5 100644 --- a/app/window.go +++ b/app/window.go @@ -23,7 +23,7 @@ import ( ) // Option configures a window. -type Option func(cnf *config) +type Option func(unit.Metric, *Config) // Window represents an operating system window. type Window struct { @@ -93,14 +93,13 @@ var ackEvent event.Event // Calling NewWindow more than once is not supported on // iOS, Android, WebAssembly. func NewWindow(options ...Option) *Window { - cnf := new(config) - // Default options. - Size(unit.Dp(800), unit.Dp(600))(cnf) - Title("Gio")(cnf) - - for _, o := range options { - o(cnf) + defaultOptions := []Option{ + Size(unit.Dp(800), unit.Dp(600)), + Title("Gio"), } + options = append(defaultOptions, options...) + var cnf Config + cnf.apply(unit.Metric{}, options) w := &Window{ in: make(chan event.Event), @@ -116,7 +115,7 @@ func NewWindow(options ...Option) *Window { nocontext: cnf.CustomRenderer, } w.callbacks.w = w - go w.run(cnf) + go w.run(options) return w } @@ -260,14 +259,21 @@ func (w *Window) Invalidate() { // Option applies the options to the window. func (w *Window) Option(opts ...Option) { w.driverDefer(func(d driver) { - c := new(config) - for _, opt := range opts { - opt(c) - } - d.Configure(c) + d.Configure(opts) }) } +// Config returns the Window configuration. +// +// A FrameEvent will occur whenever the configuration changes. +func (w *Window) Config() Config { + var cnf Config + w.driverRun(func(d driver) { + cnf = d.Config() + }) + return cnf +} + // ReadClipboard initiates a read of the clipboard in the form // of a clipboard.Event. Multiple reads may be coalesced // to a single event. @@ -491,7 +497,7 @@ func (w *Window) waitFrame() (*op.Ops, bool) { } } -func (w *Window) run(cnf *config) { +func (w *Window) run(options []Option) { // Some OpenGL drivers don't like being made current on many different // OS threads. Force the Go runtime to map the event loop goroutine to // only one thread. @@ -499,7 +505,7 @@ func (w *Window) run(cnf *config) { defer close(w.out) defer close(w.dead) - if err := newWindow(&w.callbacks, cnf); err != nil { + if err := newWindow(&w.callbacks, options); err != nil { w.out <- system.DestroyEvent{Err: err} return } @@ -602,44 +608,10 @@ func (q *queue) Events(k event.Tag) []event.Event { return q.q.Events(k) } -var ( - // Windowed is the normal window mode with OS specific window decorations. - Windowed Option = modeOption(windowed) - // Fullscreen is the full screen window mode. - Fullscreen Option = modeOption(fullscreen) -) - -// WindowMode sets the window mode. -// -// Supported platforms are macOS, X11, Windows and JS. -func modeOption(mode windowMode) Option { - return func(cnf *config) { - cnf.WindowMode = &mode - } -} - -var ( - // AnyOrientation allows the window to be freely orientated. - AnyOrientation Option = orientationOption(anyOrientation) - // LandscapeOrientation constrains the window to landscape orientations. - LandscapeOrientation Option = orientationOption(landscapeOrientation) - // PortraitOrientation constrains the window to portrait orientations. - PortraitOrientation Option = orientationOption(portraitOrientation) -) - -// orientation sets the orientation of the app. -// -// Supported platforms are Android and JS. -func orientationOption(mode orientation) Option { - return func(cnf *config) { - cnf.Orientation = &mode - } -} - // Title sets the title of the window. func Title(t string) Option { - return func(cnf *config) { - cnf.Title = &t + return func(_ unit.Metric, cnf *Config) { + cnf.Title = t } } @@ -651,10 +623,10 @@ func Size(w, h unit.Value) Option { if h.V <= 0 { panic("height must be larger than or equal to 0") } - return func(cnf *config) { - cnf.Size = &size{ - Width: w, - Height: h, + return func(m unit.Metric, cnf *Config) { + cnf.Size = image.Point{ + X: m.Px(w), + Y: m.Px(h), } } } @@ -667,10 +639,10 @@ func MaxSize(w, h unit.Value) Option { if h.V <= 0 { panic("height must be larger than or equal to 0") } - return func(cnf *config) { - cnf.MaxSize = &size{ - Width: w, - Height: h, + return func(m unit.Metric, cnf *Config) { + cnf.MaxSize = image.Point{ + X: m.Px(w), + Y: m.Px(h), } } } @@ -683,32 +655,32 @@ func MinSize(w, h unit.Value) Option { if h.V <= 0 { panic("height must be larger than or equal to 0") } - return func(cnf *config) { - cnf.MinSize = &size{ - Width: w, - Height: h, + return func(m unit.Metric, cnf *Config) { + cnf.MinSize = image.Point{ + X: m.Px(w), + Y: m.Px(h), } } } // StatusColor sets the color of the Android status bar. func StatusColor(color color.NRGBA) Option { - return func(cnf *config) { - cnf.StatusColor = &color + return func(_ unit.Metric, cnf *Config) { + cnf.StatusColor = color } } // NavigationColor sets the color of the navigation bar on Android, or the address bar in browsers. func NavigationColor(color color.NRGBA) Option { - return func(cnf *config) { - cnf.NavigationColor = &color + return func(_ unit.Metric, cnf *Config) { + cnf.NavigationColor = color } } -// CustomRenderer controls whether the the window contents is +// CustomRenderer controls whether the window contents is // rendered by the client. If true, no GPU context is created. func CustomRenderer(custom bool) Option { - return func(cnf *config) { + return func(_ unit.Metric, cnf *Config) { cnf.CustomRenderer = custom } }