shithub: drawterm

ref: 229d7bb35315ade6f25d96071adfd6933a50015d
dir: /gui-cocoa/screen.m/

View raw version
#define Rect RectC
#import <Cocoa/Cocoa.h>
#import <Metal/Metal.h>
#import <QuartzCore/CAMetalLayer.h>
#undef Rect

#undef nil
#define Point Point9
#include "u.h"
#include "lib.h"
#include "kern/dat.h"
#include "kern/fns.h"
#include "error.h"
#include "user.h"
#include <draw.h>
#include <memdraw.h>
#include "screen.h"
#include "keyboard.h"

#ifndef DEBUG
#define DEBUG 0
#endif
#define LOG(fmt, ...) if(DEBUG)NSLog((@"%s:%d %s " fmt), __FILE__, __LINE__, __PRETTY_FUNCTION__, ##__VA_ARGS__)

Memimage *gscreen;

@interface DrawLayer : CAMetalLayer
@property id<MTLTexture> texture;
@end

@interface DrawtermView : NSView<NSTextInputClient>
- (void)reshape;
- (void)clearMods;
- (void)clearInput;
- (void)mouseevent:(NSEvent *)e;
- (void)resetLastInputRect;
- (void)enlargeLastInputRect:(NSRect)r;
@end

@interface AppDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate>
@end

static AppDelegate *myApp;
static DrawtermView *myview;
static NSCursor *currentCursor;

static ulong pal[256];

static int readybit;
static Rendez rend;

static int
isready(void*a)
{
	return readybit;
}

void
guimain(void)
{
	LOG();
	@autoreleasepool{
		[NSApplication sharedApplication];
		[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
		myApp = [AppDelegate new];
		[NSApp setDelegate:myApp];
		[NSApp run];
	}
}

void
screeninit(void)
{
	memimageinit();
	NSSize s = [myview convertSizeToBacking:myview.frame.size];
	screensize(Rect(0, 0, s.width, s.height), ARGB32);
	gscreen->clipr = Rect(0, 0, s.width, s.height);
	LOG(@"%g %g", s.width, s.height);
	terminit();
	readybit = 1;
	wakeup(&rend);
}

void
screensize(Rectangle r, ulong chan)
{
	Memimage *i;

	if((i = allocmemimage(r, chan)) == nil)
		return;
	if(gscreen != nil)
		freememimage(gscreen);
@autoreleasepool{
	DrawLayer *layer = (DrawLayer *)myview.layer;
	MTLTextureDescriptor *textureDesc = [MTLTextureDescriptor
		texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
		width:Dx(r)
		height:Dy(r)
		mipmapped:NO];
	textureDesc.allowGPUOptimizedContents = YES;
	textureDesc.usage = MTLTextureUsageShaderRead;
	textureDesc.cpuCacheMode = MTLCPUCacheModeWriteCombined;
	layer.texture = [layer.device newTextureWithDescriptor:textureDesc];

	CGFloat scale = myview.window.backingScaleFactor;
	[layer setDrawableSize:NSMakeSize(Dx(r), Dy(r))];
	[layer setContentsScale:scale];
}
	gscreen = i;
	gscreen->clipr = ZR;
}

Memdata*
attachscreen(Rectangle *r, ulong *chan, int *depth, int *width, int *softscreen)
{
	LOG();
	*r = gscreen->clipr;
	*chan = gscreen->chan;
	*depth = gscreen->depth;
	*width = gscreen->width;
	*softscreen = 1;

	gscreen->data->ref++;
	return gscreen->data;
}

char *
clipread(void)
{
	@autoreleasepool{
		NSPasteboard *pb = [NSPasteboard generalPasteboard];
		NSString *s = [pb stringForType:NSPasteboardTypeString];
		if(s)
			return strdup([s UTF8String]);
	}
	return nil;
}

int
clipwrite(char *buf)
{
	@autoreleasepool{
		NSString *s = [[NSString alloc] initWithUTF8String:buf];
		NSPasteboard *pb = [NSPasteboard generalPasteboard];
		[pb clearContents];
		[pb writeObjects:@[s]];
	}
	return strlen(buf);
}

void
flushmemscreen(Rectangle r)
{
	LOG(@"<- %d %d %d %d", r.min.x, r.min.y, Dx(r), Dy(r));
	if(rectclip(&r, gscreen->clipr) == 0)
		return;
	LOG(@"-> %d %d %d %d", r.min.x, r.min.y, Dx(r), Dy(r));
	@autoreleasepool{
		[((DrawLayer *)myview.layer).texture
			replaceRegion:MTLRegionMake2D(r.min.x, r.min.y, Dx(r), Dy(r))
			mipmapLevel:0
			withBytes:byteaddr(gscreen, Pt(r.min.x, r.min.y))
			bytesPerRow:gscreen->width * 4];
		NSRect sr = [[myview window] convertRectFromBacking:NSMakeRect(r.min.x, r.min.y, Dx(r), Dy(r))];
		dispatch_async(dispatch_get_main_queue(), ^(void){@autoreleasepool{
			LOG(@"setNeedsDisplayInRect %g %g %g %g", sr.origin.x, sr.origin.y, sr.size.width, sr.size.height);
			[myview setNeedsDisplayInRect:sr];
			[myview enlargeLastInputRect:sr];
		}});
		// ReplaceRegion is somehow asynchronous since 10.14.5.  We wait sometime to request a update again.
		dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 8 * NSEC_PER_MSEC);
		dispatch_after(time, dispatch_get_main_queue(), ^(void){@autoreleasepool{
			LOG(@"setNeedsDisplayInRect %g %g %g %g again", sr.origin.x, sr.origin.y, sr.size.width, sr.size.height);
			[myview setNeedsDisplayInRect:sr];
		}});
		time = dispatch_time(DISPATCH_TIME_NOW, 16 * NSEC_PER_MSEC);
		dispatch_after(time, dispatch_get_main_queue(), ^(void){@autoreleasepool{
			LOG(@"setNeedsDisplayInRect %g %g %g %g again", sr.origin.x, sr.origin.y, sr.size.width, sr.size.height);
			[myview setNeedsDisplayInRect:sr];
		}});
	}
}

void
getcolor(ulong i, ulong *r, ulong *g, ulong *b)
{
	ulong v;

	v = pal[i];
	*r = (v>>16)&0xFF;
	*g = (v>>8)&0xFF;
	*b = v&0xFF;
}

void
setcolor(ulong i, ulong r, ulong g, ulong b)
{
	pal[i] = ((r&0xFF)<<16) & ((g&0xFF)<<8) & (b&0xFF);
}

void
setcursor(void)
{
	static unsigned char data[64], data2[256];
	unsigned char *planes[2] = {&data[0], &data[32]};
	unsigned char *planes2[2] = {&data2[0], &data2[128]};
	unsigned int i, x, y, a;
	unsigned char pu, pb, pl, pr, pc;  // upper, bottom, left, right, center
	unsigned char pul, pur, pbl, pbr;
	unsigned char ful, fur, fbl, fbr;

	lock(&cursor.lk);
	for(i = 0; i < 32; i++){
		data[i] = ~cursor.set[i] & cursor.clr[i];
		data[i+32] = cursor.set[i] | cursor.clr[i];
	}
	for(a=0; a<2; a++){
		for(y=0; y<16; y++){
			for(x=0; x<2; x++){
				pc = planes[a][x+2*y];
				pu = y==0 ? pc : planes[a][x+2*(y-1)];
				pb = y==15 ? pc : planes[a][x+2*(y+1)];
				pl = (pc>>1) | (x==0 ? pc&0x80 : (planes[a][x-1+2*y]&1)<<7);
				pr = (pc<<1) | (x==1 ? pc&1 : (planes[a][x+1+2*y]&0x80)>>7);
				ful = ~(pl^pu) & (pl^pb) & (pu^pr);
				pul = (ful & pu) | (~ful & pc);
				fur = ~(pu^pr) & (pu^pl) & (pr^pb);
				pur = (fur & pr) | (~fur & pc);
				fbl = ~(pb^pl) & (pb^pr) & (pl^pu);
				pbl = (fbl & pl) | (~fbl & pc);
				fbr = ~(pr^pb) & (pr^pu) & (pb^pl);
				pbr = (fbr & pb) | (~fbr & pc);
				planes2[a][2*x+4*2*y] = (pul&0x80) | ((pul&0x40)>>1)  | ((pul&0x20)>>2) | ((pul&0x10)>>3)
					| ((pur&0x80)>>1) | ((pur&0x40)>>2)  | ((pur&0x20)>>3) | ((pur&0x10)>>4);
				planes2[a][2*x+1+4*2*y] = ((pul&0x8)<<4) | ((pul&0x4)<<3)  | ((pul&0x2)<<2) | ((pul&0x1)<<1)
					| ((pur&0x8)<<3) | ((pur&0x4)<<2)  | ((pur&0x2)<<1) | (pur&0x1);
				planes2[a][2*x+4*(2*y+1)] =  (pbl&0x80) | ((pbl&0x40)>>1)  | ((pbl&0x20)>>2) | ((pbl&0x10)>>3)
					| ((pbr&0x80)>>1) | ((pbr&0x40)>>2)  | ((pbr&0x20)>>3) | ((pbr&0x10)>>4);
				planes2[a][2*x+1+4*(2*y+1)] = ((pbl&0x8)<<4) | ((pbl&0x4)<<3)  | ((pbl&0x2)<<2) | ((pbl&0x1)<<1)
					| ((pbr&0x8)<<3) | ((pbr&0x4)<<2)  | ((pbr&0x2)<<1) | (pbr&0x1);
			}
		}
	}
	NSBitmapImageRep *rep = [[NSBitmapImageRep alloc]
		initWithBitmapDataPlanes:planes
		pixelsWide:16
		pixelsHigh:16
		bitsPerSample:1
		samplesPerPixel:2
		hasAlpha:YES
		isPlanar:YES
		colorSpaceName:NSDeviceWhiteColorSpace
		bitmapFormat:0
		bytesPerRow:2
		bitsPerPixel:0];
	NSBitmapImageRep *rep2 = [[NSBitmapImageRep alloc]
		initWithBitmapDataPlanes:planes2
		pixelsWide:32
		pixelsHigh:32
		bitsPerSample:1
		samplesPerPixel:2
		hasAlpha:YES
		isPlanar:YES
		colorSpaceName:NSDeviceWhiteColorSpace
		bitmapFormat:0
		bytesPerRow:4
		bitsPerPixel:0];
	NSImage *img = [[NSImage alloc] initWithSize:NSMakeSize(16, 16)];
	[img addRepresentation:rep2];
	[img addRepresentation:rep];
	currentCursor = [[NSCursor alloc] initWithImage:img hotSpot:NSMakePoint(-cursor.offset.x, -cursor.offset.y)];
	unlock(&cursor.lk);

	dispatch_async(dispatch_get_main_queue(), ^(void){
		[[myview window] invalidateCursorRectsForView:myview];
	});
}

void
mouseset(Point p)
{
	dispatch_async(dispatch_get_main_queue(), ^(void){@autoreleasepool{
		NSPoint s;

		if([[myview window] isKeyWindow]){
			s = NSMakePoint(p.x, p.y);
			LOG(@"-> pixel  %g %g", s.x, s.y);
			s = [[myview window] convertPointFromBacking:s];
			LOG(@"-> point  %g %g", s.x, s.y);
			s = [myview convertPoint:s toView:nil];
			LOG(@"-> window %g %g", s.x, s.y);
			s = [[myview window] convertPointToScreen: s];
			LOG(@"(%g, %g) <- toScreen", s.x, s.y);
			s.y = NSScreen.screens[0].frame.size.height - s.y;
			LOG(@"(%g, %g) <- setmouse", s.x, s.y);
			CGWarpMouseCursorPosition(s);
			CGAssociateMouseAndMouseCursorPosition(true);
		}
	}});
}

@implementation AppDelegate
{
	NSWindow *_window;
}

static void
mainproc(void *aux)
{
	cpubody();
}

- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender
{
	if([sender mainWindow] == nil)
		return NSTerminateNow;

	NSAlert *alert = [[NSAlert alloc] init];
	[alert setMessageText:@"Really quit drawterm?"];
	[alert addButtonWithTitle:@"Yes"];
	[alert addButtonWithTitle:@"Cancel"];
	[alert setAlertStyle:NSAlertStyleCritical];
	int choice = [alert runModal];
	if(choice == NSAlertFirstButtonReturn)
		return NSTerminateNow;
	return NSTerminateCancel;
}

- (void) applicationDidFinishLaunching:(NSNotification *)aNotification
{
	LOG(@"BEGIN");

	NSMenu *sm = [NSMenu new];
	[sm addItemWithTitle:@"Toggle Full Screen" action:@selector(toggleFullScreen:) keyEquivalent:@"F"];
	[sm addItemWithTitle:@"Hide" action:@selector(hide:) keyEquivalent:@"H"];
	[sm addItemWithTitle:@"Quit" action:@selector(terminate:) keyEquivalent:@"q"];
	NSMenu *m = [NSMenu new];
	[m addItemWithTitle:@"DEVDRAW" action:NULL keyEquivalent:@""];
	[m setSubmenu:sm forItem:[m itemWithTitle:@"DEVDRAW"]];
	[NSApp setMainMenu:m];

	const NSWindowStyleMask Winstyle = NSWindowStyleMaskTitled
		| NSWindowStyleMaskClosable
		| NSWindowStyleMaskMiniaturizable
		| NSWindowStyleMaskResizable;

	NSRect r = [[NSScreen mainScreen] visibleFrame];

	r.size.width = r.size.width*3/4;
	r.size.height = r.size.height*3/4;
	r = [NSWindow contentRectForFrameRect:r styleMask:Winstyle];

	_window = [[NSWindow alloc] initWithContentRect:r styleMask:Winstyle
		backing:NSBackingStoreBuffered defer:NO];
	[_window setTitle:@"drawterm"];
	[_window center];
	[_window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
	[_window setContentMinSize:NSMakeSize(64,64)];
	[_window setOpaque:YES];
	[_window setRestorable:NO];
	[_window setAcceptsMouseMovedEvents:YES];
	[_window setDelegate:self];

	myview = [DrawtermView new];
	[_window setContentView:myview];

	[NSEvent setMouseCoalescingEnabled:NO];
	setcursor();

	[_window makeKeyAndOrderFront:self];
	[NSApp activateIgnoringOtherApps:YES];

	LOG(@"launch mainproc");
	kproc("mainproc", mainproc, 0);
	ksleep(&rend, isready, 0);
}

- (NSApplicationPresentationOptions) window:(NSWindow *)window
		willUseFullScreenPresentationOptions:(NSApplicationPresentationOptions)proposedOptions
{
	NSApplicationPresentationOptions o;
	o = proposedOptions;
	o &= ~(NSApplicationPresentationAutoHideDock | NSApplicationPresentationAutoHideMenuBar);
	o |= NSApplicationPresentationHideDock | NSApplicationPresentationHideMenuBar;
	return o;
}

- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
{
	return YES;
}

- (void) windowDidBecomeKey:(id)arg
{
	NSPoint p;
	p = [_window convertPointToBacking:[_window mouseLocationOutsideOfEventStream]];
	absmousetrack(p.x, [myview convertSizeToBacking:myview.frame.size].height - p.y, 0, ticks());
}

- (void) windowDidResignKey:(id)arg
{
	[myview clearMods];
}

@end

@implementation DrawtermView
{
	NSMutableString *_tmpText;
	NSRange _markedRange;
	NSRange _selectedRange;
	NSRect _lastInputRect;	// The view is flipped, this is not.
	BOOL _tapping;
	NSUInteger _tapFingers;
	NSUInteger _tapTime;
	BOOL _breakcompose;
	NSEventModifierFlags _mods;
}

- (id) initWithFrame:(NSRect)fr
{
	LOG(@"BEGIN");
	self = [super initWithFrame:fr];
	[self setWantsLayer:YES];
	[self setLayerContentsRedrawPolicy:NSViewLayerContentsRedrawOnSetNeedsDisplay];
	[self setAllowedTouchTypes:NSTouchTypeMaskDirect|NSTouchTypeMaskIndirect];
	_tmpText = [[NSMutableString alloc] initWithCapacity:2];
	_markedRange = NSMakeRange(NSNotFound, 0);
	_selectedRange = NSMakeRange(0, 0);
	_breakcompose = NO;
	_mods = 0;
	LOG(@"END");
	return self;
}

- (CALayer *) makeBackingLayer
{
	return [DrawLayer layer];
}

- (BOOL)wantsUpdateLayer
{
	return YES;
}

- (BOOL)isOpaque
{
	return YES;
}

- (BOOL)isFlipped
{
	return YES;
}

static uint
evkey(uint v)
{
	switch(v){
	case '\r': return '\n';
	case 127: return '\b';
	case NSUpArrowFunctionKey: return Kup;
	case NSDownArrowFunctionKey: return Kdown;
	case NSLeftArrowFunctionKey: return Kleft;
	case NSRightArrowFunctionKey: return Kright;
	case NSF1FunctionKey: return KF|1;
	case NSF2FunctionKey: return KF|2;
	case NSF3FunctionKey: return KF|3;
	case NSF4FunctionKey: return KF|4;
	case NSF5FunctionKey: return KF|5;
	case NSF6FunctionKey: return KF|6;
	case NSF7FunctionKey: return KF|7;
	case NSF8FunctionKey: return KF|8;
	case NSF9FunctionKey: return KF|9;
	case NSF10FunctionKey: return KF|10;
	case NSF11FunctionKey: return KF|11;
	case NSF12FunctionKey: return KF|12;
	case NSInsertFunctionKey: return Kins;
	case NSDeleteFunctionKey: return Kdel;
	case NSHomeFunctionKey: return Khome;
	case NSEndFunctionKey: return Kend;
	case NSPageUpFunctionKey: return Kpgup;
	case NSPageDownFunctionKey: return Kpgdown;
	case NSScrollLockFunctionKey: return Kscroll;
	case NSBeginFunctionKey:
	case NSF13FunctionKey:
	case NSF14FunctionKey:
	case NSF15FunctionKey:
	case NSF16FunctionKey:
	case NSF17FunctionKey:
	case NSF18FunctionKey:
	case NSF19FunctionKey:
	case NSF20FunctionKey:
	case NSF21FunctionKey:
	case NSF22FunctionKey:
	case NSF23FunctionKey:
	case NSF24FunctionKey:
	case NSF25FunctionKey:
	case NSF26FunctionKey:
	case NSF27FunctionKey:
	case NSF28FunctionKey:
	case NSF29FunctionKey:
	case NSF30FunctionKey:
	case NSF31FunctionKey:
	case NSF32FunctionKey:
	case NSF33FunctionKey:
	case NSF34FunctionKey:
	case NSF35FunctionKey:
	case NSPrintScreenFunctionKey:
	case NSPauseFunctionKey:
	case NSSysReqFunctionKey:
	case NSBreakFunctionKey:
	case NSResetFunctionKey:
	case NSStopFunctionKey:
	case NSMenuFunctionKey:
	case NSUserFunctionKey:
	case NSSystemFunctionKey:
	case NSPrintFunctionKey:
	case NSClearLineFunctionKey:
	case NSClearDisplayFunctionKey:
	case NSInsertLineFunctionKey:
	case NSDeleteLineFunctionKey:
	case NSInsertCharFunctionKey:
	case NSDeleteCharFunctionKey:
	case NSPrevFunctionKey:
	case NSNextFunctionKey:
	case NSSelectFunctionKey:
	case NSExecuteFunctionKey:
	case NSUndoFunctionKey:
	case NSRedoFunctionKey:
	case NSFindFunctionKey:
	case NSHelpFunctionKey:
	case NSModeSwitchFunctionKey: return 0;
	default: return v;
	}
}

- (void)keyDown:(NSEvent*)event {
	[self interpretKeyEvents:[NSArray arrayWithObject:event]];
	[self resetLastInputRect];
}

- (void)flagsChanged:(NSEvent*)event {
	NSEventModifierFlags x;
	NSUInteger u;

	x = [event modifierFlags];
	u = [NSEvent pressedMouseButtons];
	u = (u&~6) | (u&4)>>1 | (u&2)<<1;
	if((x & ~_mods & NSEventModifierFlagShift) != 0)
		kbdkey(Kshift, 1);
	if((x & ~_mods & NSEventModifierFlagControl) != 0){
		if(u){
			u |= 1;
			[self sendmouse:u];
			return;
		}else
			kbdkey(Kctl, 1);
	}
	if((x & ~_mods & NSEventModifierFlagOption) != 0){
		if(u){
			u |= 2;
			[self sendmouse:u];
			return;
		}else
			kbdkey(Kalt, 1);
	}
	if((x & NSEventModifierFlagCommand) != 0){
		if(u){
			u |= 4;
			[self sendmouse:u];
		}else
			kbdkey(Kmod4, 1);
	}
	if((x & ~_mods & NSEventModifierFlagCapsLock) != 0)
		kbdkey(Kcaps, 1);
	if((~x & _mods & NSEventModifierFlagShift) != 0)
		kbdkey(Kshift, 0);
	if((~x & _mods & NSEventModifierFlagControl) != 0)
		kbdkey(Kctl, 0);
	if((~x & _mods & NSEventModifierFlagOption) != 0){
		kbdkey(Kalt, 0);
		if(_breakcompose){
			kbdkey(Kalt, 1);
			kbdkey(Kalt, 0);
			_breakcompose = NO;
		}
	}
	if((~x & NSEventModifierFlagCommand) != 0)
		kbdkey(Kmod4, 0);
	if((~x & _mods & NSEventModifierFlagCapsLock) != 0)
		kbdkey(Kcaps, 0);
	_mods = x;
}

- (void) clearMods {
	if((_mods & NSEventModifierFlagShift) != 0){
		kbdkey(Kshift, 0);
		_mods ^= NSEventModifierFlagShift;
	}
	if((_mods & NSEventModifierFlagControl) != 0){
		kbdkey(Kctl, 0);
		_mods ^= NSEventModifierFlagControl;
	}
	if((_mods & NSEventModifierFlagOption) != 0){
		kbdkey(Kalt, 0);
		_mods ^= NSEventModifierFlagOption;
	}
	if((_mods & NSEventModifierFlagCommand) != 0){
		kbdkey(Kmod4, 0);
		_mods ^= NSEventModifierFlagCommand;
	}
}

- (void) mouseevent:(NSEvent*)event
{
	NSPoint p;
	Point q;
	NSUInteger u;
	NSEventModifierFlags m;

	p = [self.window convertPointToBacking:[self.window mouseLocationOutsideOfEventStream]];
	u = [NSEvent pressedMouseButtons];
	q.x = p.x;
	q.y = p.y;
	if(!ptinrect(q, gscreen->clipr)) return;
	u = (u&~6) | (u&4)>>1 | (u&2)<<1;
	if(u == 1){
		m = [event modifierFlags];
		if(m & NSEventModifierFlagOption){
			_breakcompose = 1;
			u = 2;
		}else if(m & NSEventModifierFlagCommand)
			u = 4;
	}
	absmousetrack(p.x, [self convertSizeToBacking:self.frame.size].height - p.y, u, ticks());
	if(u && _lastInputRect.size.width && _lastInputRect.size.height)
		[self resetLastInputRect];
}

- (void) sendmouse:(NSUInteger)u
{
	mousetrack(0, 0, u, ticks());
	if(u && _lastInputRect.size.width && _lastInputRect.size.height)
		[self resetLastInputRect];
}

- (void) mouseDown:(NSEvent*)event { [self mouseevent:event]; }
- (void) mouseDragged:(NSEvent*)event { [self mouseevent:event]; }
- (void) mouseUp:(NSEvent*)event { [self mouseevent:event]; }
- (void) mouseMoved:(NSEvent*)event { [self mouseevent:event]; }
- (void) rightMouseDown:(NSEvent*)event { [self mouseevent:event]; }
- (void) rightMouseDragged:(NSEvent*)event { [self mouseevent:event]; }
- (void) rightMouseUp:(NSEvent*)event { [self mouseevent:event]; }
- (void) otherMouseDown:(NSEvent*)event { [self mouseevent:event]; }
- (void) otherMouseDragged:(NSEvent*)event { [self mouseevent:event]; }
- (void) otherMouseUp:(NSEvent*)event { [self mouseevent:event]; }

- (void) scrollWheel:(NSEvent*)event{
	NSInteger s = [event scrollingDeltaY];
	if(s > 0)
		[self sendmouse:8];
	else if(s < 0)
		[self sendmouse:16];
}

- (void)magnifyWithEvent:(NSEvent*)e{
	if(fabs([e magnification]) > 0.02)
		[[self window] toggleFullScreen:nil];
}

- (void)touchesBeganWithEvent:(NSEvent*)e
{
	_tapping = YES;
	_tapFingers = [e touchesMatchingPhase:NSTouchPhaseTouching inView:nil].count;
	_tapTime = ticks();
}

- (void)touchesMovedWithEvent:(NSEvent*)e
{
	_tapping = NO;
}

- (void)touchesEndedWithEvent:(NSEvent*)e
{
	if(_tapping
		&& [e touchesMatchingPhase:NSTouchPhaseTouching inView:nil].count == 0
		&& ticks() - _tapTime < 250){
		switch(_tapFingers){
		case 3:
			[self sendmouse:2];
			[self sendmouse:0];
			break;
		case 4:
			[self sendmouse:2];
			[self sendmouse:1];
			[self sendmouse:0];
			break;
		}
		_tapping = NO;
	}
}

- (void)touchesCancelledWithEvent:(NSEvent*)e
{
	_tapping = NO;
}

- (BOOL) acceptsFirstResponder
{
	return TRUE;
}

- (void) resetCursorRects
{
	[super resetCursorRects];
	lock(&cursor.lk);
	[self addCursorRect:self.bounds cursor:currentCursor];
	unlock(&cursor.lk);
}

- (void) reshape
{
	NSSize s = [self convertSizeToBacking:self.frame.size];
	LOG(@"%g %g", s.width, s.height);
	if(gscreen != nil){
		screenresize(Rect(0, 0, s.width, s.height));
	}
}

- (void)windowDidResize:(NSNotification *)notification
{
	if(![self inLiveResize])
		[self reshape];
}

- (void)viewDidEndLiveResize
{
	LOG();
	[super viewDidEndLiveResize];
	[self reshape];
}

- (void)viewDidChangeBackingProperties
{
	LOG();
	[super viewDidChangeBackingProperties];
	[self reshape];
}

static void
keystroke(Rune r)
{
	kbdkey(r, 1);
	kbdkey(r, 0);
}

// conforms to protocol NSTextInputClient
- (BOOL)hasMarkedText
{
	return _markedRange.location != NSNotFound;
}
- (NSRange)markedRange
{
	return _markedRange;
}
- (NSRange)selectedRange
{
	return _selectedRange;
}
- (void)setMarkedText:(id)string
	selectedRange:(NSRange)sRange
	replacementRange:(NSRange)rRange
{
	NSString *str;

	[self clearInput];

	if([string isKindOfClass:[NSAttributedString class]])
		str = [string string];
	else
		str = string;

	if(rRange.location == NSNotFound){
		if(_markedRange.location != NSNotFound){
			rRange = _markedRange;
		}else{
			rRange = _selectedRange;
		}
	}

	if(str.length == 0){
		[_tmpText deleteCharactersInRange:rRange];
		[self unmarkText];
	}else{
		_markedRange = NSMakeRange(rRange.location, str.length);
		[_tmpText replaceCharactersInRange:rRange withString:str];
	}
	_selectedRange.location = rRange.location + sRange.location;
	_selectedRange.length = sRange.length;

	if(_tmpText.length){
		for(uint i = 0; i <= _tmpText.length; ++i){
			if(i == _markedRange.location)
				keystroke('[');
			if(_selectedRange.length){
				if(i == _selectedRange.location)
					keystroke('{');
				if(i == NSMaxRange(_selectedRange))
					keystroke('}');
				}
			if(i == NSMaxRange(_markedRange))
				keystroke(']');
			if(i < _tmpText.length)
				keystroke([_tmpText characterAtIndex:i]);
		}
		uint l = 1 + _tmpText.length - NSMaxRange(_selectedRange)
			+ (_selectedRange.length > 0);
		for(uint i = 0; i < l; ++i)
			keystroke(Kleft);
	}
}
- (void)unmarkText
{
	[_tmpText deleteCharactersInRange:NSMakeRange(0, [_tmpText length])];
	_markedRange = NSMakeRange(NSNotFound, 0);
	_selectedRange = NSMakeRange(0, 0);
}
- (NSArray<NSAttributedStringKey> *)validAttributesForMarkedText
{
	return @[];
}
- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)r
	actualRange:(NSRangePointer)actualRange
{
	NSRange sr;
	NSAttributedString *s;

	sr = NSMakeRange(0, [_tmpText length]);
	sr = NSIntersectionRange(sr, r);
	if(actualRange)
		*actualRange = sr;
	if(sr.length)
		s = [[NSAttributedString alloc]
			initWithString:[_tmpText substringWithRange:sr]];
	return s;
}
- (void)insertText:(id)s
	replacementRange:(NSRange)r
{
	NSUInteger i;
	NSUInteger len;

	[self clearInput];

	len = [s length];
	for(i = 0; i < len; ++i)
		keystroke([s characterAtIndex:i]);
	[_tmpText deleteCharactersInRange:NSMakeRange(0, _tmpText.length)];
	_markedRange = NSMakeRange(NSNotFound, 0);
	_selectedRange = NSMakeRange(0, 0);
}
- (NSUInteger)characterIndexForPoint:(NSPoint)point
{
	return 0;
}
- (NSRect)firstRectForCharacterRange:(NSRange)r
	actualRange:(NSRangePointer)actualRange
{
	if(actualRange)
		*actualRange = r;
	return [[self window] convertRectToScreen:_lastInputRect];
}
- (void)doCommandBySelector:(SEL)s
{
	NSEvent *e;
	uint c, k;

	e = [NSApp currentEvent];
	c = [[e charactersIgnoringModifiers] characterAtIndex:0];
	k = evkey(c);
	if(k>0)
		keystroke(k);
}

// Helper for managing input rect approximately
- (void)resetLastInputRect
{
	_lastInputRect.origin.x = 0.0;
	_lastInputRect.origin.y = 0.0;
	_lastInputRect.size.width = 0.0;
	_lastInputRect.size.height = 0.0;
}

- (void)enlargeLastInputRect:(NSRect)r
{
	r.origin.y = [self bounds].size.height - r.origin.y - r.size.height;
	_lastInputRect = NSUnionRect(_lastInputRect, r);
}

- (void)clearInput
{
	if(_tmpText.length){
		uint l = 1 + _tmpText.length - NSMaxRange(_selectedRange)
			+ (_selectedRange.length > 0);
		for(uint i = 0; i < l; ++i)
			keystroke(Kright);
		l = _tmpText.length+2+2*(_selectedRange.length > 0);
		for(uint i = 0; i < l; ++i)
			keystroke(Kbs);
	}
}
@end

@implementation DrawLayer
{
	id<MTLCommandQueue> _commandQueue;
}

- (id) init {
	LOG();
	self = [super init];
	self.device = MTLCreateSystemDefaultDevice();
	self.pixelFormat = MTLPixelFormatBGRA8Unorm;
	self.framebufferOnly = YES;
	self.opaque = YES;

	// We use a default transparent layer on top of the CAMetalLayer.
	// This seems to make fullscreen applications behave.
	{
		CALayer *stub = [CALayer layer];
		stub.frame = CGRectMake(0, 0, 1, 1);
		[stub setNeedsDisplay];
		[self addSublayer:stub];
	}

	_commandQueue = [self.device newCommandQueue];

	return self;
}

- (void) display
{
	id<MTLCommandBuffer> cbuf;
	id<MTLBlitCommandEncoder> blit;

	cbuf = [_commandQueue commandBuffer];

@autoreleasepool{
	id<CAMetalDrawable> drawable = [self nextDrawable];
	if(!drawable){
		LOG(@"display couldn't get drawable");
		[self setNeedsDisplay];
		return;
	}

	blit = [cbuf blitCommandEncoder];
	[blit copyFromTexture:_texture
		sourceSlice:0
		sourceLevel:0
		sourceOrigin:MTLOriginMake(0, 0, 0)
		sourceSize:MTLSizeMake(_texture.width, _texture.height, _texture.depth)
		toTexture:drawable.texture
		destinationSlice:0
		destinationLevel:0
		destinationOrigin:MTLOriginMake(0, 0, 0)];
	[blit endEncoding];

	[cbuf presentDrawable:drawable];
	drawable = nil;
}
	[cbuf addCompletedHandler:^(id<MTLCommandBuffer> cmdBuff){
		if(cmdBuff.error){
			NSLog(@"command buffer finished with error: %@",
				cmdBuff.error.localizedDescription);
		}else{
			LOG(@"command buffer finished");
		}
	}];
	[cbuf commit];
}

@end