shiny: add window title support

Titles are UTF-8 compatible on all platforms; drivers
default to an empty string.

Change-Id: I4cf1a16ceb4e9641b2e1fc78dcff0501aac7dedb
Reviewed-on: https://go-review.googlesource.com/37200
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/shiny/driver/gldriver/cocoa.go b/shiny/driver/gldriver/cocoa.go
index b089c64..c41ec3c 100644
--- a/shiny/driver/gldriver/cocoa.go
+++ b/shiny/driver/gldriver/cocoa.go
@@ -16,12 +16,13 @@
 #import <Cocoa/Cocoa.h>
 #include <pthread.h>
 #include <stdint.h>
+#include <stdlib.h>
 
 void startDriver();
 void stopDriver();
 void makeCurrentContext(uintptr_t ctx);
 void flushContext(uintptr_t ctx);
-uintptr_t doNewWindow(int width, int height);
+uintptr_t doNewWindow(int width, int height, char* title);
 void doShowWindow(uintptr_t id);
 void doCloseWindow(uintptr_t id);
 uint64_t threadID();
@@ -33,6 +34,7 @@
 	"fmt"
 	"log"
 	"runtime"
+	"unsafe"
 
 	"golang.org/x/exp/shiny/driver/internal/lifecycler"
 	"golang.org/x/exp/shiny/screen"
@@ -62,7 +64,15 @@
 
 func newWindow(opts *screen.NewWindowOptions) (uintptr, error) {
 	width, height := optsSize(opts)
-	return uintptr(C.doNewWindow(C.int(width), C.int(height))), nil
+
+	var title string
+	if opts != nil {
+		title = opts.Title
+	}
+	titlePtr := C.CString(title)
+	defer C.free(unsafe.Pointer(titlePtr))
+
+	return uintptr(C.doNewWindow(C.int(width), C.int(height), titlePtr)), nil
 }
 
 func initWindow(w *windowImpl) {
diff --git a/shiny/driver/gldriver/cocoa.m b/shiny/driver/gldriver/cocoa.m
index 8da18f1..e8e863e 100644
--- a/shiny/driver/gldriver/cocoa.m
+++ b/shiny/driver/gldriver/cocoa.m
@@ -241,7 +241,7 @@
 }
 @end
 
-uintptr_t doNewWindow(int width, int height) {
+uintptr_t doNewWindow(int width, int height, char* title) {
 	NSScreen *screen = [NSScreen mainScreen];
 	double w = (double)width / [screen backingScaleFactor];
 	double h = (double)height / [screen backingScaleFactor];
@@ -254,7 +254,7 @@
 		[NSApp setMainMenu:menuBar];
 
 		id menu = [NSMenu new];
-		NSString* name = [[NSProcessInfo processInfo] processName];
+		NSString* name = [[NSString alloc] initWithUTF8String:title];
 
 		id hideMenuItem = [[NSMenuItem alloc] initWithTitle:@"Hide"
 			action:@selector(hide:) keyEquivalent:@"h"];
diff --git a/shiny/driver/gldriver/x11.c b/shiny/driver/gldriver/x11.c
index 58ca910..11d7084 100644
--- a/shiny/driver/gldriver/x11.c
+++ b/shiny/driver/gldriver/x11.c
@@ -8,10 +8,14 @@
 #include <EGL/egl.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 
+Atom utf8_string;
 Atom wm_delete_window;
+Atom wm_name;
 Atom wm_protocols;
 Atom wm_take_focus;
+
 EGLConfig e_config;
 EGLContext e_ctx;
 EGLDisplay e_dpy;
@@ -127,9 +131,11 @@
 		exit(1);
 	}
 
+	utf8_string = XInternAtom(x_dpy, "UTF8_STRING", False);
 	wm_delete_window = XInternAtom(x_dpy, "WM_DELETE_WINDOW", False);
+	wm_name = XInternAtom(x_dpy, "WM_NAME", False);
 	wm_protocols = XInternAtom(x_dpy, "WM_PROTOCOLS", False);
-	wm_take_focus= XInternAtom(x_dpy, "WM_TAKE_FOCUS", False);
+	wm_take_focus = XInternAtom(x_dpy, "WM_TAKE_FOCUS", False);
 
 	const int key_lo = 8;
 	const int key_hi = 255;
@@ -225,7 +231,7 @@
 }
 
 uintptr_t
-doNewWindow(int width, int height) {
+doNewWindow(int width, int height, char* title) {
 	XSetWindowAttributes attr;
 	attr.colormap = x_colormap;
 	attr.event_mask =
@@ -253,7 +259,9 @@
 	atoms[1] = wm_take_focus;
 	XSetWMProtocols(x_dpy, win, atoms, 2);
 
-	XSetStandardProperties(x_dpy, win, "App", "App", None, (char **)NULL, 0, &sizehints);
+	XSetStandardProperties(x_dpy, win, "", "App", None, (char **)NULL, 0, &sizehints);
+	XChangeProperty(x_dpy, win, wm_name, utf8_string, 8, PropModeReplace, title, strlen(title));
+
 	return win;
 }
 
diff --git a/shiny/driver/gldriver/x11.go b/shiny/driver/gldriver/x11.go
index 03f68e6..0985dd7 100644
--- a/shiny/driver/gldriver/x11.go
+++ b/shiny/driver/gldriver/x11.go
@@ -11,6 +11,7 @@
 
 #include <stdbool.h>
 #include <stdint.h>
+#include <stdlib.h>
 
 char *eglGetErrorStr();
 void startDriver();
@@ -18,7 +19,7 @@
 void makeCurrent(uintptr_t ctx);
 void swapBuffers(uintptr_t ctx);
 void doCloseWindow(uintptr_t id);
-uintptr_t doNewWindow(int width, int height);
+uintptr_t doNewWindow(int width, int height, char* title);
 uintptr_t doShowWindow(uintptr_t id);
 uintptr_t surfaceCreate();
 */
@@ -27,6 +28,7 @@
 	"errors"
 	"runtime"
 	"time"
+	"unsafe"
 
 	"golang.org/x/exp/shiny/driver/internal/x11key"
 	"golang.org/x/exp/shiny/screen"
@@ -50,10 +52,18 @@
 
 func newWindow(opts *screen.NewWindowOptions) (uintptr, error) {
 	width, height := optsSize(opts)
+
+	var title string
+	if opts != nil {
+		title = opts.Title
+	}
+	titlePtr := C.CString(title)
+	defer C.free(unsafe.Pointer(titlePtr))
+
 	retc := make(chan uintptr)
 	uic <- uiClosure{
 		f: func() uintptr {
-			return uintptr(C.doNewWindow(C.int(width), C.int(height)))
+			return uintptr(C.doNewWindow(C.int(width), C.int(height), titlePtr))
 		},
 		retc: retc,
 	}
diff --git a/shiny/driver/internal/win32/win32.go b/shiny/driver/internal/win32/win32.go
index 78895bd..151d198 100644
--- a/shiny/driver/internal/win32/win32.go
+++ b/shiny/driver/internal/win32/win32.go
@@ -66,7 +66,11 @@
 	if err != nil {
 		return 0, err
 	}
-	title, err := syscall.UTF16PtrFromString("Shiny Window")
+	var title string
+	if opts != nil {
+		title = opts.Title
+	}
+	windowTitle, err := syscall.UTF16PtrFromString(title)
 	if err != nil {
 		return 0, err
 	}
@@ -80,7 +84,7 @@
 		}
 	}
 	hwnd, err := _CreateWindowEx(0,
-		wcname, title,
+		wcname, windowTitle,
 		_WS_OVERLAPPEDWINDOW,
 		_CW_USEDEFAULT, _CW_USEDEFAULT,
 		int32(w), int32(h),
diff --git a/shiny/driver/x11driver/screen.go b/shiny/driver/x11driver/screen.go
index a9208c5..2f9cc71 100644
--- a/shiny/driver/x11driver/screen.go
+++ b/shiny/driver/x11driver/screen.go
@@ -33,6 +33,7 @@
 	xsi     *xproto.ScreenInfo
 	keysyms x11key.KeysymTable
 
+	atomUTF8String     xproto.Atom
 	atomWMDeleteWindow xproto.Atom
 	atomWMProtocols    xproto.Atom
 	atomWMTakeFocus    xproto.Atom
@@ -421,6 +422,13 @@
 		},
 	)
 	s.setProperty(xw, s.atomWMProtocols, s.atomWMDeleteWindow, s.atomWMTakeFocus)
+
+	var title []byte
+	if opts != nil {
+		title = []byte(opts.Title)
+	}
+	xproto.ChangeProperty(s.xc, xproto.PropModeReplace, xw, xproto.AtomWmName, s.atomUTF8String, 8, uint32(len(title)), title)
+
 	xproto.CreateGC(s.xc, xg, xproto.Drawable(xw), 0, nil)
 	render.CreatePicture(s.xc, xp, xproto.Drawable(xw), pictformat, 0, nil)
 	xproto.MapWindow(s.xc, xw)
@@ -429,6 +437,10 @@
 }
 
 func (s *screenImpl) initAtoms() (err error) {
+	s.atomUTF8String, err = s.internAtom("UTF8_STRING")
+	if err != nil {
+		return err
+	}
 	s.atomWMDeleteWindow, err = s.internAtom("WM_DELETE_WINDOW")
 	if err != nil {
 		return err
diff --git a/shiny/screen/screen.go b/shiny/screen/screen.go
index c4b326b..d3c7d7b 100644
--- a/shiny/screen/screen.go
+++ b/shiny/screen/screen.go
@@ -232,7 +232,10 @@
 	// zero value dimension.
 	Width, Height int
 
-	// TODO: fullscreen, title, icon, cursorHidden?
+	// Title specifies the window title.
+	Title string
+
+	// TODO: fullscreen, icon, cursorHidden?
 }
 
 // Uploader is something you can upload a Buffer to.