diff options
author | StapleButter <thetotalworm@gmail.com> | 2017-09-09 02:30:51 +0200 |
---|---|---|
committer | StapleButter <thetotalworm@gmail.com> | 2017-09-09 02:30:51 +0200 |
commit | 70e4841d311d68689724768157cc9cbfbde7a9fc (patch) | |
tree | ba9499f77d1258530a7e60aa6e1732c41d98161c /src/libui_sdl/libui/darwin | |
parent | 81747d6c34eb159481a6ca3f283d065fa3568617 (diff) |
another UI attempt, I guess.
sorry.
Diffstat (limited to 'src/libui_sdl/libui/darwin')
40 files changed, 8472 insertions, 0 deletions
diff --git a/src/libui_sdl/libui/darwin/CMakeLists.txt b/src/libui_sdl/libui/darwin/CMakeLists.txt new file mode 100644 index 0000000..dbef5d4 --- /dev/null +++ b/src/libui_sdl/libui/darwin/CMakeLists.txt @@ -0,0 +1,79 @@ +# 3 june 2016 + +list(APPEND _LIBUI_SOURCES + darwin/alloc.m + darwin/area.m + darwin/areaevents.m + darwin/autolayout.m + darwin/box.m + darwin/button.m + darwin/checkbox.m + darwin/colorbutton.m + darwin/combobox.m + darwin/control.m + darwin/datetimepicker.m + darwin/debug.m + darwin/draw.m + darwin/drawtext.m + darwin/editablecombo.m + darwin/entry.m + darwin/fontbutton.m + darwin/form.m + darwin/grid.m + darwin/group.m + darwin/image.m + darwin/label.m + darwin/main.m + darwin/map.m + darwin/menu.m + darwin/multilineentry.m + darwin/progressbar.m + darwin/radiobuttons.m + darwin/scrollview.m + darwin/separator.m + darwin/slider.m + darwin/spinbox.m + darwin/stddialogs.m + darwin/tab.m + darwin/text.m + darwin/util.m + darwin/window.m + darwin/winmoveresize.m +) +set(_LIBUI_SOURCES ${_LIBUI_SOURCES} PARENT_SCOPE) + +list(APPEND _LIBUI_INCLUDEDIRS + darwin +) +set(_LIBUI_INCLUDEDIRS _LIBUI_INCLUDEDIRS PARENT_SCOPE) + +set(_LIBUINAME libui PARENT_SCOPE) +if(NOT BUILD_SHARED_LIBS) + set(_LIBUINAME libui-temporary PARENT_SCOPE) +endif() +# thanks to Mr-Hide in irc.freenode.net/#cmake +macro(_handle_static) + set_target_properties(${_LIBUINAME} PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}") + set(_aname $<TARGET_FILE:${_LIBUINAME}>) + set(_lname libui-combined.list) + set(_oname libui-combined.o) + add_custom_command( + OUTPUT ${_oname} + COMMAND + nm -m ${_aname} | sed -E -n "'s/^[0-9a-f]* \\([A-Z_]+,[a-z_]+\\) external //p'" > ${_lname} + COMMAND + ld -exported_symbols_list ${_lname} -r -all_load ${_aname} -o ${_oname} + COMMENT "Removing hidden symbols") + add_library(libui STATIC ${_oname}) + # otherwise cmake won't know which linker to use + set_target_properties(libui PROPERTIES + LINKER_LANGUAGE C) + set(_aname) + set(_lname) + set(_oname) +endmacro() + +set(_LIBUI_LIBS + objc "-framework Foundation" "-framework AppKit" +PARENT_SCOPE) diff --git a/src/libui_sdl/libui/darwin/alloc.m b/src/libui_sdl/libui/darwin/alloc.m new file mode 100644 index 0000000..e271b90 --- /dev/null +++ b/src/libui_sdl/libui/darwin/alloc.m @@ -0,0 +1,89 @@ +// 4 december 2014 +#import <stdlib.h> +#import "uipriv_darwin.h" + +static NSMutableArray *allocations; +NSMutableArray *delegates; + +void initAlloc(void) +{ + allocations = [NSMutableArray new]; + delegates = [NSMutableArray new]; +} + +#define UINT8(p) ((uint8_t *) (p)) +#define PVOID(p) ((void *) (p)) +#define EXTRA (sizeof (size_t) + sizeof (const char **)) +#define DATA(p) PVOID(UINT8(p) + EXTRA) +#define BASE(p) PVOID(UINT8(p) - EXTRA) +#define SIZE(p) ((size_t *) (p)) +#define CCHAR(p) ((const char **) (p)) +#define TYPE(p) CCHAR(UINT8(p) + sizeof (size_t)) + +void uninitAlloc(void) +{ + NSMutableString *str; + NSValue *v; + + [delegates release]; + if ([allocations count] == 0) { + [allocations release]; + return; + } + str = [NSMutableString new]; + for (v in allocations) { + void *ptr; + + ptr = [v pointerValue]; + [str appendString:[NSString stringWithFormat:@"%p %s\n", ptr, *TYPE(ptr)]]; + } + userbug("Some data was leaked; either you left a uiControl lying around or there's a bug in libui itself. Leaked data:\n%s", [str UTF8String]); + [str release]; +} + +void *uiAlloc(size_t size, const char *type) +{ + void *out; + + out = malloc(EXTRA + size); + if (out == NULL) { + fprintf(stderr, "memory exhausted in uiAlloc()\n"); + abort(); + } + memset(DATA(out), 0, size); + *SIZE(out) = size; + *TYPE(out) = type; + [allocations addObject:[NSValue valueWithPointer:out]]; + return DATA(out); +} + +void *uiRealloc(void *p, size_t new, const char *type) +{ + void *out; + size_t *s; + + if (p == NULL) + return uiAlloc(new, type); + p = BASE(p); + out = realloc(p, EXTRA + new); + if (out == NULL) { + fprintf(stderr, "memory exhausted in uiRealloc()\n"); + abort(); + } + s = SIZE(out); + if (new <= *s) + memset(((uint8_t *) DATA(out)) + *s, 0, new - *s); + *s = new; + [allocations removeObject:[NSValue valueWithPointer:p]]; + [allocations addObject:[NSValue valueWithPointer:out]]; + return DATA(out); +} + +void uiFree(void *p) +{ + if (p == NULL) + implbug("attempt to uiFree(NULL)"); + p = BASE(p); + free(p); + [allocations removeObject:[NSValue valueWithPointer:p]]; +} diff --git a/src/libui_sdl/libui/darwin/area.m b/src/libui_sdl/libui/darwin/area.m new file mode 100644 index 0000000..23162e6 --- /dev/null +++ b/src/libui_sdl/libui/darwin/area.m @@ -0,0 +1,475 @@ +// 9 september 2015 +#import "uipriv_darwin.h" + +// 10.8 fixups +#define NSEventModifierFlags NSUInteger + +@interface areaView : NSView { + uiArea *libui_a; + NSTrackingArea *libui_ta; + NSSize libui_ss; + BOOL libui_enabled; +} +- (id)initWithFrame:(NSRect)r area:(uiArea *)a; +- (uiModifiers)parseModifiers:(NSEvent *)e; +- (void)doMouseEvent:(NSEvent *)e; +- (int)sendKeyEvent:(uiAreaKeyEvent *)ke; +- (int)doKeyDownUp:(NSEvent *)e up:(int)up; +- (int)doKeyDown:(NSEvent *)e; +- (int)doKeyUp:(NSEvent *)e; +- (int)doFlagsChanged:(NSEvent *)e; +- (void)setupNewTrackingArea; +- (void)setScrollingSize:(NSSize)s; +- (BOOL)isEnabled; +- (void)setEnabled:(BOOL)e; +@end + +struct uiArea { + uiDarwinControl c; + NSView *view; // either sv or area depending on whether it is scrolling + NSScrollView *sv; + areaView *area; + struct scrollViewData *d; + uiAreaHandler *ah; + BOOL scrolling; + NSEvent *dragevent; +}; + +@implementation areaView + +- (id)initWithFrame:(NSRect)r area:(uiArea *)a +{ + self = [super initWithFrame:r]; + if (self) { + self->libui_a = a; + [self setupNewTrackingArea]; + self->libui_ss = r.size; + self->libui_enabled = YES; + } + return self; +} + +- (void)drawRect:(NSRect)r +{ + uiArea *a = self->libui_a; + CGContextRef c; + uiAreaDrawParams dp; + + c = (CGContextRef) [[NSGraphicsContext currentContext] graphicsPort]; + // see draw.m under text for why we need the height + dp.Context = newContext(c, [self bounds].size.height); + + dp.AreaWidth = 0; + dp.AreaHeight = 0; + if (!a->scrolling) { + dp.AreaWidth = [self frame].size.width; + dp.AreaHeight = [self frame].size.height; + } + + dp.ClipX = r.origin.x; + dp.ClipY = r.origin.y; + dp.ClipWidth = r.size.width; + dp.ClipHeight = r.size.height; + + // no need to save or restore the graphics state to reset transformations; Cocoa creates a brand-new context each time + (*(a->ah->Draw))(a->ah, a, &dp); + + freeContext(dp.Context); +} + +- (BOOL)isFlipped +{ + return YES; +} + +- (BOOL)acceptsFirstResponder +{ + return YES; +} + +- (uiModifiers)parseModifiers:(NSEvent *)e +{ + NSEventModifierFlags mods; + uiModifiers m; + + m = 0; + mods = [e modifierFlags]; + if ((mods & NSControlKeyMask) != 0) + m |= uiModifierCtrl; + if ((mods & NSAlternateKeyMask) != 0) + m |= uiModifierAlt; + if ((mods & NSShiftKeyMask) != 0) + m |= uiModifierShift; + if ((mods & NSCommandKeyMask) != 0) + m |= uiModifierSuper; + return m; +} + +- (void)setupNewTrackingArea +{ + self->libui_ta = [[NSTrackingArea alloc] initWithRect:[self bounds] + options:(NSTrackingMouseEnteredAndExited | + NSTrackingMouseMoved | + NSTrackingActiveAlways | + NSTrackingInVisibleRect | + NSTrackingEnabledDuringMouseDrag) + owner:self + userInfo:nil]; + [self addTrackingArea:self->libui_ta]; +} + +- (void)updateTrackingAreas +{ + [self removeTrackingArea:self->libui_ta]; + [self->libui_ta release]; + [self setupNewTrackingArea]; +} + +// capture on drag is done automatically on OS X +- (void)doMouseEvent:(NSEvent *)e +{ + uiArea *a = self->libui_a; + uiAreaMouseEvent me; + NSPoint point; + int buttonNumber; + NSUInteger pmb; + unsigned int i, max; + + // this will convert point to drawing space + // thanks swillits in irc.freenode.net/#macdev + point = [self convertPoint:[e locationInWindow] fromView:nil]; + me.X = point.x; + me.Y = point.y; + + me.AreaWidth = 0; + me.AreaHeight = 0; + if (!a->scrolling) { + me.AreaWidth = [self frame].size.width; + me.AreaHeight = [self frame].size.height; + } + + buttonNumber = [e buttonNumber] + 1; + // swap button numbers 2 and 3 (right and middle) + if (buttonNumber == 2) + buttonNumber = 3; + else if (buttonNumber == 3) + buttonNumber = 2; + + me.Down = 0; + me.Up = 0; + me.Count = 0; + switch ([e type]) { + case NSLeftMouseDown: + case NSRightMouseDown: + case NSOtherMouseDown: + me.Down = buttonNumber; + me.Count = [e clickCount]; + break; + case NSLeftMouseUp: + case NSRightMouseUp: + case NSOtherMouseUp: + me.Up = buttonNumber; + break; + case NSLeftMouseDragged: + case NSRightMouseDragged: + case NSOtherMouseDragged: + // we include the button that triggered the dragged event in the Held fields + buttonNumber = 0; + break; + } + + me.Modifiers = [self parseModifiers:e]; + + pmb = [NSEvent pressedMouseButtons]; + me.Held1To64 = 0; + if (buttonNumber != 1 && (pmb & 1) != 0) + me.Held1To64 |= 1; + if (buttonNumber != 2 && (pmb & 4) != 0) + me.Held1To64 |= 2; + if (buttonNumber != 3 && (pmb & 2) != 0) + me.Held1To64 |= 4; + // buttons 4..32 + // https://developer.apple.com/library/mac/documentation/Carbon/Reference/QuartzEventServicesRef/index.html#//apple_ref/c/tdef/CGMouseButton says Quartz only supports up to 32 buttons + max = 32; + for (i = 4; i <= max; i++) { + uint64_t j; + + if (buttonNumber == i) + continue; + j = 1 << (i - 1); + if ((pmb & j) != 0) + me.Held1To64 |= j; + } + + if (self->libui_enabled) { + // and allow dragging here + a->dragevent = e; + (*(a->ah->MouseEvent))(a->ah, a, &me); + a->dragevent = nil; + } +} + +#define mouseEvent(name) \ + - (void)name:(NSEvent *)e \ + { \ + [self doMouseEvent:e]; \ + } +mouseEvent(mouseMoved) +mouseEvent(mouseDragged) +mouseEvent(rightMouseDragged) +mouseEvent(otherMouseDragged) +mouseEvent(mouseDown) +mouseEvent(rightMouseDown) +mouseEvent(otherMouseDown) +mouseEvent(mouseUp) +mouseEvent(rightMouseUp) +mouseEvent(otherMouseUp) + +- (void)mouseEntered:(NSEvent *)e +{ + uiArea *a = self->libui_a; + + if (self->libui_enabled) + (*(a->ah->MouseCrossed))(a->ah, a, 0); +} + +- (void)mouseExited:(NSEvent *)e +{ + uiArea *a = self->libui_a; + + if (self->libui_enabled) + (*(a->ah->MouseCrossed))(a->ah, a, 1); +} + +// note: there is no equivalent to WM_CAPTURECHANGED on Mac OS X; there literally is no way to break a grab like that +// even if I invoke the task switcher and switch processes, the mouse grab will still be held until I let go of all buttons +// therefore, no DragBroken() + +- (int)sendKeyEvent:(uiAreaKeyEvent *)ke +{ + uiArea *a = self->libui_a; + + return (*(a->ah->KeyEvent))(a->ah, a, ke); +} + +- (int)doKeyDownUp:(NSEvent *)e up:(int)up +{ + uiAreaKeyEvent ke; + + ke.Key = 0; + ke.ExtKey = 0; + ke.Modifier = 0; + + ke.Modifiers = [self parseModifiers:e]; + + ke.Up = up; + + if (!fromKeycode([e keyCode], &ke)) + return 0; + return [self sendKeyEvent:&ke]; +} + +- (int)doKeyDown:(NSEvent *)e +{ + return [self doKeyDownUp:e up:0]; +} + +- (int)doKeyUp:(NSEvent *)e +{ + return [self doKeyDownUp:e up:1]; +} + +- (int)doFlagsChanged:(NSEvent *)e +{ + uiAreaKeyEvent ke; + uiModifiers whichmod; + + ke.Key = 0; + ke.ExtKey = 0; + + // Mac OS X sends this event on both key up and key down. + // Fortunately -[e keyCode] IS valid here, so we can simply map from key code to Modifiers, get the value of [e modifierFlags], and check if the respective bit is set or not — that will give us the up/down state + if (!keycodeModifier([e keyCode], &whichmod)) + return 0; + ke.Modifier = whichmod; + ke.Modifiers = [self parseModifiers:e]; + ke.Up = (ke.Modifiers & ke.Modifier) == 0; + // and then drop the current modifier from Modifiers + ke.Modifiers &= ~ke.Modifier; + return [self sendKeyEvent:&ke]; +} + +- (void)setFrameSize:(NSSize)size +{ + uiArea *a = self->libui_a; + + [super setFrameSize:size]; + if (!a->scrolling) + // we must redraw everything on resize because Windows requires it + [self setNeedsDisplay:YES]; +} + +// TODO does this update the frame? +- (void)setScrollingSize:(NSSize)s +{ + self->libui_ss = s; + [self invalidateIntrinsicContentSize]; +} + +- (NSSize)intrinsicContentSize +{ + if (!self->libui_a->scrolling) + return [super intrinsicContentSize]; + return self->libui_ss; +} + +- (BOOL)becomeFirstResponder +{ + return [self isEnabled]; +} + +- (BOOL)isEnabled +{ + return self->libui_enabled; +} + +- (void)setEnabled:(BOOL)e +{ + self->libui_enabled = e; + if (!self->libui_enabled && [self window] != nil) + if ([[self window] firstResponder] == self) + [[self window] makeFirstResponder:nil]; +} + +@end + +uiDarwinControlAllDefaultsExceptDestroy(uiArea, view) + +static void uiAreaDestroy(uiControl *c) +{ + uiArea *a = uiArea(c); + + if (a->scrolling) + scrollViewFreeData(a->sv, a->d); + [a->area release]; + if (a->scrolling) + [a->sv release]; + uiFreeControl(uiControl(a)); +} + +// called by subclasses of -[NSApplication sendEvent:] +// by default, NSApplication eats some key events +// this prevents that from happening with uiArea +// see http://stackoverflow.com/questions/24099063/how-do-i-detect-keyup-in-my-nsview-with-the-command-key-held and http://lists.apple.com/archives/cocoa-dev/2003/Oct/msg00442.html +int sendAreaEvents(NSEvent *e) +{ + NSEventType type; + id focused; + areaView *view; + + type = [e type]; + if (type != NSKeyDown && type != NSKeyUp && type != NSFlagsChanged) + return 0; + focused = [[e window] firstResponder]; + if (focused == nil) + return 0; + if (![focused isKindOfClass:[areaView class]]) + return 0; + view = (areaView *) focused; + switch (type) { + case NSKeyDown: + return [view doKeyDown:e]; + case NSKeyUp: + return [view doKeyUp:e]; + case NSFlagsChanged: + return [view doFlagsChanged:e]; + } + return 0; +} + +void uiAreaSetSize(uiArea *a, int width, int height) +{ + if (!a->scrolling) + userbug("You cannot call uiAreaSetSize() on a non-scrolling uiArea. (area: %p)", a); + [a->area setScrollingSize:NSMakeSize(width, height)]; +} + +void uiAreaQueueRedrawAll(uiArea *a) +{ + [a->area setNeedsDisplay:YES]; +} + +void uiAreaScrollTo(uiArea *a, double x, double y, double width, double height) +{ + if (!a->scrolling) + userbug("You cannot call uiAreaScrollTo() on a non-scrolling uiArea. (area: %p)", a); + [a->area scrollRectToVisible:NSMakeRect(x, y, width, height)]; + // don't worry about the return value; it just says whether scrolling was needed +} + +void uiAreaBeginUserWindowMove(uiArea *a) +{ + libuiNSWindow *w; + + w = (libuiNSWindow *) [a->area window]; + if (w == nil) + return; // TODO + if (a->dragevent == nil) + return; // TODO + [w libui_doMove:a->dragevent]; +} + +void uiAreaBeginUserWindowResize(uiArea *a, uiWindowResizeEdge edge) +{ + libuiNSWindow *w; + + w = (libuiNSWindow *) [a->area window]; + if (w == nil) + return; // TODO + if (a->dragevent == nil) + return; // TODO + [w libui_doResize:a->dragevent on:edge]; +} + +uiArea *uiNewArea(uiAreaHandler *ah) +{ + uiArea *a; + + uiDarwinNewControl(uiArea, a); + + a->ah = ah; + a->scrolling = NO; + + a->area = [[areaView alloc] initWithFrame:NSZeroRect area:a]; + + a->view = a->area; + + return a; +} + +uiArea *uiNewScrollingArea(uiAreaHandler *ah, int width, int height) +{ + uiArea *a; + struct scrollViewCreateParams p; + + uiDarwinNewControl(uiArea, a); + + a->ah = ah; + a->scrolling = YES; + + a->area = [[areaView alloc] initWithFrame:NSMakeRect(0, 0, width, height) + area:a]; + + memset(&p, 0, sizeof (struct scrollViewCreateParams)); + p.DocumentView = a->area; + p.BackgroundColor = [NSColor controlColor]; + p.DrawsBackground = 1; + p.Bordered = NO; + p.HScroll = YES; + p.VScroll = YES; + a->sv = mkScrollView(&p, &(a->d)); + + a->view = a->sv; + + return a; +} diff --git a/src/libui_sdl/libui/darwin/areaevents.m b/src/libui_sdl/libui/darwin/areaevents.m new file mode 100644 index 0000000..d7ceaaa --- /dev/null +++ b/src/libui_sdl/libui/darwin/areaevents.m @@ -0,0 +1,159 @@ +// 30 march 2014 +#import "uipriv_darwin.h" + +/* +Mac OS X uses its own set of hardware key codes that are different from PC keyboard scancodes, but are positional (like PC keyboard scancodes). These are defined in <HIToolbox/Events.h>, a Carbon header. As far as I can tell, there's no way to include this header without either using an absolute path or linking Carbon into the program, so the constant values are used here instead. + +The Cocoa docs do guarantee that -[NSEvent keyCode] results in key codes that are the same as those returned by Carbon; that is, these codes. +*/ + +// use uintptr_t to be safe +static const struct { + uintptr_t keycode; + char equiv; +} keycodeKeys[] = { + { 0x00, 'a' }, + { 0x01, 's' }, + { 0x02, 'd' }, + { 0x03, 'f' }, + { 0x04, 'h' }, + { 0x05, 'g' }, + { 0x06, 'z' }, + { 0x07, 'x' }, + { 0x08, 'c' }, + { 0x09, 'v' }, + { 0x0B, 'b' }, + { 0x0C, 'q' }, + { 0x0D, 'w' }, + { 0x0E, 'e' }, + { 0x0F, 'r' }, + { 0x10, 'y' }, + { 0x11, 't' }, + { 0x12, '1' }, + { 0x13, '2' }, + { 0x14, '3' }, + { 0x15, '4' }, + { 0x16, '6' }, + { 0x17, '5' }, + { 0x18, '=' }, + { 0x19, '9' }, + { 0x1A, '7' }, + { 0x1B, '-' }, + { 0x1C, '8' }, + { 0x1D, '0' }, + { 0x1E, ']' }, + { 0x1F, 'o' }, + { 0x20, 'u' }, + { 0x21, '[' }, + { 0x22, 'i' }, + { 0x23, 'p' }, + { 0x25, 'l' }, + { 0x26, 'j' }, + { 0x27, '\'' }, + { 0x28, 'k' }, + { 0x29, ';' }, + { 0x2A, '\\' }, + { 0x2B, ',' }, + { 0x2C, '/' }, + { 0x2D, 'n' }, + { 0x2E, 'm' }, + { 0x2F, '.' }, + { 0x32, '`' }, + { 0x24, '\n' }, + { 0x30, '\t' }, + { 0x31, ' ' }, + { 0x33, '\b' }, + { 0xFFFF, 0 }, +}; + +static const struct { + uintptr_t keycode; + uiExtKey equiv; +} keycodeExtKeys[] = { + { 0x41, uiExtKeyNDot }, + { 0x43, uiExtKeyNMultiply }, + { 0x45, uiExtKeyNAdd }, + { 0x4B, uiExtKeyNDivide }, + { 0x4C, uiExtKeyNEnter }, + { 0x4E, uiExtKeyNSubtract }, + { 0x52, uiExtKeyN0 }, + { 0x53, uiExtKeyN1 }, + { 0x54, uiExtKeyN2 }, + { 0x55, uiExtKeyN3 }, + { 0x56, uiExtKeyN4 }, + { 0x57, uiExtKeyN5 }, + { 0x58, uiExtKeyN6 }, + { 0x59, uiExtKeyN7 }, + { 0x5B, uiExtKeyN8 }, + { 0x5C, uiExtKeyN9 }, + { 0x35, uiExtKeyEscape }, + { 0x60, uiExtKeyF5 }, + { 0x61, uiExtKeyF6 }, + { 0x62, uiExtKeyF7 }, + { 0x63, uiExtKeyF3 }, + { 0x64, uiExtKeyF8 }, + { 0x65, uiExtKeyF9 }, + { 0x67, uiExtKeyF11 }, + { 0x6D, uiExtKeyF10 }, + { 0x6F, uiExtKeyF12 }, + { 0x72, uiExtKeyInsert }, // listed as the Help key but it's in the same position on an Apple keyboard as the Insert key on a Windows keyboard; thanks to SeanieB from irc.badnik.net and Psy in irc.freenode.net/#macdev for confirming they have the same code + { 0x73, uiExtKeyHome }, + { 0x74, uiExtKeyPageUp }, + { 0x75, uiExtKeyDelete }, + { 0x76, uiExtKeyF4 }, + { 0x77, uiExtKeyEnd }, + { 0x78, uiExtKeyF2 }, + { 0x79, uiExtKeyPageDown }, + { 0x7A, uiExtKeyF1 }, + { 0x7B, uiExtKeyLeft }, + { 0x7C, uiExtKeyRight }, + { 0x7D, uiExtKeyDown }, + { 0x7E, uiExtKeyUp }, + { 0xFFFF, 0 }, +}; + +static const struct { + uintptr_t keycode; + uiModifiers equiv; +} keycodeModifiers[] = { + { 0x37, uiModifierSuper }, // left command + { 0x38, uiModifierShift }, // left shift + { 0x3A, uiModifierAlt }, // left option + { 0x3B, uiModifierCtrl }, // left control + { 0x3C, uiModifierShift }, // right shift + { 0x3D, uiModifierAlt }, // right alt + { 0x3E, uiModifierCtrl }, // right control + // the following is not in Events.h for some reason + // thanks to Nicole and jedivulcan from irc.badnik.net + { 0x36, uiModifierSuper }, // right command + { 0xFFFF, 0 }, +}; + +BOOL fromKeycode(unsigned short keycode, uiAreaKeyEvent *ke) +{ + int i; + + for (i = 0; keycodeKeys[i].keycode != 0xFFFF; i++) + if (keycodeKeys[i].keycode == keycode) { + ke->Key = keycodeKeys[i].equiv; + return YES; + } + for (i = 0; keycodeExtKeys[i].keycode != 0xFFFF; i++) + if (keycodeExtKeys[i].keycode == keycode) { + ke->ExtKey = keycodeExtKeys[i].equiv; + return YES; + } + return NO; +} + +BOOL keycodeModifier(unsigned short keycode, uiModifiers *mod) +{ + int i; + + for (i = 0; keycodeModifiers[i].keycode != 0xFFFF; i++) + if (keycodeModifiers[i].keycode == keycode) { + *mod = keycodeModifiers[i].equiv; + return YES; + } + return NO; +} diff --git a/src/libui_sdl/libui/darwin/autolayout.m b/src/libui_sdl/libui/darwin/autolayout.m new file mode 100644 index 0000000..9964155 --- /dev/null +++ b/src/libui_sdl/libui/darwin/autolayout.m @@ -0,0 +1,161 @@ +// 15 august 2015 +#import "uipriv_darwin.h" + +NSLayoutConstraint *mkConstraint(id view1, NSLayoutAttribute attr1, NSLayoutRelation relation, id view2, NSLayoutAttribute attr2, CGFloat multiplier, CGFloat c, NSString *desc) +{ + NSLayoutConstraint *constraint; + + constraint = [NSLayoutConstraint constraintWithItem:view1 + attribute:attr1 + relatedBy:relation + toItem:view2 + attribute:attr2 + multiplier:multiplier + constant:c]; + // apparently only added in 10.9 + if ([constraint respondsToSelector:@selector(setIdentifier:)]) + [((id) constraint) setIdentifier:desc]; + return constraint; +} + +CGFloat uiDarwinMarginAmount(void *reserved) +{ + return 20.0; +} + +CGFloat uiDarwinPaddingAmount(void *reserved) +{ + return 8.0; +} + +// this is needed for NSSplitView to work properly; see http://stackoverflow.com/questions/34574478/how-can-i-set-the-position-of-a-nssplitview-nowadays-setpositionofdivideratind (stal in irc.freenode.net/#macdev came up with the exact combination) +// turns out it also works on NSTabView and NSBox too, possibly others! +// and for bonus points, it even seems to fix unsatisfiable-constraint-autoresizing-mask issues with NSTabView and NSBox too!!! this is nuts +void jiggleViewLayout(NSView *view) +{ + [view setNeedsLayout:YES]; + [view layoutSubtreeIfNeeded]; +} + +static CGFloat margins(int margined) +{ + if (!margined) + return 0.0; + return uiDarwinMarginAmount(NULL); +} + +void singleChildConstraintsEstablish(struct singleChildConstraints *c, NSView *contentView, NSView *childView, BOOL hugsTrailing, BOOL hugsBottom, int margined, NSString *desc) +{ + CGFloat margin; + + margin = margins(margined); + + c->leadingConstraint = mkConstraint(contentView, NSLayoutAttributeLeading, + NSLayoutRelationEqual, + childView, NSLayoutAttributeLeading, + 1, -margin, + [desc stringByAppendingString:@" leading constraint"]); + [contentView addConstraint:c->leadingConstraint]; + [c->leadingConstraint retain]; + + c->topConstraint = mkConstraint(contentView, NSLayoutAttributeTop, + NSLayoutRelationEqual, + childView, NSLayoutAttributeTop, + 1, -margin, + [desc stringByAppendingString:@" top constraint"]); + [contentView addConstraint:c->topConstraint]; + [c->topConstraint retain]; + + c->trailingConstraintGreater = mkConstraint(contentView, NSLayoutAttributeTrailing, + NSLayoutRelationGreaterThanOrEqual, + childView, NSLayoutAttributeTrailing, + 1, margin, + [desc stringByAppendingString:@" trailing >= constraint"]); + if (hugsTrailing) + [c->trailingConstraintGreater setPriority:NSLayoutPriorityDefaultLow]; + [contentView addConstraint:c->trailingConstraintGreater]; + [c->trailingConstraintGreater retain]; + + c->trailingConstraintEqual = mkConstraint(contentView, NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + childView, NSLayoutAttributeTrailing, + 1, margin, + [desc stringByAppendingString:@" trailing == constraint"]); + if (!hugsTrailing) + [c->trailingConstraintEqual setPriority:NSLayoutPriorityDefaultLow]; + [contentView addConstraint:c->trailingConstraintEqual]; + [c->trailingConstraintEqual retain]; + + c->bottomConstraintGreater = mkConstraint(contentView, NSLayoutAttributeBottom, + NSLayoutRelationGreaterThanOrEqual, + childView, NSLayoutAttributeBottom, + 1, margin, + [desc stringByAppendingString:@" bottom >= constraint"]); + if (hugsBottom) + [c->bottomConstraintGreater setPriority:NSLayoutPriorityDefaultLow]; + [contentView addConstraint:c->bottomConstraintGreater]; + [c->bottomConstraintGreater retain]; + + c->bottomConstraintEqual = mkConstraint(contentView, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + childView, NSLayoutAttributeBottom, + 1, margin, + [desc stringByAppendingString:@" bottom == constraint"]); + if (!hugsBottom) + [c->bottomConstraintEqual setPriority:NSLayoutPriorityDefaultLow]; + [contentView addConstraint:c->bottomConstraintEqual]; + [c->bottomConstraintEqual retain]; +} + +void singleChildConstraintsRemove(struct singleChildConstraints *c, NSView *cv) +{ + if (c->leadingConstraint != nil) { + [cv removeConstraint:c->leadingConstraint]; + [c->leadingConstraint release]; + c->leadingConstraint = nil; + } + if (c->topConstraint != nil) { + [cv removeConstraint:c->topConstraint]; + [c->topConstraint release]; + c->topConstraint = nil; + } + if (c->trailingConstraintGreater != nil) { + [cv removeConstraint:c->trailingConstraintGreater]; + [c->trailingConstraintGreater release]; + c->trailingConstraintGreater = nil; + } + if (c->trailingConstraintEqual != nil) { + [cv removeConstraint:c->trailingConstraintEqual]; + [c->trailingConstraintEqual release]; + c->trailingConstraintEqual = nil; + } + if (c->bottomConstraintGreater != nil) { + [cv removeConstraint:c->bottomConstraintGreater]; + [c->bottomConstraintGreater release]; + c->bottomConstraintGreater = nil; + } + if (c->bottomConstraintEqual != nil) { + [cv removeConstraint:c->bottomConstraintEqual]; + [c->bottomConstraintEqual release]; + c->bottomConstraintEqual = nil; + } +} + +void singleChildConstraintsSetMargined(struct singleChildConstraints *c, int margined) +{ + CGFloat margin; + + margin = margins(margined); + if (c->leadingConstraint != nil) + [c->leadingConstraint setConstant:-margin]; + if (c->topConstraint != nil) + [c->topConstraint setConstant:-margin]; + if (c->trailingConstraintGreater != nil) + [c->trailingConstraintGreater setConstant:margin]; + if (c->trailingConstraintEqual != nil) + [c->trailingConstraintEqual setConstant:margin]; + if (c->bottomConstraintGreater != nil) + [c->bottomConstraintGreater setConstant:margin]; + if (c->bottomConstraintEqual != nil) + [c->bottomConstraintEqual setConstant:margin]; +} diff --git a/src/libui_sdl/libui/darwin/box.m b/src/libui_sdl/libui/darwin/box.m new file mode 100644 index 0000000..18d536d --- /dev/null +++ b/src/libui_sdl/libui/darwin/box.m @@ -0,0 +1,469 @@ +// 15 august 2015 +#import "uipriv_darwin.h" + +// TODO hiding all stretchy controls still hugs trailing edge + +@interface boxChild : NSObject +@property uiControl *c; +@property BOOL stretchy; +@property NSLayoutPriority oldPrimaryHuggingPri; +@property NSLayoutPriority oldSecondaryHuggingPri; +- (NSView *)view; +@end + +@interface boxView : NSView { + uiBox *b; + NSMutableArray *children; + BOOL vertical; + int padded; + + NSLayoutConstraint *first; + NSMutableArray *inBetweens; + NSLayoutConstraint *last; + NSMutableArray *otherConstraints; + + NSLayoutAttribute primaryStart; + NSLayoutAttribute primaryEnd; + NSLayoutAttribute secondaryStart; + NSLayoutAttribute secondaryEnd; + NSLayoutAttribute primarySize; + NSLayoutConstraintOrientation primaryOrientation; + NSLayoutConstraintOrientation secondaryOrientation; +} +- (id)initWithVertical:(BOOL)vert b:(uiBox *)bb; +- (void)onDestroy; +- (void)removeOurConstraints; +- (void)syncEnableStates:(int)enabled; +- (CGFloat)paddingAmount; +- (void)establishOurConstraints; +- (void)append:(uiControl *)c stretchy:(int)stretchy; +- (void)delete:(int)n; +- (int)isPadded; +- (void)setPadded:(int)p; +- (BOOL)hugsTrailing; +- (BOOL)hugsBottom; +- (int)nStretchy; +@end + +struct uiBox { + uiDarwinControl c; + boxView *view; +}; + +@implementation boxChild + +- (NSView *)view +{ + return (NSView *) uiControlHandle(self.c); +} + +@end + +@implementation boxView + +- (id)initWithVertical:(BOOL)vert b:(uiBox *)bb +{ + self = [super initWithFrame:NSZeroRect]; + if (self != nil) { + // the weird names vert and bb are to shut the compiler up about shadowing because implicit this/self is stupid + self->b = bb; + self->vertical = vert; + self->padded = 0; + self->children = [NSMutableArray new]; + + self->inBetweens = [NSMutableArray new]; + self->otherConstraints = [NSMutableArray new]; + + if (self->vertical) { + self->primaryStart = NSLayoutAttributeTop; + self->primaryEnd = NSLayoutAttributeBottom; + self->secondaryStart = NSLayoutAttributeLeading; + self->secondaryEnd = NSLayoutAttributeTrailing; + self->primarySize = NSLayoutAttributeHeight; + self->primaryOrientation = NSLayoutConstraintOrientationVertical; + self->secondaryOrientation = NSLayoutConstraintOrientationHorizontal; + } else { + self->primaryStart = NSLayoutAttributeLeading; + self->primaryEnd = NSLayoutAttributeTrailing; + self->secondaryStart = NSLayoutAttributeTop; + self->secondaryEnd = NSLayoutAttributeBottom; + self->primarySize = NSLayoutAttributeWidth; + self->primaryOrientation = NSLayoutConstraintOrientationHorizontal; + self->secondaryOrientation = NSLayoutConstraintOrientationVertical; + } + } + return self; +} + +- (void)onDestroy +{ + boxChild *bc; + + [self removeOurConstraints]; + [self->inBetweens release]; + [self->otherConstraints release]; + + for (bc in self->children) { + uiControlSetParent(bc.c, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(bc.c), nil); + uiControlDestroy(bc.c); + } + [self->children release]; +} + +- (void)removeOurConstraints +{ + if (self->first != nil) { + [self removeConstraint:self->first]; + [self->first release]; + self->first = nil; + } + if ([self->inBetweens count] != 0) { + [self removeConstraints:self->inBetweens]; + [self->inBetweens removeAllObjects]; + } + if (self->last != nil) { + [self removeConstraint:self->last]; + [self->last release]; + self->last = nil; + } + if ([self->otherConstraints count] != 0) { + [self removeConstraints:self->otherConstraints]; + [self->otherConstraints removeAllObjects]; + } +} + +- (void)syncEnableStates:(int)enabled +{ + boxChild *bc; + + for (bc in self->children) + uiDarwinControlSyncEnableState(uiDarwinControl(bc.c), enabled); +} + +- (CGFloat)paddingAmount +{ + if (!self->padded) + return 0.0; + return uiDarwinPaddingAmount(NULL); +} + +- (void)establishOurConstraints +{ + boxChild *bc; + CGFloat padding; + NSView *prev; + NSLayoutConstraint *c; + BOOL (*hugsSecondary)(uiDarwinControl *); + + [self removeOurConstraints]; + if ([self->children count] == 0) + return; + padding = [self paddingAmount]; + + // first arrange in the primary direction + prev = nil; + for (bc in self->children) { + if (!uiControlVisible(bc.c)) + continue; + if (prev == nil) { // first view + self->first = mkConstraint(self, self->primaryStart, + NSLayoutRelationEqual, + [bc view], self->primaryStart, + 1, 0, + @"uiBox first primary constraint"); + [self addConstraint:self->first]; + [self->first retain]; + prev = [bc view]; + continue; + } + // not the first; link it + c = mkConstraint(prev, self->primaryEnd, + NSLayoutRelationEqual, + [bc view], self->primaryStart, + 1, -padding, + @"uiBox in-between primary constraint"); + [self addConstraint:c]; + [self->inBetweens addObject:c]; + prev = [bc view]; + } + if (prev == nil) // no control visible; act as if no controls + return; + self->last = mkConstraint(prev, self->primaryEnd, + NSLayoutRelationEqual, + self, self->primaryEnd, + 1, 0, + @"uiBox last primary constraint"); + [self addConstraint:self->last]; + [self->last retain]; + + // then arrange in the secondary direction + hugsSecondary = uiDarwinControlHugsTrailingEdge; + if (!self->vertical) + hugsSecondary = uiDarwinControlHugsBottom; + for (bc in self->children) { + if (!uiControlVisible(bc.c)) + continue; + c = mkConstraint(self, self->secondaryStart, + NSLayoutRelationEqual, + [bc view], self->secondaryStart, + 1, 0, + @"uiBox secondary start constraint"); + [self addConstraint:c]; + [self->otherConstraints addObject:c]; + c = mkConstraint([bc view], self->secondaryEnd, + NSLayoutRelationLessThanOrEqual, + self, self->secondaryEnd, + 1, 0, + @"uiBox secondary end <= constraint"); + if ((*hugsSecondary)(uiDarwinControl(bc.c))) + [c setPriority:NSLayoutPriorityDefaultLow]; + [self addConstraint:c]; + [self->otherConstraints addObject:c]; + c = mkConstraint([bc view], self->secondaryEnd, + NSLayoutRelationEqual, + self, self->secondaryEnd, + 1, 0, + @"uiBox secondary end == constraint"); + if (!(*hugsSecondary)(uiDarwinControl(bc.c))) + [c setPriority:NSLayoutPriorityDefaultLow]; + [self addConstraint:c]; + [self->otherConstraints addObject:c]; + } + + // and make all stretchy controls the same size + if ([self nStretchy] == 0) + return; + prev = nil; // first stretchy view + for (bc in self->children) { + if (!uiControlVisible(bc.c)) + continue; + if (!bc.stretchy) + continue; + if (prev == nil) { + prev = [bc view]; + continue; + } + c = mkConstraint(prev, self->primarySize, + NSLayoutRelationEqual, + [bc view], self->primarySize, + 1, 0, + @"uiBox stretchy size constraint"); + [self addConstraint:c]; + [self->otherConstraints addObject:c]; + } +} + +- (void)append:(uiControl *)c stretchy:(int)stretchy +{ + boxChild *bc; + NSLayoutPriority priority; + int oldnStretchy; + + bc = [boxChild new]; + bc.c = c; + bc.stretchy = stretchy; + bc.oldPrimaryHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(bc.c), self->primaryOrientation); + bc.oldSecondaryHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(bc.c), self->secondaryOrientation); + + uiControlSetParent(bc.c, uiControl(self->b)); + uiDarwinControlSetSuperview(uiDarwinControl(bc.c), self); + uiDarwinControlSyncEnableState(uiDarwinControl(bc.c), uiControlEnabledToUser(uiControl(self->b))); + + // if a control is stretchy, it should not hug in the primary direction + // otherwise, it should *forcibly* hug + if (bc.stretchy) + priority = NSLayoutPriorityDefaultLow; + else + // LONGTERM will default high work? + priority = NSLayoutPriorityRequired; + uiDarwinControlSetHuggingPriority(uiDarwinControl(bc.c), priority, self->primaryOrientation); + // make sure controls don't hug their secondary direction so they fill the width of the view + uiDarwinControlSetHuggingPriority(uiDarwinControl(bc.c), NSLayoutPriorityDefaultLow, self->secondaryOrientation); + + oldnStretchy = [self nStretchy]; + [self->children addObject:bc]; + + [self establishOurConstraints]; + if (bc.stretchy) + if (oldnStretchy == 0) + uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->b)); + + [bc release]; // we don't need the initial reference now +} + +- (void)delete:(int)n +{ + boxChild *bc; + int stretchy; + + bc = (boxChild *) [self->children objectAtIndex:n]; + stretchy = bc.stretchy; + + uiControlSetParent(bc.c, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(bc.c), nil); + + uiDarwinControlSetHuggingPriority(uiDarwinControl(bc.c), bc.oldPrimaryHuggingPri, self->primaryOrientation); + uiDarwinControlSetHuggingPriority(uiDarwinControl(bc.c), bc.oldSecondaryHuggingPri, self->secondaryOrientation); + + [self->children removeObjectAtIndex:n]; + + [self establishOurConstraints]; + if (stretchy) + if ([self nStretchy] == 0) + uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->b)); +} + +- (int)isPadded +{ + return self->padded; +} + +- (void)setPadded:(int)p +{ + CGFloat padding; + NSLayoutConstraint *c; + + self->padded = p; + padding = [self paddingAmount]; + for (c in self->inBetweens) + [c setConstant:-padding]; +} + +- (BOOL)hugsTrailing +{ + if (self->vertical) // always hug if vertical + return YES; + return [self nStretchy] != 0; +} + +- (BOOL)hugsBottom +{ + if (!self->vertical) // always hug if horizontal + return YES; + return [self nStretchy] != 0; +} + +- (int)nStretchy +{ + boxChild *bc; + int n; + + n = 0; + for (bc in self->children) { + if (!uiControlVisible(bc.c)) + continue; + if (bc.stretchy) + n++; + } + return n; +} + +@end + +static void uiBoxDestroy(uiControl *c) +{ + uiBox *b = uiBox(c); + + [b->view onDestroy]; + [b->view release]; + uiFreeControl(uiControl(b)); +} + +uiDarwinControlDefaultHandle(uiBox, view) +uiDarwinControlDefaultParent(uiBox, view) +uiDarwinControlDefaultSetParent(uiBox, view) +uiDarwinControlDefaultToplevel(uiBox, view) +uiDarwinControlDefaultVisible(uiBox, view) +uiDarwinControlDefaultShow(uiBox, view) +uiDarwinControlDefaultHide(uiBox, view) +uiDarwinControlDefaultEnabled(uiBox, view) +uiDarwinControlDefaultEnable(uiBox, view) +uiDarwinControlDefaultDisable(uiBox, view) + +static void uiBoxSyncEnableState(uiDarwinControl *c, int enabled) +{ + uiBox *b = uiBox(c); + + if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(b), enabled)) + return; + [b->view syncEnableStates:enabled]; +} + +uiDarwinControlDefaultSetSuperview(uiBox, view) + +static BOOL uiBoxHugsTrailingEdge(uiDarwinControl *c) +{ + uiBox *b = uiBox(c); + + return [b->view hugsTrailing]; +} + +static BOOL uiBoxHugsBottom(uiDarwinControl *c) +{ + uiBox *b = uiBox(c); + + return [b->view hugsBottom]; +} + +static void uiBoxChildEdgeHuggingChanged(uiDarwinControl *c) +{ + uiBox *b = uiBox(c); + + [b->view establishOurConstraints]; +} + +uiDarwinControlDefaultHuggingPriority(uiBox, view) +uiDarwinControlDefaultSetHuggingPriority(uiBox, view) + +static void uiBoxChildVisibilityChanged(uiDarwinControl *c) +{ + uiBox *b = uiBox(c); + + [b->view establishOurConstraints]; +} + +void uiBoxAppend(uiBox *b, uiControl *c, int stretchy) +{ + // LONGTERM on other platforms + // or at leat allow this and implicitly turn it into a spacer + if (c == NULL) + userbug("You cannot add NULL to a uiBox."); + [b->view append:c stretchy:stretchy]; +} + +void uiBoxDelete(uiBox *b, int n) +{ + [b->view delete:n]; +} + +int uiBoxPadded(uiBox *b) +{ + return [b->view isPadded]; +} + +void uiBoxSetPadded(uiBox *b, int padded) +{ + [b->view setPadded:padded]; +} + +static uiBox *finishNewBox(BOOL vertical) +{ + uiBox *b; + + uiDarwinNewControl(uiBox, b); + + b->view = [[boxView alloc] initWithVertical:vertical b:b]; + + return b; +} + +uiBox *uiNewHorizontalBox(void) +{ + return finishNewBox(NO); +} + +uiBox *uiNewVerticalBox(void) +{ + return finishNewBox(YES); +} diff --git a/src/libui_sdl/libui/darwin/button.m b/src/libui_sdl/libui/darwin/button.m new file mode 100644 index 0000000..baccabb --- /dev/null +++ b/src/libui_sdl/libui/darwin/button.m @@ -0,0 +1,113 @@ +// 13 august 2015 +#import "uipriv_darwin.h" + +struct uiButton { + uiDarwinControl c; + NSButton *button; + void (*onClicked)(uiButton *, void *); + void *onClickedData; +}; + +@interface buttonDelegateClass : NSObject { + struct mapTable *buttons; +} +- (IBAction)onClicked:(id)sender; +- (void)registerButton:(uiButton *)b; +- (void)unregisterButton:(uiButton *)b; +@end + +@implementation buttonDelegateClass + +- (id)init +{ + self = [super init]; + if (self) + self->buttons = newMap(); + return self; +} + +- (void)dealloc +{ + mapDestroy(self->buttons); + [super dealloc]; +} + +- (IBAction)onClicked:(id)sender +{ + uiButton *b; + + b = (uiButton *) mapGet(self->buttons, sender); + (*(b->onClicked))(b, b->onClickedData); +} + +- (void)registerButton:(uiButton *)b +{ + mapSet(self->buttons, b->button, b); + [b->button setTarget:self]; + [b->button setAction:@selector(onClicked:)]; +} + +- (void)unregisterButton:(uiButton *)b +{ + [b->button setTarget:nil]; + mapDelete(self->buttons, b->button); +} + +@end + +static buttonDelegateClass *buttonDelegate = nil; + +uiDarwinControlAllDefaultsExceptDestroy(uiButton, button) + +static void uiButtonDestroy(uiControl *c) +{ + uiButton *b = uiButton(c); + + [buttonDelegate unregisterButton:b]; + [b->button release]; + uiFreeControl(uiControl(b)); +} + +char *uiButtonText(uiButton *b) +{ + return uiDarwinNSStringToText([b->button title]); +} + +void uiButtonSetText(uiButton *b, const char *text) +{ + [b->button setTitle:toNSString(text)]; +} + +void uiButtonOnClicked(uiButton *b, void (*f)(uiButton *, void *), void *data) +{ + b->onClicked = f; + b->onClickedData = data; +} + +static void defaultOnClicked(uiButton *b, void *data) +{ + // do nothing +} + +uiButton *uiNewButton(const char *text) +{ + uiButton *b; + + uiDarwinNewControl(uiButton, b); + + b->button = [[NSButton alloc] initWithFrame:NSZeroRect]; + [b->button setTitle:toNSString(text)]; + [b->button setButtonType:NSMomentaryPushInButton]; + [b->button setBordered:YES]; + [b->button setBezelStyle:NSRoundedBezelStyle]; + uiDarwinSetControlFont(b->button, NSRegularControlSize); + + if (buttonDelegate == nil) { + buttonDelegate = [[buttonDelegateClass new] autorelease]; + [delegates addObject:buttonDelegate]; + } + [buttonDelegate registerButton:b]; + uiButtonOnClicked(b, defaultOnClicked, NULL); + + return b; +} diff --git a/src/libui_sdl/libui/darwin/checkbox.m b/src/libui_sdl/libui/darwin/checkbox.m new file mode 100644 index 0000000..dd1ce09 --- /dev/null +++ b/src/libui_sdl/libui/darwin/checkbox.m @@ -0,0 +1,129 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +struct uiCheckbox { + uiDarwinControl c; + NSButton *button; + void (*onToggled)(uiCheckbox *, void *); + void *onToggledData; +}; + +@interface checkboxDelegateClass : NSObject { + struct mapTable *buttons; +} +- (IBAction)onToggled:(id)sender; +- (void)registerCheckbox:(uiCheckbox *)c; +- (void)unregisterCheckbox:(uiCheckbox *)c; +@end + +@implementation checkboxDelegateClass + +- (id)init +{ + self = [super init]; + if (self) + self->buttons = newMap(); + return self; +} + +- (void)dealloc +{ + mapDestroy(self->buttons); + [super dealloc]; +} + +- (IBAction)onToggled:(id)sender +{ + uiCheckbox *c; + + c = (uiCheckbox *) mapGet(self->buttons, sender); + (*(c->onToggled))(c, c->onToggledData); +} + +- (void)registerCheckbox:(uiCheckbox *)c +{ + mapSet(self->buttons, c->button, c); + [c->button setTarget:self]; + [c->button setAction:@selector(onToggled:)]; +} + +- (void)unregisterCheckbox:(uiCheckbox *)c +{ + [c->button setTarget:nil]; + mapDelete(self->buttons, c->button); +} + +@end + +static checkboxDelegateClass *checkboxDelegate = nil; + +uiDarwinControlAllDefaultsExceptDestroy(uiCheckbox, button) + +static void uiCheckboxDestroy(uiControl *cc) +{ + uiCheckbox *c = uiCheckbox(cc); + + [checkboxDelegate unregisterCheckbox:c]; + [c->button release]; + uiFreeControl(uiControl(c)); +} + +char *uiCheckboxText(uiCheckbox *c) +{ + return uiDarwinNSStringToText([c->button title]); +} + +void uiCheckboxSetText(uiCheckbox *c, const char *text) +{ + [c->button setTitle:toNSString(text)]; +} + +void uiCheckboxOnToggled(uiCheckbox *c, void (*f)(uiCheckbox *, void *), void *data) +{ + c->onToggled = f; + c->onToggledData = data; +} + +int uiCheckboxChecked(uiCheckbox *c) +{ + return [c->button state] == NSOnState; +} + +void uiCheckboxSetChecked(uiCheckbox *c, int checked) +{ + NSInteger state; + + state = NSOnState; + if (!checked) + state = NSOffState; + [c->button setState:state]; +} + +static void defaultOnToggled(uiCheckbox *c, void *data) +{ + // do nothing +} + +uiCheckbox *uiNewCheckbox(const char *text) +{ + uiCheckbox *c; + + uiDarwinNewControl(uiCheckbox, c); + + c->button = [[NSButton alloc] initWithFrame:NSZeroRect]; + [c->button setTitle:toNSString(text)]; + [c->button setButtonType:NSSwitchButton]; + // doesn't seem to have an associated bezel style + [c->button setBordered:NO]; + [c->button setTransparent:NO]; + uiDarwinSetControlFont(c->button, NSRegularControlSize); + + if (checkboxDelegate == nil) { + checkboxDelegate = [[checkboxDelegateClass new] autorelease]; + [delegates addObject:checkboxDelegate]; + } + [checkboxDelegate registerCheckbox:c]; + uiCheckboxOnToggled(c, defaultOnToggled, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/darwin/colorbutton.m b/src/libui_sdl/libui/darwin/colorbutton.m new file mode 100644 index 0000000..83b6157 --- /dev/null +++ b/src/libui_sdl/libui/darwin/colorbutton.m @@ -0,0 +1,159 @@ +// 15 may 2016 +#import "uipriv_darwin.h" + +// TODO no intrinsic height? + +@interface colorButton : NSColorWell { + uiColorButton *libui_b; + BOOL libui_changing; + BOOL libui_setting; +} +- (id)initWithFrame:(NSRect)frame libuiColorButton:(uiColorButton *)b; +- (void)deactivateOnClose:(NSNotification *)note; +- (void)libuiColor:(double *)r g:(double *)g b:(double *)b a:(double *)a; +- (void)libuiSetColor:(double)r g:(double)g b:(double)b a:(double)a; +@end + +// only one may be active at one time +static colorButton *activeColorButton = nil; + +struct uiColorButton { + uiDarwinControl c; + colorButton *button; + void (*onChanged)(uiColorButton *, void *); + void *onChangedData; +}; + +@implementation colorButton + +- (id)initWithFrame:(NSRect)frame libuiColorButton:(uiColorButton *)b +{ + self = [super initWithFrame:frame]; + if (self) { + // the default color is white; set it to black first (see -setColor: below for why we do it first) + [self libuiSetColor:0.0 g:0.0 b:0.0 a:1.0]; + + self->libui_b = b; + self->libui_changing = NO; + } + return self; +} + +- (void)activate:(BOOL)exclusive +{ + if (activeColorButton != nil) + activeColorButton->libui_changing = YES; + [NSColorPanel setPickerMask:NSColorPanelAllModesMask]; + [[NSColorPanel sharedColorPanel] setShowsAlpha:YES]; + [super activate:YES]; + activeColorButton = self; + // see stddialogs.m for details + [[NSColorPanel sharedColorPanel] setWorksWhenModal:NO]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deactivateOnClose:) + name:NSWindowWillCloseNotification + object:[NSColorPanel sharedColorPanel]]; +} + +- (void)deactivate +{ + [super deactivate]; + activeColorButton = nil; + if (!self->libui_changing) + [[NSColorPanel sharedColorPanel] orderOut:nil]; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSWindowWillCloseNotification + object:[NSColorPanel sharedColorPanel]]; + self->libui_changing = NO; +} + +- (void)deactivateOnClose:(NSNotification *)note +{ + [self deactivate]; +} + +- (void)setColor:(NSColor *)color +{ + uiColorButton *b = self->libui_b; + + [super setColor:color]; + // this is called by NSColorWell's init, so we have to guard + // also don't signal during a programmatic change + if (b != nil && !self->libui_setting) + (*(b->onChanged))(b, b->onChangedData); +} + +- (void)libuiColor:(double *)r g:(double *)g b:(double *)b a:(double *)a +{ + NSColor *rgba; + CGFloat cr, cg, cb, ca; + + // the given color may not be an RGBA color, which will cause the -getRed:green:blue:alpha: call to throw an exception + rgba = [[self color] colorUsingColorSpace:[NSColorSpace sRGBColorSpace]]; + [rgba getRed:&cr green:&cg blue:&cb alpha:&ca]; + *r = cr; + *g = cg; + *b = cb; + *a = ca; + // rgba will be autoreleased since it isn't a new or init call +} + +- (void)libuiSetColor:(double)r g:(double)g b:(double)b a:(double)a +{ + self->libui_setting = YES; + [self setColor:[NSColor colorWithSRGBRed:r green:g blue:b alpha:a]]; + self->libui_setting = NO; +} + +// NSColorWell has no intrinsic size by default; give it the default Interface Builder size. +- (NSSize)intrinsicContentSize +{ + return NSMakeSize(44, 23); +} + +@end + +uiDarwinControlAllDefaults(uiColorButton, button) + +// we do not want color change events to be sent to any controls other than the color buttons +// see main.m for more details +BOOL colorButtonInhibitSendAction(SEL sel, id from, id to) +{ + if (sel != @selector(changeColor:)) + return NO; + return ![to isKindOfClass:[colorButton class]]; +} + +static void defaultOnChanged(uiColorButton *b, void *data) +{ + // do nothing +} + +void uiColorButtonColor(uiColorButton *b, double *r, double *g, double *bl, double *a) +{ + [b->button libuiColor:r g:g b:bl a:a]; +} + +void uiColorButtonSetColor(uiColorButton *b, double r, double g, double bl, double a) +{ + [b->button libuiSetColor:r g:g b:bl a:a]; +} + +void uiColorButtonOnChanged(uiColorButton *b, void (*f)(uiColorButton *, void *), void *data) +{ + b->onChanged = f; + b->onChangedData = data; +} + +uiColorButton *uiNewColorButton(void) +{ + uiColorButton *b; + + uiDarwinNewControl(uiColorButton, b); + + b->button = [[colorButton alloc] initWithFrame:NSZeroRect libuiColorButton:b]; + + uiColorButtonOnChanged(b, defaultOnChanged, NULL); + + return b; +} diff --git a/src/libui_sdl/libui/darwin/combobox.m b/src/libui_sdl/libui/darwin/combobox.m new file mode 100644 index 0000000..89a2e28 --- /dev/null +++ b/src/libui_sdl/libui/darwin/combobox.m @@ -0,0 +1,145 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +// NSComboBoxes have no intrinsic width; we'll use the default Interface Builder width for them. +// NSPopUpButton is fine. +#define comboboxWidth 96 + +struct uiCombobox { + uiDarwinControl c; + NSPopUpButton *pb; + NSArrayController *pbac; + void (*onSelected)(uiCombobox *, void *); + void *onSelectedData; +}; + +@interface comboboxDelegateClass : NSObject { + struct mapTable *comboboxes; +} +- (IBAction)onSelected:(id)sender; +- (void)registerCombobox:(uiCombobox *)c; +- (void)unregisterCombobox:(uiCombobox *)c; +@end + +@implementation comboboxDelegateClass + +- (id)init +{ + self = [super init]; + if (self) + self->comboboxes = newMap(); + return self; +} + +- (void)dealloc +{ + mapDestroy(self->comboboxes); + [super dealloc]; +} + +- (IBAction)onSelected:(id)sender +{ + uiCombobox *c; + + c = uiCombobox(mapGet(self->comboboxes, sender)); + (*(c->onSelected))(c, c->onSelectedData); +} + +- (void)registerCombobox:(uiCombobox *)c +{ + mapSet(self->comboboxes, c->pb, c); + [c->pb setTarget:self]; + [c->pb setAction:@selector(onSelected:)]; +} + +- (void)unregisterCombobox:(uiCombobox *)c +{ + [c->pb setTarget:nil]; + mapDelete(self->comboboxes, c->pb); +} + +@end + +static comboboxDelegateClass *comboboxDelegate = nil; + +uiDarwinControlAllDefaultsExceptDestroy(uiCombobox, pb) + +static void uiComboboxDestroy(uiControl *cc) +{ + uiCombobox *c = uiCombobox(cc); + + [comboboxDelegate unregisterCombobox:c]; + [c->pb unbind:@"contentObjects"]; + [c->pb unbind:@"selectedIndex"]; + [c->pbac release]; + [c->pb release]; + uiFreeControl(uiControl(c)); +} + +void uiComboboxAppend(uiCombobox *c, const char *text) +{ + [c->pbac addObject:toNSString(text)]; +} + +int uiComboboxSelected(uiCombobox *c) +{ + return [c->pb indexOfSelectedItem]; +} + +void uiComboboxSetSelected(uiCombobox *c, int n) +{ + [c->pb selectItemAtIndex:n]; +} + +void uiComboboxOnSelected(uiCombobox *c, void (*f)(uiCombobox *c, void *data), void *data) +{ + c->onSelected = f; + c->onSelectedData = data; +} + +static void defaultOnSelected(uiCombobox *c, void *data) +{ + // do nothing +} + +uiCombobox *uiNewCombobox(void) +{ + uiCombobox *c; + NSPopUpButtonCell *pbcell; + + uiDarwinNewControl(uiCombobox, c); + + c->pb = [[NSPopUpButton alloc] initWithFrame:NSZeroRect pullsDown:NO]; + [c->pb setPreferredEdge:NSMinYEdge]; + pbcell = (NSPopUpButtonCell *) [c->pb cell]; + [pbcell setArrowPosition:NSPopUpArrowAtBottom]; + // the font defined by Interface Builder is Menu 13, which is lol + // just use the regular control size for consistency + uiDarwinSetControlFont(c->pb, NSRegularControlSize); + + // NSPopUpButton doesn't work like a combobox + // - it automatically selects the first item + // - it doesn't support duplicates + // but we can use a NSArrayController and Cocoa bindings to bypass these restrictions + c->pbac = [NSArrayController new]; + [c->pbac setAvoidsEmptySelection:NO]; + [c->pbac setSelectsInsertedObjects:NO]; + [c->pbac setAutomaticallyRearrangesObjects:NO]; + [c->pb bind:@"contentValues" + toObject:c->pbac + withKeyPath:@"arrangedObjects" + options:nil]; + [c->pb bind:@"selectedIndex" + toObject:c->pbac + withKeyPath:@"selectionIndex" + options:nil]; + + if (comboboxDelegate == nil) { + comboboxDelegate = [[comboboxDelegateClass new] autorelease]; + [delegates addObject:comboboxDelegate]; + } + [comboboxDelegate registerCombobox:c]; + uiComboboxOnSelected(c, defaultOnSelected, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/darwin/control.m b/src/libui_sdl/libui/darwin/control.m new file mode 100644 index 0000000..9eaf47a --- /dev/null +++ b/src/libui_sdl/libui/darwin/control.m @@ -0,0 +1,84 @@ +// 16 august 2015 +#import "uipriv_darwin.h" + +void uiDarwinControlSyncEnableState(uiDarwinControl *c, int state) +{ + (*(c->SyncEnableState))(c, state); +} + +void uiDarwinControlSetSuperview(uiDarwinControl *c, NSView *superview) +{ + (*(c->SetSuperview))(c, superview); +} + +BOOL uiDarwinControlHugsTrailingEdge(uiDarwinControl *c) +{ + return (*(c->HugsTrailingEdge))(c); +} + +BOOL uiDarwinControlHugsBottom(uiDarwinControl *c) +{ + return (*(c->HugsBottom))(c); +} + +void uiDarwinControlChildEdgeHuggingChanged(uiDarwinControl *c) +{ + (*(c->ChildEdgeHuggingChanged))(c); +} + +NSLayoutPriority uiDarwinControlHuggingPriority(uiDarwinControl *c, NSLayoutConstraintOrientation orientation) +{ + return (*(c->HuggingPriority))(c, orientation); +} + +void uiDarwinControlSetHuggingPriority(uiDarwinControl *c, NSLayoutPriority priority, NSLayoutConstraintOrientation orientation) +{ + (*(c->SetHuggingPriority))(c, priority, orientation); +} + +void uiDarwinControlChildVisibilityChanged(uiDarwinControl *c) +{ + (*(c->ChildVisibilityChanged))(c); +} + +void uiDarwinSetControlFont(NSControl *c, NSControlSize size) +{ + [c setFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:size]]]; +} + +#define uiDarwinControlSignature 0x44617277 + +uiDarwinControl *uiDarwinAllocControl(size_t n, uint32_t typesig, const char *typenamestr) +{ + return uiDarwinControl(uiAllocControl(n, uiDarwinControlSignature, typesig, typenamestr)); +} + +BOOL uiDarwinShouldStopSyncEnableState(uiDarwinControl *c, BOOL enabled) +{ + int ce; + + ce = uiControlEnabled(uiControl(c)); + // only stop if we're going from disabled back to enabled; don't stop under any other condition + // (if we stop when going from enabled to disabled then enabled children of a disabled control won't get disabled at the OS level) + if (!ce && enabled) + return YES; + return NO; +} + +void uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl *c) +{ + uiControl *parent; + + parent = uiControlParent(uiControl(c)); + if (parent != NULL) + uiDarwinControlChildEdgeHuggingChanged(uiDarwinControl(parent)); +} + +void uiDarwinNotifyVisibilityChanged(uiDarwinControl *c) +{ + uiControl *parent; + + parent = uiControlParent(uiControl(c)); + if (parent != NULL) + uiDarwinControlChildVisibilityChanged(uiDarwinControl(parent)); +} diff --git a/src/libui_sdl/libui/darwin/datetimepicker.m b/src/libui_sdl/libui/darwin/datetimepicker.m new file mode 100644 index 0000000..44364d9 --- /dev/null +++ b/src/libui_sdl/libui/darwin/datetimepicker.m @@ -0,0 +1,42 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +struct uiDateTimePicker { + uiDarwinControl c; + NSDatePicker *dp; +}; + +uiDarwinControlAllDefaults(uiDateTimePicker, dp) + +static uiDateTimePicker *finishNewDateTimePicker(NSDatePickerElementFlags elements) +{ + uiDateTimePicker *d; + + uiDarwinNewControl(uiDateTimePicker, d); + + d->dp = [[NSDatePicker alloc] initWithFrame:NSZeroRect]; + [d->dp setBordered:NO]; + [d->dp setBezeled:YES]; + [d->dp setDrawsBackground:YES]; + [d->dp setDatePickerStyle:NSTextFieldAndStepperDatePickerStyle]; + [d->dp setDatePickerElements:elements]; + [d->dp setDatePickerMode:NSSingleDateMode]; + uiDarwinSetControlFont(d->dp, NSRegularControlSize); + + return d; +} + +uiDateTimePicker *uiNewDateTimePicker(void) +{ + return finishNewDateTimePicker(NSYearMonthDayDatePickerElementFlag | NSHourMinuteSecondDatePickerElementFlag); +} + +uiDateTimePicker *uiNewDatePicker(void) +{ + return finishNewDateTimePicker(NSYearMonthDayDatePickerElementFlag); +} + +uiDateTimePicker *uiNewTimePicker(void) +{ + return finishNewDateTimePicker(NSHourMinuteSecondDatePickerElementFlag); +} diff --git a/src/libui_sdl/libui/darwin/debug.m b/src/libui_sdl/libui/darwin/debug.m new file mode 100644 index 0000000..c91c6a7 --- /dev/null +++ b/src/libui_sdl/libui/darwin/debug.m @@ -0,0 +1,19 @@ +// 13 may 2016 +#import "uipriv_darwin.h" + +// LONGTERM don't halt on release builds + +void realbug(const char *file, const char *line, const char *func, const char *prefix, const char *format, va_list ap) +{ + NSMutableString *str; + NSString *formatted; + + str = [NSMutableString new]; + [str appendString:[NSString stringWithFormat:@"[libui] %s:%s:%s() %s", file, line, func, prefix]]; + formatted = [[NSString alloc] initWithFormat:[NSString stringWithUTF8String:format] arguments:ap]; + [str appendString:formatted]; + [formatted release]; + NSLog(@"%@", str); + [str release]; + __builtin_trap(); +} diff --git a/src/libui_sdl/libui/darwin/draw.m b/src/libui_sdl/libui/darwin/draw.m new file mode 100644 index 0000000..262ad3e --- /dev/null +++ b/src/libui_sdl/libui/darwin/draw.m @@ -0,0 +1,454 @@ +// 6 september 2015 +#import "uipriv_darwin.h" + +struct uiDrawPath { + CGMutablePathRef path; + uiDrawFillMode fillMode; + BOOL ended; +}; + +uiDrawPath *uiDrawNewPath(uiDrawFillMode mode) +{ + uiDrawPath *p; + + p = uiNew(uiDrawPath); + p->path = CGPathCreateMutable(); + p->fillMode = mode; + return p; +} + +void uiDrawFreePath(uiDrawPath *p) +{ + CGPathRelease((CGPathRef) (p->path)); + uiFree(p); +} + +void uiDrawPathNewFigure(uiDrawPath *p, double x, double y) +{ + if (p->ended) + userbug("You cannot call uiDrawPathNewFigure() on a uiDrawPath that has already been ended. (path; %p)", p); + CGPathMoveToPoint(p->path, NULL, x, y); +} + +void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) +{ + double sinStart, cosStart; + double startx, starty; + + if (p->ended) + userbug("You cannot call uiDrawPathNewFigureWithArc() on a uiDrawPath that has already been ended. (path; %p)", p); + sinStart = sin(startAngle); + cosStart = cos(startAngle); + startx = xCenter + radius * cosStart; + starty = yCenter + radius * sinStart; + CGPathMoveToPoint(p->path, NULL, startx, starty); + uiDrawPathArcTo(p, xCenter, yCenter, radius, startAngle, sweep, negative); +} + +void uiDrawPathLineTo(uiDrawPath *p, double x, double y) +{ + // TODO refine this to require being in a path + if (p->ended) + implbug("attempt to add line to ended path in uiDrawPathLineTo()"); + CGPathAddLineToPoint(p->path, NULL, x, y); +} + +void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) +{ + bool cw; + + // TODO likewise + if (p->ended) + implbug("attempt to add arc to ended path in uiDrawPathArcTo()"); + if (sweep > 2 * uiPi) + sweep = 2 * uiPi; + cw = false; + if (negative) + cw = true; + CGPathAddArc(p->path, NULL, + xCenter, yCenter, + radius, + startAngle, startAngle + sweep, + cw); +} + +void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY) +{ + // TODO likewise + if (p->ended) + implbug("attempt to add bezier to ended path in uiDrawPathBezierTo()"); + CGPathAddCurveToPoint(p->path, NULL, + c1x, c1y, + c2x, c2y, + endX, endY); +} + +void uiDrawPathCloseFigure(uiDrawPath *p) +{ + // TODO likewise + if (p->ended) + implbug("attempt to close figure of ended path in uiDrawPathCloseFigure()"); + CGPathCloseSubpath(p->path); +} + +void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height) +{ + if (p->ended) + userbug("You cannot call uiDrawPathAddRectangle() on a uiDrawPath that has already been ended. (path; %p)", p); + CGPathAddRect(p->path, NULL, CGRectMake(x, y, width, height)); +} + +void uiDrawPathEnd(uiDrawPath *p) +{ + p->ended = TRUE; +} + +struct uiDrawContext { + CGContextRef c; + CGFloat height; // needed for text; see below +}; + +uiDrawContext *newContext(CGContextRef ctxt, CGFloat height) +{ + uiDrawContext *c; + + c = uiNew(uiDrawContext); + c->c = ctxt; + c->height = height; + return c; +} + +void freeContext(uiDrawContext *c) +{ + uiFree(c); +} + +// a stroke is identical to a fill of a stroked path +// we need to do this in order to stroke with a gradient; see http://stackoverflow.com/a/25034854/3408572 +// doing this for other brushes works too +void uiDrawStroke(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b, uiDrawStrokeParams *p) +{ + CGLineCap cap; + CGLineJoin join; + CGPathRef dashPath; + CGFloat *dashes; + size_t i; + uiDrawPath p2; + + if (!path->ended) + userbug("You cannot call uiDrawStroke() on a uiDrawPath that has not been ended. (path: %p)", path); + + switch (p->Cap) { + case uiDrawLineCapFlat: + cap = kCGLineCapButt; + break; + case uiDrawLineCapRound: + cap = kCGLineCapRound; + break; + case uiDrawLineCapSquare: + cap = kCGLineCapSquare; + break; + } + switch (p->Join) { + case uiDrawLineJoinMiter: + join = kCGLineJoinMiter; + break; + case uiDrawLineJoinRound: + join = kCGLineJoinRound; + break; + case uiDrawLineJoinBevel: + join = kCGLineJoinBevel; + break; + } + + // create a temporary path identical to the previous one + dashPath = (CGPathRef) path->path; + if (p->NumDashes != 0) { + dashes = (CGFloat *) uiAlloc(p->NumDashes * sizeof (CGFloat), "CGFloat[]"); + for (i = 0; i < p->NumDashes; i++) + dashes[i] = p->Dashes[i]; + dashPath = CGPathCreateCopyByDashingPath(path->path, + NULL, + p->DashPhase, + dashes, + p->NumDashes); + uiFree(dashes); + } + // the documentation is wrong: this produces a path suitable for calling CGPathCreateCopyByStrokingPath(), not for filling directly + // the cast is safe; we never modify the CGPathRef and always cast it back to a CGPathRef anyway + p2.path = (CGMutablePathRef) CGPathCreateCopyByStrokingPath(dashPath, + NULL, + p->Thickness, + cap, + join, + p->MiterLimit); + if (p->NumDashes != 0) + CGPathRelease(dashPath); + + // always draw stroke fills using the winding rule + // otherwise intersecting figures won't draw correctly + p2.fillMode = uiDrawFillModeWinding; + p2.ended = path->ended; + uiDrawFill(c, &p2, b); + // and clean up + CGPathRelease((CGPathRef) (p2.path)); +} + +// for a solid fill, we can merely have Core Graphics fill directly +static void fillSolid(CGContextRef ctxt, uiDrawPath *p, uiDrawBrush *b) +{ + // TODO this uses DeviceRGB; switch to sRGB + CGContextSetRGBFillColor(ctxt, b->R, b->G, b->B, b->A); + switch (p->fillMode) { + case uiDrawFillModeWinding: + CGContextFillPath(ctxt); + break; + case uiDrawFillModeAlternate: + CGContextEOFillPath(ctxt); + break; + } +} + +// for a gradient fill, we need to clip to the path and then draw the gradient +// see http://stackoverflow.com/a/25034854/3408572 +static void fillGradient(CGContextRef ctxt, uiDrawPath *p, uiDrawBrush *b) +{ + CGGradientRef gradient; + CGColorSpaceRef colorspace; + CGFloat *colors; + CGFloat *locations; + size_t i; + + // gradients need a color space + // for consistency with windows, use sRGB + colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + + // make the gradient + colors = uiAlloc(b->NumStops * 4 * sizeof (CGFloat), "CGFloat[]"); + locations = uiAlloc(b->NumStops * sizeof (CGFloat), "CGFloat[]"); + for (i = 0; i < b->NumStops; i++) { + colors[i * 4 + 0] = b->Stops[i].R; + colors[i * 4 + 1] = b->Stops[i].G; + colors[i * 4 + 2] = b->Stops[i].B; + colors[i * 4 + 3] = b->Stops[i].A; + locations[i] = b->Stops[i].Pos; + } + gradient = CGGradientCreateWithColorComponents(colorspace, colors, locations, b->NumStops); + uiFree(locations); + uiFree(colors); + + // because we're mucking with clipping, we need to save the graphics state and restore it later + CGContextSaveGState(ctxt); + + // clip + switch (p->fillMode) { + case uiDrawFillModeWinding: + CGContextClip(ctxt); + break; + case uiDrawFillModeAlternate: + CGContextEOClip(ctxt); + break; + } + + // draw the gradient + switch (b->Type) { + case uiDrawBrushTypeLinearGradient: + CGContextDrawLinearGradient(ctxt, + gradient, + CGPointMake(b->X0, b->Y0), + CGPointMake(b->X1, b->Y1), + kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + break; + case uiDrawBrushTypeRadialGradient: + CGContextDrawRadialGradient(ctxt, + gradient, + CGPointMake(b->X0, b->Y0), + // make the start circle radius 0 to make it a point + 0, + CGPointMake(b->X1, b->Y1), + b->OuterRadius, + kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); + break; + } + + // and clean up + CGContextRestoreGState(ctxt); + CGGradientRelease(gradient); + CGColorSpaceRelease(colorspace); +} + +void uiDrawFill(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b) +{ + if (!path->ended) + userbug("You cannot call uiDrawStroke() on a uiDrawPath that has not been ended. (path: %p)", path); + CGContextAddPath(c->c, (CGPathRef) (path->path)); + switch (b->Type) { + case uiDrawBrushTypeSolid: + fillSolid(c->c, path, b); + return; + case uiDrawBrushTypeLinearGradient: + case uiDrawBrushTypeRadialGradient: + fillGradient(c->c, path, b); + return; +// case uiDrawBrushTypeImage: + // TODO + return; + } + userbug("Unknown brush type %d passed to uiDrawFill().", b->Type); +} + +static void m2c(uiDrawMatrix *m, CGAffineTransform *c) +{ + c->a = m->M11; + c->b = m->M12; + c->c = m->M21; + c->d = m->M22; + c->tx = m->M31; + c->ty = m->M32; +} + +static void c2m(CGAffineTransform *c, uiDrawMatrix *m) +{ + m->M11 = c->a; + m->M12 = c->b; + m->M21 = c->c; + m->M22 = c->d; + m->M31 = c->tx; + m->M32 = c->ty; +} + +void uiDrawMatrixTranslate(uiDrawMatrix *m, double x, double y) +{ + CGAffineTransform c; + + m2c(m, &c); + c = CGAffineTransformTranslate(c, x, y); + c2m(&c, m); +} + +void uiDrawMatrixScale(uiDrawMatrix *m, double xCenter, double yCenter, double x, double y) +{ + CGAffineTransform c; + double xt, yt; + + m2c(m, &c); + xt = x; + yt = y; + scaleCenter(xCenter, yCenter, &xt, &yt); + c = CGAffineTransformTranslate(c, xt, yt); + c = CGAffineTransformScale(c, x, y); + c = CGAffineTransformTranslate(c, -xt, -yt); + c2m(&c, m); +} + +void uiDrawMatrixRotate(uiDrawMatrix *m, double x, double y, double amount) +{ + CGAffineTransform c; + + m2c(m, &c); + c = CGAffineTransformTranslate(c, x, y); + c = CGAffineTransformRotate(c, amount); + c = CGAffineTransformTranslate(c, -x, -y); + c2m(&c, m); +} + +void uiDrawMatrixSkew(uiDrawMatrix *m, double x, double y, double xamount, double yamount) +{ + fallbackSkew(m, x, y, xamount, yamount); +} + +void uiDrawMatrixMultiply(uiDrawMatrix *dest, uiDrawMatrix *src) +{ + CGAffineTransform c; + CGAffineTransform d; + + m2c(dest, &c); + m2c(src, &d); + c = CGAffineTransformConcat(c, d); + c2m(&c, dest); +} + +// there is no test for invertibility; CGAffineTransformInvert() is merely documented as returning the matrix unchanged if it isn't invertible +// therefore, special care must be taken to catch matrices who are their own inverses +// TODO figure out which matrices these are and do so +int uiDrawMatrixInvertible(uiDrawMatrix *m) +{ + CGAffineTransform c, d; + + m2c(m, &c); + d = CGAffineTransformInvert(c); + return CGAffineTransformEqualToTransform(c, d) == false; +} + +int uiDrawMatrixInvert(uiDrawMatrix *m) +{ + CGAffineTransform c, d; + + m2c(m, &c); + d = CGAffineTransformInvert(c); + if (CGAffineTransformEqualToTransform(c, d)) + return 0; + c2m(&d, m); + return 1; +} + +void uiDrawMatrixTransformPoint(uiDrawMatrix *m, double *x, double *y) +{ + CGAffineTransform c; + CGPoint p; + + m2c(m, &c); + p = CGPointApplyAffineTransform(CGPointMake(*x, *y), c); + *x = p.x; + *y = p.y; +} + +void uiDrawMatrixTransformSize(uiDrawMatrix *m, double *x, double *y) +{ + CGAffineTransform c; + CGSize s; + + m2c(m, &c); + s = CGSizeApplyAffineTransform(CGSizeMake(*x, *y), c); + *x = s.width; + *y = s.height; +} + +void uiDrawTransform(uiDrawContext *c, uiDrawMatrix *m) +{ + CGAffineTransform cm; + + m2c(m, &cm); + CGContextConcatCTM(c->c, cm); +} + +void uiDrawClip(uiDrawContext *c, uiDrawPath *path) +{ + if (!path->ended) + userbug("You cannot call uiDrawCilp() on a uiDrawPath that has not been ended. (path: %p)", path); + CGContextAddPath(c->c, (CGPathRef) (path->path)); + switch (path->fillMode) { + case uiDrawFillModeWinding: + CGContextClip(c->c); + break; + case uiDrawFillModeAlternate: + CGContextEOClip(c->c); + break; + } +} + +// TODO figure out what besides transforms these save/restore on all platforms +void uiDrawSave(uiDrawContext *c) +{ + CGContextSaveGState(c->c); +} + +void uiDrawRestore(uiDrawContext *c) +{ + CGContextRestoreGState(c->c); +} + +void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout) +{ + doDrawText(c->c, c->height, x, y, layout); +} diff --git a/src/libui_sdl/libui/darwin/drawtext.m b/src/libui_sdl/libui/darwin/drawtext.m new file mode 100644 index 0000000..c376536 --- /dev/null +++ b/src/libui_sdl/libui/darwin/drawtext.m @@ -0,0 +1,655 @@ +// 6 september 2015 +#import "uipriv_darwin.h" + +// TODO +#define complain(...) implbug(__VA_ARGS__) + +// TODO double-check that we are properly handling allocation failures (or just toll free bridge from cocoa) +struct uiDrawFontFamilies { + CFArrayRef fonts; +}; + +uiDrawFontFamilies *uiDrawListFontFamilies(void) +{ + uiDrawFontFamilies *ff; + + ff = uiNew(uiDrawFontFamilies); + ff->fonts = CTFontManagerCopyAvailableFontFamilyNames(); + if (ff->fonts == NULL) + implbug("error getting available font names (no reason specified) (TODO)"); + return ff; +} + +int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff) +{ + return CFArrayGetCount(ff->fonts); +} + +char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n) +{ + CFStringRef familystr; + char *family; + + familystr = (CFStringRef) CFArrayGetValueAtIndex(ff->fonts, n); + // toll-free bridge + family = uiDarwinNSStringToText((NSString *) familystr); + // Get Rule means we do not free familystr + return family; +} + +void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff) +{ + CFRelease(ff->fonts); + uiFree(ff); +} + +struct uiDrawTextFont { + CTFontRef f; +}; + +uiDrawTextFont *mkTextFont(CTFontRef f, BOOL retain) +{ + uiDrawTextFont *font; + + font = uiNew(uiDrawTextFont); + font->f = f; + if (retain) + CFRetain(font->f); + return font; +} + +uiDrawTextFont *mkTextFontFromNSFont(NSFont *f) +{ + // toll-free bridging; we do retain, though + return mkTextFont((CTFontRef) f, YES); +} + +static CFMutableDictionaryRef newAttrList(void) +{ + CFMutableDictionaryRef attr; + + attr = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + if (attr == NULL) + complain("error creating attribute dictionary in newAttrList()()"); + return attr; +} + +static void addFontFamilyAttr(CFMutableDictionaryRef attr, const char *family) +{ + CFStringRef cfstr; + + cfstr = CFStringCreateWithCString(NULL, family, kCFStringEncodingUTF8); + if (cfstr == NULL) + complain("error creating font family name CFStringRef in addFontFamilyAttr()"); + CFDictionaryAddValue(attr, kCTFontFamilyNameAttribute, cfstr); + CFRelease(cfstr); // dictionary holds its own reference +} + +static void addFontSizeAttr(CFMutableDictionaryRef attr, double size) +{ + CFNumberRef n; + + n = CFNumberCreate(NULL, kCFNumberDoubleType, &size); + CFDictionaryAddValue(attr, kCTFontSizeAttribute, n); + CFRelease(n); +} + +#if 0 +TODO +// See http://stackoverflow.com/questions/4810409/does-coretext-support-small-caps/4811371#4811371 and https://git.gnome.org/browse/pango/tree/pango/pangocoretext-fontmap.c for what these do +// And fortunately, unlike the traits (see below), unmatched features are simply ignored without affecting the other features :D +static void addFontSmallCapsAttr(CFMutableDictionaryRef attr) +{ + CFMutableArrayRef outerArray; + CFMutableDictionaryRef innerDict; + CFNumberRef numType, numSelector; + int num; + + outerArray = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks); + if (outerArray == NULL) + complain("error creating outer CFArray for adding small caps attributes in addFontSmallCapsAttr()"); + + // Apple's headers say these are deprecated, but a few fonts still rely on them + num = kLetterCaseType; + numType = CFNumberCreate(NULL, kCFNumberIntType, &num); + num = kSmallCapsSelector; + numSelector = CFNumberCreate(NULL, kCFNumberIntType, &num); + innerDict = newAttrList(); + CFDictionaryAddValue(innerDict, kCTFontFeatureTypeIdentifierKey, numType); + CFRelease(numType); + CFDictionaryAddValue(innerDict, kCTFontFeatureSelectorIdentifierKey, numSelector); + CFRelease(numSelector); + CFArrayAppendValue(outerArray, innerDict); + CFRelease(innerDict); // and likewise for CFArray + + // these are the non-deprecated versions of the above; some fonts have these instead + num = kLowerCaseType; + numType = CFNumberCreate(NULL, kCFNumberIntType, &num); + num = kLowerCaseSmallCapsSelector; + numSelector = CFNumberCreate(NULL, kCFNumberIntType, &num); + innerDict = newAttrList(); + CFDictionaryAddValue(innerDict, kCTFontFeatureTypeIdentifierKey, numType); + CFRelease(numType); + CFDictionaryAddValue(innerDict, kCTFontFeatureSelectorIdentifierKey, numSelector); + CFRelease(numSelector); + CFArrayAppendValue(outerArray, innerDict); + CFRelease(innerDict); // and likewise for CFArray + + CFDictionaryAddValue(attr, kCTFontFeatureSettingsAttribute, outerArray); + CFRelease(outerArray); +} +#endif + +// Named constants for these were NOT added until 10.11, and even then they were added as external symbols instead of macros, so we can't use them directly :( +// kode54 got these for me before I had access to El Capitan; thanks to him. +#define ourNSFontWeightUltraLight -0.800000 +#define ourNSFontWeightThin -0.600000 +#define ourNSFontWeightLight -0.400000 +#define ourNSFontWeightRegular 0.000000 +#define ourNSFontWeightMedium 0.230000 +#define ourNSFontWeightSemibold 0.300000 +#define ourNSFontWeightBold 0.400000 +#define ourNSFontWeightHeavy 0.560000 +#define ourNSFontWeightBlack 0.620000 +static const CGFloat ctWeights[] = { + // yeah these two have their names swapped; blame Pango + [uiDrawTextWeightThin] = ourNSFontWeightUltraLight, + [uiDrawTextWeightUltraLight] = ourNSFontWeightThin, + [uiDrawTextWeightLight] = ourNSFontWeightLight, + // for this one let's go between Light and Regular + // we're doing nearest so if there happens to be an exact value hopefully it's close enough + [uiDrawTextWeightBook] = ourNSFontWeightLight + ((ourNSFontWeightRegular - ourNSFontWeightLight) / 2), + [uiDrawTextWeightNormal] = ourNSFontWeightRegular, + [uiDrawTextWeightMedium] = ourNSFontWeightMedium, + [uiDrawTextWeightSemiBold] = ourNSFontWeightSemibold, + [uiDrawTextWeightBold] = ourNSFontWeightBold, + // for this one let's go between Bold and Heavy + [uiDrawTextWeightUltraBold] = ourNSFontWeightBold + ((ourNSFontWeightHeavy - ourNSFontWeightBold) / 2), + [uiDrawTextWeightHeavy] = ourNSFontWeightHeavy, + [uiDrawTextWeightUltraHeavy] = ourNSFontWeightBlack, +}; + +// Unfortunately there are still no named constants for these. +// Let's just use normalized widths. +// As far as I can tell (OS X only ships with condensed fonts, not expanded fonts; TODO), regardless of condensed or expanded, negative means condensed and positive means expanded. +// TODO verify this is correct +static const CGFloat ctStretches[] = { + [uiDrawTextStretchUltraCondensed] = -1.0, + [uiDrawTextStretchExtraCondensed] = -0.75, + [uiDrawTextStretchCondensed] = -0.5, + [uiDrawTextStretchSemiCondensed] = -0.25, + [uiDrawTextStretchNormal] = 0.0, + [uiDrawTextStretchSemiExpanded] = 0.25, + [uiDrawTextStretchExpanded] = 0.5, + [uiDrawTextStretchExtraExpanded] = 0.75, + [uiDrawTextStretchUltraExpanded] = 1.0, +}; + +struct closeness { + CFIndex index; + CGFloat weight; + CGFloat italic; + CGFloat stretch; + CGFloat distance; +}; + +// Stupidity: CTFont requires an **exact match for the entire traits dictionary**, otherwise it will **drop ALL the traits**. +// We have to implement the closest match ourselves. +// Also we have to do this before adding the small caps flags, because the matching descriptors won't have those. +CTFontDescriptorRef matchTraits(CTFontDescriptorRef against, uiDrawTextWeight weight, uiDrawTextItalic italic, uiDrawTextStretch stretch) +{ + CGFloat targetWeight; + CGFloat italicCloseness, obliqueCloseness, normalCloseness; + CGFloat targetStretch; + CFArrayRef matching; + CFIndex i, n; + struct closeness *closeness; + CTFontDescriptorRef current; + CTFontDescriptorRef out; + + targetWeight = ctWeights[weight]; + switch (italic) { + case uiDrawTextItalicNormal: + italicCloseness = 1; + obliqueCloseness = 1; + normalCloseness = 0; + break; + case uiDrawTextItalicOblique: + italicCloseness = 0.5; + obliqueCloseness = 0; + normalCloseness = 1; + break; + case uiDrawTextItalicItalic: + italicCloseness = 0; + obliqueCloseness = 0.5; + normalCloseness = 1; + break; + } + targetStretch = ctStretches[stretch]; + + matching = CTFontDescriptorCreateMatchingFontDescriptors(against, NULL); + if (matching == NULL) + // no matches; give the original back and hope for the best + return against; + n = CFArrayGetCount(matching); + if (n == 0) { + // likewise + CFRelease(matching); + return against; + } + + closeness = (struct closeness *) uiAlloc(n * sizeof (struct closeness), "struct closeness[]"); + for (i = 0; i < n; i++) { + CFDictionaryRef traits; + CFNumberRef cfnum; + CTFontSymbolicTraits symbolic; + + closeness[i].index = i; + + current = CFArrayGetValueAtIndex(matching, i); + traits = CTFontDescriptorCopyAttribute(current, kCTFontTraitsAttribute); + if (traits == NULL) { + // couldn't get traits; be safe by ranking it lowest + // LONGTERM figure out what the longest possible distances are + closeness[i].weight = 3; + closeness[i].italic = 2; + closeness[i].stretch = 3; + continue; + } + + symbolic = 0; // assume no symbolic traits if none are listed + cfnum = CFDictionaryGetValue(traits, kCTFontSymbolicTrait); + if (cfnum != NULL) { + SInt32 s; + + if (CFNumberGetValue(cfnum, kCFNumberSInt32Type, &s) == false) + complain("error getting symbolic traits in matchTraits()"); + symbolic = (CTFontSymbolicTraits) s; + // Get rule; do not release cfnum + } + + // now try weight + cfnum = CFDictionaryGetValue(traits, kCTFontWeightTrait); + if (cfnum != NULL) { + CGFloat val; + + // LONGTERM instead of complaining for this and width and possibly also symbolic traits above, should we just fall through to the default? + if (CFNumberGetValue(cfnum, kCFNumberCGFloatType, &val) == false) + complain("error getting weight value in matchTraits()"); + closeness[i].weight = val - targetWeight; + } else + // okay there's no weight key; let's try the literal meaning of the symbolic constant + // LONGTERM is the weight key guaranteed? + if ((symbolic & kCTFontBoldTrait) != 0) + closeness[i].weight = ourNSFontWeightBold - targetWeight; + else + closeness[i].weight = ourNSFontWeightRegular - targetWeight; + + // italics is a bit harder because Core Text doesn't expose a concept of obliqueness + // Pango just does a g_strrstr() (backwards case-sensitive search) for "Oblique" in the font's style name (see https://git.gnome.org/browse/pango/tree/pango/pangocoretext-fontmap.c); let's do that too I guess + if ((symbolic & kCTFontItalicTrait) != 0) + closeness[i].italic = italicCloseness; + else { + CFStringRef styleName; + BOOL isOblique; + + isOblique = NO; // default value + styleName = CTFontDescriptorCopyAttribute(current, kCTFontStyleNameAttribute); + if (styleName != NULL) { + CFRange range; + + // note the use of the toll-free bridge for the string literal, since CFSTR() *can* return NULL + range = CFStringFind(styleName, (CFStringRef) @"Oblique", kCFCompareBackwards); + if (range.location != kCFNotFound) + isOblique = YES; + CFRelease(styleName); + } + if (isOblique) + closeness[i].italic = obliqueCloseness; + else + closeness[i].italic = normalCloseness; + } + + // now try width + // TODO this does not seem to be enough for Skia's extended variants; the width trait is 0 but the Expanded flag is on + // TODO verify the rest of this matrix (what matrix?) + cfnum = CFDictionaryGetValue(traits, kCTFontWidthTrait); + if (cfnum != NULL) { + CGFloat val; + + if (CFNumberGetValue(cfnum, kCFNumberCGFloatType, &val) == false) + complain("error getting width value in matchTraits()"); + closeness[i].stretch = val - targetStretch; + } else + // okay there's no width key; let's try the literal meaning of the symbolic constant + // LONGTERM is the width key guaranteed? + if ((symbolic & kCTFontExpandedTrait) != 0) + closeness[i].stretch = 1.0 - targetStretch; + else if ((symbolic & kCTFontCondensedTrait) != 0) + closeness[i].stretch = -1.0 - targetStretch; + else + closeness[i].stretch = 0.0 - targetStretch; + + CFRelease(traits); + } + + // now figure out the 3-space difference between the three and sort by that + for (i = 0; i < n; i++) { + CGFloat weight, italic, stretch; + + weight = closeness[i].weight; + weight *= weight; + italic = closeness[i].italic; + italic *= italic; + stretch = closeness[i].stretch; + stretch *= stretch; + closeness[i].distance = sqrt(weight + italic + stretch); + } + qsort_b(closeness, n, sizeof (struct closeness), ^(const void *aa, const void *bb) { + const struct closeness *a = (const struct closeness *) aa; + const struct closeness *b = (const struct closeness *) bb; + + // via http://www.gnu.org/software/libc/manual/html_node/Comparison-Functions.html#Comparison-Functions + // LONGTERM is this really the best way? isn't it the same as if (*a < *b) return -1; if (*a > *b) return 1; return 0; ? + return (a->distance > b->distance) - (a->distance < b->distance); + }); + // and the first element of the sorted array is what we want + out = CFArrayGetValueAtIndex(matching, closeness[0].index); + CFRetain(out); // get rule + + // release everything + uiFree(closeness); + CFRelease(matching); + // and release the original descriptor since we no longer need it + CFRelease(against); + + return out; +} + +// Now remember what I said earlier about having to add the small caps traits after calling the above? This gets a dictionary back so we can do so. +CFMutableDictionaryRef extractAttributes(CTFontDescriptorRef desc) +{ + CFDictionaryRef dict; + CFMutableDictionaryRef mdict; + + dict = CTFontDescriptorCopyAttributes(desc); + // this might not be mutable, so make a mutable copy + mdict = CFDictionaryCreateMutableCopy(NULL, 0, dict); + CFRelease(dict); + return mdict; +} + +uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc) +{ + CTFontRef f; + CFMutableDictionaryRef attr; + CTFontDescriptorRef cfdesc; + + attr = newAttrList(); + addFontFamilyAttr(attr, desc->Family); + addFontSizeAttr(attr, desc->Size); + + // now we have to do the traits matching, so create a descriptor, match the traits, and then get the attributes back + cfdesc = CTFontDescriptorCreateWithAttributes(attr); + // TODO release attr? + cfdesc = matchTraits(cfdesc, desc->Weight, desc->Italic, desc->Stretch); + + // specify the initial size again just to be safe + f = CTFontCreateWithFontDescriptor(cfdesc, desc->Size, NULL); + // TODO release cfdesc? + + return mkTextFont(f, NO); // we hold the initial reference; no need to retain again +} + +void uiDrawFreeTextFont(uiDrawTextFont *font) +{ + CFRelease(font->f); + uiFree(font); +} + +uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font) +{ + return (uintptr_t) (font->f); +} + +void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc) +{ + // TODO +} + +// text sizes and user space points are identical: +// - https://developer.apple.com/library/mac/documentation/TextFonts/Conceptual/CocoaTextArchitecture/TypoFeatures/TextSystemFeatures.html#//apple_ref/doc/uid/TP40009459-CH6-51627-BBCCHIFF text points are 72 per inch +// - https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CocoaDrawingGuide/Transforms/Transforms.html#//apple_ref/doc/uid/TP40003290-CH204-SW5 user space points are 72 per inch +void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics) +{ + metrics->Ascent = CTFontGetAscent(font->f); + metrics->Descent = CTFontGetDescent(font->f); + metrics->Leading = CTFontGetLeading(font->f); + metrics->UnderlinePos = CTFontGetUnderlinePosition(font->f); + metrics->UnderlineThickness = CTFontGetUnderlineThickness(font->f); +} + +struct uiDrawTextLayout { + CFMutableAttributedStringRef mas; + CFRange *charsToRanges; + double width; +}; + +uiDrawTextLayout *uiDrawNewTextLayout(const char *str, uiDrawTextFont *defaultFont, double width) +{ + uiDrawTextLayout *layout; + CFAttributedStringRef immutable; + CFMutableDictionaryRef attr; + CFStringRef backing; + CFIndex i, j, n; + + layout = uiNew(uiDrawTextLayout); + + // TODO docs say we need to use a different set of key callbacks + // TODO see if the font attribute key callbacks need to be the same + attr = newAttrList(); + // this will retain defaultFont->f; no need to worry + CFDictionaryAddValue(attr, kCTFontAttributeName, defaultFont->f); + + immutable = CFAttributedStringCreate(NULL, (CFStringRef) [NSString stringWithUTF8String:str], attr); + if (immutable == NULL) + complain("error creating immutable attributed string in uiDrawNewTextLayout()"); + CFRelease(attr); + + layout->mas = CFAttributedStringCreateMutableCopy(NULL, 0, immutable); + if (layout->mas == NULL) + complain("error creating attributed string in uiDrawNewTextLayout()"); + CFRelease(immutable); + + uiDrawTextLayoutSetWidth(layout, width); + + // unfortunately the CFRanges for attributes expect UTF-16 codepoints + // we want graphemes + // fortunately CFStringGetRangeOfComposedCharactersAtIndex() is here for us + // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Strings/Articles/stringsClusters.html says that this does work on all multi-codepoint graphemes (despite the name), and that this is the preferred function for this particular job anyway + backing = CFAttributedStringGetString(layout->mas); + n = CFStringGetLength(backing); + // allocate one extra, just to be safe + layout->charsToRanges = (CFRange *) uiAlloc((n + 1) * sizeof (CFRange), "CFRange[]"); + i = 0; + j = 0; + while (i < n) { + CFRange range; + + range = CFStringGetRangeOfComposedCharactersAtIndex(backing, i); + i = range.location + range.length; + layout->charsToRanges[j] = range; + j++; + } + // and set the last one + layout->charsToRanges[j].location = i; + layout->charsToRanges[j].length = 0; + + return layout; +} + +void uiDrawFreeTextLayout(uiDrawTextLayout *layout) +{ + uiFree(layout->charsToRanges); + CFRelease(layout->mas); + uiFree(layout); +} + +void uiDrawTextLayoutSetWidth(uiDrawTextLayout *layout, double width) +{ + layout->width = width; +} + +struct framesetter { + CTFramesetterRef fs; + CFMutableDictionaryRef frameAttrib; + CGSize extents; +}; + +// TODO CTFrameProgression for RTL/LTR +// TODO kCTParagraphStyleSpecifierMaximumLineSpacing, kCTParagraphStyleSpecifierMinimumLineSpacing, kCTParagraphStyleSpecifierLineSpacingAdjustment for line spacing +static void mkFramesetter(uiDrawTextLayout *layout, struct framesetter *fs) +{ + CFRange fitRange; + CGFloat width; + + fs->fs = CTFramesetterCreateWithAttributedString(layout->mas); + if (fs->fs == NULL) + complain("error creating CTFramesetter object in mkFramesetter()"); + + // TODO kCTFramePathWidthAttributeName? + fs->frameAttrib = NULL; + + width = layout->width; + if (layout->width < 0) + width = CGFLOAT_MAX; + // TODO these seem to be floor()'d or truncated? + fs->extents = CTFramesetterSuggestFrameSizeWithConstraints(fs->fs, + CFRangeMake(0, 0), + fs->frameAttrib, + CGSizeMake(width, CGFLOAT_MAX), + &fitRange); // not documented as accepting NULL +} + +static void freeFramesetter(struct framesetter *fs) +{ + if (fs->frameAttrib != NULL) + CFRelease(fs->frameAttrib); + CFRelease(fs->fs); +} + +// LONGTERM allow line separation and leading to be factored into a wrapping text layout + +// TODO reconcile differences in character wrapping on platforms +void uiDrawTextLayoutExtents(uiDrawTextLayout *layout, double *width, double *height) +{ + struct framesetter fs; + + mkFramesetter(layout, &fs); + *width = fs.extents.width; + *height = fs.extents.height; + freeFramesetter(&fs); +} + +// Core Text doesn't draw onto a flipped view correctly; we have to do this +// see the iOS bits of the first example at https://developer.apple.com/library/mac/documentation/StringsTextFonts/Conceptual/CoreText_Programming/LayoutOperations/LayoutOperations.html#//apple_ref/doc/uid/TP40005533-CH12-SW1 (iOS is naturally flipped) +// TODO how is this affected by the CTM? +static void prepareContextForText(CGContextRef c, CGFloat cheight, double *y) +{ + CGContextSaveGState(c); + CGContextTranslateCTM(c, 0, cheight); + CGContextScaleCTM(c, 1.0, -1.0); + CGContextSetTextMatrix(c, CGAffineTransformIdentity); + + // wait, that's not enough; we need to offset y values to account for our new flipping + *y = cheight - *y; +} + +// TODO placement is incorrect for Helvetica +void doDrawText(CGContextRef c, CGFloat cheight, double x, double y, uiDrawTextLayout *layout) +{ + struct framesetter fs; + CGRect rect; + CGPathRef path; + CTFrameRef frame; + + prepareContextForText(c, cheight, &y); + mkFramesetter(layout, &fs); + + // oh, and since we're flipped, y is the bottom-left coordinate of the rectangle, not the top-left + // since we are flipped, we subtract + y -= fs.extents.height; + + rect.origin = CGPointMake(x, y); + rect.size = fs.extents; + path = CGPathCreateWithRect(rect, NULL); + + frame = CTFramesetterCreateFrame(fs.fs, + CFRangeMake(0, 0), + path, + fs.frameAttrib); + if (frame == NULL) + complain("error creating CTFrame object in doDrawText()"); + CTFrameDraw(frame, c); + CFRelease(frame); + + CFRelease(path); + + freeFramesetter(&fs); + CGContextRestoreGState(c); +} + +// LONGTERM provide an equivalent to CTLineGetTypographicBounds() on uiDrawTextLayout? + +// LONGTERM keep this for later features and documentation purposes +#if 0 + w = CTLineGetTypographicBounds(line, &ascent, &descent, NULL); + // though CTLineGetTypographicBounds() returns 0 on error, it also returns 0 on an empty string, so we can't reasonably check for error + CFRelease(line); + + // LONGTERM provide a way to get the image bounds as a separate function later + bounds = CTLineGetImageBounds(line, c); + // though CTLineGetImageBounds() returns CGRectNull on error, it also returns CGRectNull on an empty string, so we can't reasonably check for error + + // CGContextSetTextPosition() positions at the baseline in the case of CTLineDraw(); we need the top-left corner instead + CTLineGetTypographicBounds(line, &yoff, NULL, NULL); + // remember that we're flipped, so we subtract + y -= yoff; + CGContextSetTextPosition(c, x, y); +#endif + +static CFRange charsToRange(uiDrawTextLayout *layout, int startChar, int endChar) +{ + CFRange start, end; + CFRange out; + + start = layout->charsToRanges[startChar]; + end = layout->charsToRanges[endChar]; + out.location = start.location; + out.length = end.location - start.location; + return out; +} + +#define rangeToCFRange() charsToRange(layout, startChar, endChar) + +void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a) +{ + CGColorSpaceRef colorspace; + CGFloat components[4]; + CGColorRef color; + + // for consistency with windows, use sRGB + colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); + components[0] = r; + components[1] = g; + components[2] = b; + components[3] = a; + color = CGColorCreate(colorspace, components); + CGColorSpaceRelease(colorspace); + + CFAttributedStringSetAttribute(layout->mas, + rangeToCFRange(), + kCTForegroundColorAttributeName, + color); + CGColorRelease(color); // TODO safe? +} diff --git a/src/libui_sdl/libui/darwin/editablecombo.m b/src/libui_sdl/libui/darwin/editablecombo.m new file mode 100644 index 0000000..434add7 --- /dev/null +++ b/src/libui_sdl/libui/darwin/editablecombo.m @@ -0,0 +1,185 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +// So why did I split uiCombobox into uiCombobox and uiEditableCombobox? Here's (90% of the; the other 10% is GTK+ events) answer: +// When you type a value into a NSComboBox that just happens to be in the list, it will autoselect that item! +// I can't seem to find a workaround. +// Fortunately, there's other weird behaviors that made this split worth it. +// And besides, selected items make little sense with editable comboboxes... you either separate or combine them with the text entry :V + +// NSComboBoxes have no intrinsic width; we'll use the default Interface Builder width for them. +#define comboboxWidth 96 + +@interface libui_intrinsicWidthNSComboBox : NSComboBox +@end + +@implementation libui_intrinsicWidthNSComboBox + +- (NSSize)intrinsicContentSize +{ + NSSize s; + + s = [super intrinsicContentSize]; + s.width = comboboxWidth; + return s; +} + +@end + +struct uiEditableCombobox { + uiDarwinControl c; + NSComboBox *cb; + void (*onChanged)(uiEditableCombobox *, void *); + void *onChangedData; +}; + +@interface editableComboboxDelegateClass : NSObject<NSComboBoxDelegate> { + struct mapTable *comboboxes; +} +- (void)controlTextDidChange:(NSNotification *)note; +- (void)comboBoxSelectionDidChange:(NSNotification *)note; +- (void)registerCombobox:(uiEditableCombobox *)c; +- (void)unregisterCombobox:(uiEditableCombobox *)c; +@end + +@implementation editableComboboxDelegateClass + +- (id)init +{ + self = [super init]; + if (self) + self->comboboxes = newMap(); + return self; +} + +- (void)dealloc +{ + mapDestroy(self->comboboxes); + [super dealloc]; +} + +- (void)controlTextDidChange:(NSNotification *)note +{ + uiEditableCombobox *c; + + c = uiEditableCombobox(mapGet(self->comboboxes, [note object])); + (*(c->onChanged))(c, c->onChangedData); +} + +// the above doesn't handle when an item is selected; this will +- (void)comboBoxSelectionDidChange:(NSNotification *)note +{ + // except this is sent BEFORE the entry is changed, and that doesn't send the above, so + // this is via http://stackoverflow.com/a/21059819/3408572 - it avoids the need to manage selected items + // this still isn't perfect — I get residual changes to the same value while navigating the list — but it's good enough + [self performSelector:@selector(controlTextDidChange:) + withObject:note + afterDelay:0]; +} + +- (void)registerCombobox:(uiEditableCombobox *)c +{ + mapSet(self->comboboxes, c->cb, c); + [c->cb setDelegate:self]; +} + +- (void)unregisterCombobox:(uiEditableCombobox *)c +{ + [c->cb setDelegate:nil]; + mapDelete(self->comboboxes, c->cb); +} + +@end + +static editableComboboxDelegateClass *comboboxDelegate = nil; + +uiDarwinControlAllDefaultsExceptDestroy(uiEditableCombobox, cb) + +static void uiEditableComboboxDestroy(uiControl *cc) +{ + uiEditableCombobox *c = uiEditableCombobox(cc); + + [comboboxDelegate unregisterCombobox:c]; + [c->cb release]; + uiFreeControl(uiControl(c)); +} + +void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text) +{ + [c->cb addItemWithObjectValue:toNSString(text)]; +} + +char *uiEditableComboboxText(uiEditableCombobox *c) +{ + return uiDarwinNSStringToText([c->cb stringValue]); +} + +void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text) +{ + NSString *t; + + t = toNSString(text); + [c->cb setStringValue:t]; + // yes, let's imitate the behavior that caused uiEditableCombobox to be separate in the first place! + // just to avoid confusion when users see an option in the list in the text field but not selected in the list + [c->cb selectItemWithObjectValue:t]; +} + +#if 0 +// LONGTERM +void uiEditableComboboxSetSelected(uiEditableCombobox *c, int n) +{ + if (c->editable) { + // see https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ComboBox/Tasks/SettingComboBoxValue.html#//apple_ref/doc/uid/20000256 + id delegate; + + // this triggers the delegate; turn it off for now + delegate = [c->cb delegate]; + [c->cb setDelegate:nil]; + + // this seems to work fine for -1 too + [c->cb selectItemAtIndex:n]; + if (n == -1) + [c->cb setObjectValue:@""]; + else + [c->cb setObjectValue:[c->cb objectValueOfSelectedItem]]; + + [c->cb setDelegate:delegate]; + return; + } + [c->pb selectItemAtIndex:n]; +} +#endif + +void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data) +{ + c->onChanged = f; + c->onChangedData = data; +} + +static void defaultOnChanged(uiEditableCombobox *c, void *data) +{ + // do nothing +} + +uiEditableCombobox *uiNewEditableCombobox(void) +{ + uiEditableCombobox *c; + + uiDarwinNewControl(uiEditableCombobox, c); + + c->cb = [[libui_intrinsicWidthNSComboBox alloc] initWithFrame:NSZeroRect]; + [c->cb setUsesDataSource:NO]; + [c->cb setButtonBordered:YES]; + [c->cb setCompletes:NO]; + uiDarwinSetControlFont(c->cb, NSRegularControlSize); + + if (comboboxDelegate == nil) { + comboboxDelegate = [[editableComboboxDelegateClass new] autorelease]; + [delegates addObject:comboboxDelegate]; + } + [comboboxDelegate registerCombobox:c]; + uiEditableComboboxOnChanged(c, defaultOnChanged, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/darwin/entry.m b/src/libui_sdl/libui/darwin/entry.m new file mode 100644 index 0000000..219d080 --- /dev/null +++ b/src/libui_sdl/libui/darwin/entry.m @@ -0,0 +1,251 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +// Text fields for entering text have no intrinsic width; we'll use the default Interface Builder width for them. +#define textfieldWidth 96 + +@interface libui_intrinsicWidthNSTextField : NSTextField +@end + +@implementation libui_intrinsicWidthNSTextField + +- (NSSize)intrinsicContentSize +{ + NSSize s; + + s = [super intrinsicContentSize]; + s.width = textfieldWidth; + return s; +} + +@end + +// TODO does this have one on its own? +@interface libui_intrinsicWidthNSSecureTextField : NSSecureTextField +@end + +@implementation libui_intrinsicWidthNSSecureTextField + +- (NSSize)intrinsicContentSize +{ + NSSize s; + + s = [super intrinsicContentSize]; + s.width = textfieldWidth; + return s; +} + +@end + +// TODO does this have one on its own? +@interface libui_intrinsicWidthNSSearchField : NSSearchField +@end + +@implementation libui_intrinsicWidthNSSearchField + +- (NSSize)intrinsicContentSize +{ + NSSize s; + + s = [super intrinsicContentSize]; + s.width = textfieldWidth; + return s; +} + +@end + +struct uiEntry { + uiDarwinControl c; + NSTextField *textfield; + void (*onChanged)(uiEntry *, void *); + void *onChangedData; +}; + +static BOOL isSearchField(NSTextField *tf) +{ + return [tf isKindOfClass:[NSSearchField class]]; +} + +@interface entryDelegateClass : NSObject<NSTextFieldDelegate> { + struct mapTable *entries; +} +- (void)controlTextDidChange:(NSNotification *)note; +- (IBAction)onSearch:(id)sender; +- (void)registerEntry:(uiEntry *)e; +- (void)unregisterEntry:(uiEntry *)e; +@end + +@implementation entryDelegateClass + +- (id)init +{ + self = [super init]; + if (self) + self->entries = newMap(); + return self; +} + +- (void)dealloc +{ + mapDestroy(self->entries); + [super dealloc]; +} + +- (void)controlTextDidChange:(NSNotification *)note +{ + [self onSearch:[note object]]; +} + +- (IBAction)onSearch:(id)sender +{ + uiEntry *e; + + e = (uiEntry *) mapGet(self->entries, sender); + (*(e->onChanged))(e, e->onChangedData); +} + +- (void)registerEntry:(uiEntry *)e +{ + mapSet(self->entries, e->textfield, e); + if (isSearchField(e->textfield)) { + [e->textfield setTarget:self]; + [e->textfield setAction:@selector(onSearch:)]; + } else + [e->textfield setDelegate:self]; +} + +- (void)unregisterEntry:(uiEntry *)e +{ + if (isSearchField(e->textfield)) + [e->textfield setTarget:nil]; + else + [e->textfield setDelegate:nil]; + mapDelete(self->entries, e->textfield); +} + +@end + +static entryDelegateClass *entryDelegate = nil; + +uiDarwinControlAllDefaultsExceptDestroy(uiEntry, textfield) + +static void uiEntryDestroy(uiControl *c) +{ + uiEntry *e = uiEntry(c); + + [entryDelegate unregisterEntry:e]; + [e->textfield release]; + uiFreeControl(uiControl(e)); +} + +char *uiEntryText(uiEntry *e) +{ + return uiDarwinNSStringToText([e->textfield stringValue]); +} + +void uiEntrySetText(uiEntry *e, const char *text) +{ + [e->textfield setStringValue:toNSString(text)]; + // don't queue the control for resize; entry sizes are independent of their contents +} + +void uiEntryOnChanged(uiEntry *e, void (*f)(uiEntry *, void *), void *data) +{ + e->onChanged = f; + e->onChangedData = data; +} + +int uiEntryReadOnly(uiEntry *e) +{ + return [e->textfield isEditable] == NO; +} + +void uiEntrySetReadOnly(uiEntry *e, int readonly) +{ + BOOL editable; + + editable = YES; + if (readonly) + editable = NO; + [e->textfield setEditable:editable]; +} + +static void defaultOnChanged(uiEntry *e, void *data) +{ + // do nothing +} + +// these are based on interface builder defaults; my comments in the old code weren't very good so I don't really know what talked about what, sorry :/ +void finishNewTextField(NSTextField *t, BOOL isEntry) +{ + uiDarwinSetControlFont(t, NSRegularControlSize); + + // THE ORDER OF THESE CALLS IS IMPORTANT; CHANGE IT AND THE BORDERS WILL DISAPPEAR + [t setBordered:NO]; + [t setBezelStyle:NSTextFieldSquareBezel]; + [t setBezeled:isEntry]; + + // we don't need to worry about substitutions/autocorrect here; see window_darwin.m for details + + [[t cell] setLineBreakMode:NSLineBreakByClipping]; + [[t cell] setScrollable:YES]; +} + +static NSTextField *realNewEditableTextField(Class class) +{ + NSTextField *tf; + + tf = [[class alloc] initWithFrame:NSZeroRect]; + [tf setSelectable:YES]; // otherwise the setting is masked by the editable default of YES + finishNewTextField(tf, YES); + return tf; +} + +NSTextField *newEditableTextField(void) +{ + return realNewEditableTextField([libui_intrinsicWidthNSTextField class]); +} + +static uiEntry *finishNewEntry(Class class) +{ + uiEntry *e; + + uiDarwinNewControl(uiEntry, e); + + e->textfield = realNewEditableTextField(class); + + if (entryDelegate == nil) { + entryDelegate = [[entryDelegateClass new] autorelease]; + [delegates addObject:entryDelegate]; + } + [entryDelegate registerEntry:e]; + uiEntryOnChanged(e, defaultOnChanged, NULL); + + return e; +} + +uiEntry *uiNewEntry(void) +{ + return finishNewEntry([libui_intrinsicWidthNSTextField class]); +} + +uiEntry *uiNewPasswordEntry(void) +{ + return finishNewEntry([libui_intrinsicWidthNSSecureTextField class]); +} + +uiEntry *uiNewSearchEntry(void) +{ + uiEntry *e; + NSSearchField *s; + + e = finishNewEntry([libui_intrinsicWidthNSSearchField class]); + s = (NSSearchField *) (e->textfield); + // TODO these are only on 10.10 +// [s setSendsSearchStringImmediately:NO]; +// [s setSendsWholeSearchString:NO]; + [s setBordered:NO]; + [s setBezelStyle:NSTextFieldRoundedBezel]; + [s setBezeled:YES]; + return e; +} diff --git a/src/libui_sdl/libui/darwin/fontbutton.m b/src/libui_sdl/libui/darwin/fontbutton.m new file mode 100644 index 0000000..22bc646 --- /dev/null +++ b/src/libui_sdl/libui/darwin/fontbutton.m @@ -0,0 +1,218 @@ +// 14 april 2016 +#import "uipriv_darwin.h" + +@interface fontButton : NSButton { + uiFontButton *libui_b; + NSFont *libui_font; +} +- (id)initWithFrame:(NSRect)frame libuiFontButton:(uiFontButton *)b; +- (void)updateFontButtonLabel; +- (IBAction)fontButtonClicked:(id)sender; +- (void)activateFontButton; +- (void)deactivateFontButton:(BOOL)activatingAnother; +- (void)deactivateOnClose:(NSNotification *)note; +- (uiDrawTextFont *)libuiFont; +@end + +// only one may be active at one time +static fontButton *activeFontButton = nil; + +struct uiFontButton { + uiDarwinControl c; + fontButton *button; + void (*onChanged)(uiFontButton *, void *); + void *onChangedData; +}; + +@implementation fontButton + +- (id)initWithFrame:(NSRect)frame libuiFontButton:(uiFontButton *)b +{ + self = [super initWithFrame:frame]; + if (self) { + self->libui_b = b; + + // imitate a NSColorWell in appearance + [self setButtonType:NSPushOnPushOffButton]; + [self setBordered:YES]; + [self setBezelStyle:NSShadowlessSquareBezelStyle]; + + // default font values according to the CTFontDescriptor reference + // this is autoreleased (thanks swillits in irc.freenode.net/#macdev) + self->libui_font = [[NSFont fontWithName:@"Helvetica" size:12.0] retain]; + [self updateFontButtonLabel]; + + // for when clicked + [self setTarget:self]; + [self setAction:@selector(fontButtonClicked:)]; + } + return self; +} + +- (void)dealloc +{ + // clean up notifications + if (activeFontButton == self) + [self deactivateFontButton:NO]; + [self->libui_font release]; + [super dealloc]; +} + +- (void)updateFontButtonLabel +{ + NSString *title; + + title = [NSString stringWithFormat:@"%@ %g", + [self->libui_font displayName], + [self->libui_font pointSize]]; + [self setTitle:title]; +} + +- (IBAction)fontButtonClicked:(id)sender +{ + if ([self state] == NSOnState) + [self activateFontButton]; + else + [self deactivateFontButton:NO]; +} + +- (void)activateFontButton +{ + NSFontManager *sfm; + + sfm = [NSFontManager sharedFontManager]; + if (activeFontButton != nil) + [activeFontButton deactivateFontButton:YES]; + [sfm setTarget:self]; + [sfm setSelectedFont:self->libui_font isMultiple:NO]; + [sfm orderFrontFontPanel:self]; + activeFontButton = self; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(deactivateOnClose:) + name:NSWindowWillCloseNotification + object:[NSFontPanel sharedFontPanel]]; + [self setState:NSOnState]; +} + +- (void)deactivateFontButton:(BOOL)activatingAnother +{ + NSFontManager *sfm; + + sfm = [NSFontManager sharedFontManager]; + [sfm setTarget:nil]; + if (!activatingAnother) + [[NSFontPanel sharedFontPanel] orderOut:self]; + activeFontButton = nil; + [[NSNotificationCenter defaultCenter] removeObserver:self + name:NSWindowWillCloseNotification + object:[NSFontPanel sharedFontPanel]]; + [self setState:NSOffState]; +} + +- (void)deactivateOnClose:(NSNotification *)note +{ + [self deactivateFontButton:NO]; +} + +- (void)changeFont:(id)sender +{ + NSFontManager *fm; + NSFont *old; + uiFontButton *b = self->libui_b; + + fm = (NSFontManager *) sender; + old = self->libui_font; + self->libui_font = [sender convertFont:self->libui_font]; + // do this even if it returns the same; we don't own anything that isn't from a new or alloc/init + [self->libui_font retain]; + // do this second just in case + [old release]; + [self updateFontButtonLabel]; + (*(b->onChanged))(b, b->onChangedData); +} + +- (NSUInteger)validModesForFontPanel:(NSFontPanel *)panel +{ + return NSFontPanelFaceModeMask | + NSFontPanelSizeModeMask | + NSFontPanelCollectionModeMask; +} + +- (uiDrawTextFont *)libuiFont +{ + return mkTextFontFromNSFont(self->libui_font); +} + +@end + +uiDarwinControlAllDefaults(uiFontButton, button) + +// we do not want font change events to be sent to any controls other than the font buttons +// see main.m for more details +BOOL fontButtonInhibitSendAction(SEL sel, id from, id to) +{ + if (sel != @selector(changeFont:)) + return NO; + return ![to isKindOfClass:[fontButton class]]; +} + +// we do not want NSFontPanelValidation messages to be sent to any controls other than the font buttons when a font button is active +// see main.m for more details +BOOL fontButtonOverrideTargetForAction(SEL sel, id from, id to, id *override) +{ + if (activeFontButton == nil) + return NO; + if (sel != @selector(validModesForFontPanel:)) + return NO; + *override = activeFontButton; + return YES; +} + +// we also don't want the panel to be usable when there's a dialog running; see stddialogs.m for more details on that +// unfortunately the panel seems to ignore -setWorksWhenModal: so we'll have to do things ourselves +@interface nonModalFontPanel : NSFontPanel +@end + +@implementation nonModalFontPanel + +- (BOOL)worksWhenModal +{ + return NO; +} + +@end + +void setupFontPanel(void) +{ + [NSFontManager setFontPanelFactory:[nonModalFontPanel class]]; +} + +static void defaultOnChanged(uiFontButton *b, void *data) +{ + // do nothing +} + +uiDrawTextFont *uiFontButtonFont(uiFontButton *b) +{ + return [b->button libuiFont]; +} + +void uiFontButtonOnChanged(uiFontButton *b, void (*f)(uiFontButton *, void *), void *data) +{ + b->onChanged = f; + b->onChangedData = data; +} + +uiFontButton *uiNewFontButton(void) +{ + uiFontButton *b; + + uiDarwinNewControl(uiFontButton, b); + + b->button = [[fontButton alloc] initWithFrame:NSZeroRect libuiFontButton:b]; + uiDarwinSetControlFont(b->button, NSRegularControlSize); + + uiFontButtonOnChanged(b, defaultOnChanged, NULL); + + return b; +} diff --git a/src/libui_sdl/libui/darwin/form.m b/src/libui_sdl/libui/darwin/form.m new file mode 100644 index 0000000..7cdb965 --- /dev/null +++ b/src/libui_sdl/libui/darwin/form.m @@ -0,0 +1,561 @@ +// 7 june 2016 +#import "uipriv_darwin.h" + +// TODO in the test program, sometimes one of the radio buttons can disappear (try when spaced) + +@interface formChild : NSView +@property uiControl *c; +@property (strong) NSTextField *label; +@property BOOL stretchy; +@property NSLayoutPriority oldHorzHuggingPri; +@property NSLayoutPriority oldVertHuggingPri; +@property (strong) NSLayoutConstraint *baseline; +@property (strong) NSLayoutConstraint *leading; +@property (strong) NSLayoutConstraint *top; +@property (strong) NSLayoutConstraint *trailing; +@property (strong) NSLayoutConstraint *bottom; +- (id)initWithLabel:(NSTextField *)l; +- (void)onDestroy; +- (NSView *)view; +@end + +@interface formView : NSView { + uiForm *f; + NSMutableArray *children; + int padded; + + NSLayoutConstraint *first; + NSMutableArray *inBetweens; + NSLayoutConstraint *last; + NSMutableArray *widths; + NSMutableArray *leadings; + NSMutableArray *middles; + NSMutableArray *trailings; +} +- (id)initWithF:(uiForm *)ff; +- (void)onDestroy; +- (void)removeOurConstraints; +- (void)syncEnableStates:(int)enabled; +- (CGFloat)paddingAmount; +- (void)establishOurConstraints; +- (void)append:(NSString *)label c:(uiControl *)c stretchy:(int)stretchy; +- (void)delete:(int)n; +- (int)isPadded; +- (void)setPadded:(int)p; +- (BOOL)hugsTrailing; +- (BOOL)hugsBottom; +- (int)nStretchy; +@end + +struct uiForm { + uiDarwinControl c; + formView *view; +}; + +@implementation formChild + +- (id)initWithLabel:(NSTextField *)l +{ + self = [super initWithFrame:NSZeroRect]; + if (self) { + self.label = l; + [self.label setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self.label setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + [self.label setContentHuggingPriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [self.label setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationHorizontal]; + [self.label setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintOrientationVertical]; + [self addSubview:self.label]; + + self.leading = mkConstraint(self.label, NSLayoutAttributeLeading, + NSLayoutRelationGreaterThanOrEqual, + self, NSLayoutAttributeLeading, + 1, 0, + @"uiForm label leading"); + [self addConstraint:self.leading]; + self.top = mkConstraint(self.label, NSLayoutAttributeTop, + NSLayoutRelationEqual, + self, NSLayoutAttributeTop, + 1, 0, + @"uiForm label top"); + [self addConstraint:self.top]; + self.trailing = mkConstraint(self.label, NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + self, NSLayoutAttributeTrailing, + 1, 0, + @"uiForm label trailing"); + [self addConstraint:self.trailing]; + self.bottom = mkConstraint(self.label, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + self, NSLayoutAttributeBottom, + 1, 0, + @"uiForm label bottom"); + [self addConstraint:self.bottom]; + } + return self; +} + +- (void)onDestroy +{ + [self removeConstraint:self.trailing]; + self.trailing = nil; + [self removeConstraint:self.top]; + self.top = nil; + [self removeConstraint:self.bottom]; + self.bottom = nil; + + [self.label removeFromSuperview]; + self.label = nil; +} + +- (NSView *)view +{ + return (NSView *) uiControlHandle(self.c); +} + +@end + +@implementation formView + +- (id)initWithF:(uiForm *)ff +{ + self = [super initWithFrame:NSZeroRect]; + if (self != nil) { + self->f = ff; + self->padded = 0; + self->children = [NSMutableArray new]; + + self->inBetweens = [NSMutableArray new]; + self->widths = [NSMutableArray new]; + self->leadings = [NSMutableArray new]; + self->middles = [NSMutableArray new]; + self->trailings = [NSMutableArray new]; + } + return self; +} + +- (void)onDestroy +{ + formChild *fc; + + [self removeOurConstraints]; + [self->inBetweens release]; + [self->widths release]; + [self->leadings release]; + [self->middles release]; + [self->trailings release]; + + for (fc in self->children) { + [self removeConstraint:fc.baseline]; + fc.baseline = nil; + uiControlSetParent(fc.c, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(fc.c), nil); + uiControlDestroy(fc.c); + [fc onDestroy]; + [fc removeFromSuperview]; + } + [self->children release]; +} + +- (void)removeOurConstraints +{ + if (self->first != nil) { + [self removeConstraint:self->first]; + [self->first release]; + self->first = nil; + } + if ([self->inBetweens count] != 0) { + [self removeConstraints:self->inBetweens]; + [self->inBetweens removeAllObjects]; + } + if (self->last != nil) { + [self removeConstraint:self->last]; + [self->last release]; + self->last = nil; + } + if ([self->widths count] != 0) { + [self removeConstraints:self->widths]; + [self->widths removeAllObjects]; + } + if ([self->leadings count] != 0) { + [self removeConstraints:self->leadings]; + [self->leadings removeAllObjects]; + } + if ([self->middles count] != 0) { + [self removeConstraints:self->middles]; + [self->middles removeAllObjects]; + } + if ([self->trailings count] != 0) { + [self removeConstraints:self->trailings]; + [self->trailings removeAllObjects]; + } +} + +- (void)syncEnableStates:(int)enabled +{ + formChild *fc; + + for (fc in self->children) + uiDarwinControlSyncEnableState(uiDarwinControl(fc.c), enabled); +} + +- (CGFloat)paddingAmount +{ + if (!self->padded) + return 0.0; + return uiDarwinPaddingAmount(NULL); +} + +- (void)establishOurConstraints +{ + formChild *fc; + CGFloat padding; + NSView *prev, *prevlabel; + NSLayoutConstraint *c; + + [self removeOurConstraints]; + if ([self->children count] == 0) + return; + padding = [self paddingAmount]; + + // first arrange the children vertically and make them the same width + prev = nil; + for (fc in self->children) { + [fc setHidden:!uiControlVisible(fc.c)]; + if (!uiControlVisible(fc.c)) + continue; + if (prev == nil) { // first view + self->first = mkConstraint(self, NSLayoutAttributeTop, + NSLayoutRelationEqual, + [fc view], NSLayoutAttributeTop, + 1, 0, + @"uiForm first vertical constraint"); + [self addConstraint:self->first]; + [self->first retain]; + prev = [fc view]; + prevlabel = fc; + continue; + } + // not the first; link it + c = mkConstraint(prev, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + [fc view], NSLayoutAttributeTop, + 1, -padding, + @"uiForm in-between vertical constraint"); + [self addConstraint:c]; + [self->inBetweens addObject:c]; + // and make the same width + c = mkConstraint(prev, NSLayoutAttributeWidth, + NSLayoutRelationEqual, + [fc view], NSLayoutAttributeWidth, + 1, 0, + @"uiForm control width constraint"); + [self addConstraint:c]; + [self->widths addObject:c]; + c = mkConstraint(prevlabel, NSLayoutAttributeWidth, + NSLayoutRelationEqual, + fc, NSLayoutAttributeWidth, + 1, 0, + @"uiForm label lwidth constraint"); + [self addConstraint:c]; + [self->widths addObject:c]; + prev = [fc view]; + prevlabel = fc; + } + if (prev == nil) // all hidden; act as if nothing there + return; + self->last = mkConstraint(prev, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + self, NSLayoutAttributeBottom, + 1, 0, + @"uiForm last vertical constraint"); + [self addConstraint:self->last]; + [self->last retain]; + + // now arrange the controls horizontally + for (fc in self->children) { + if (!uiControlVisible(fc.c)) + continue; + c = mkConstraint(self, NSLayoutAttributeLeading, + NSLayoutRelationEqual, + fc, NSLayoutAttributeLeading, + 1, 0, + @"uiForm leading constraint"); + [self addConstraint:c]; + [self->leadings addObject:c]; + // coerce the control to be as wide as possible + // see http://stackoverflow.com/questions/37710892/in-auto-layout-i-set-up-labels-that-shouldnt-grow-horizontally-and-controls-th + c = mkConstraint(self, NSLayoutAttributeLeading, + NSLayoutRelationEqual, + [fc view], NSLayoutAttributeLeading, + 1, 0, + @"uiForm leading constraint"); + [c setPriority:NSLayoutPriorityDefaultHigh]; + [self addConstraint:c]; + [self->leadings addObject:c]; + c = mkConstraint(fc, NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + [fc view], NSLayoutAttributeLeading, + 1, -padding, + @"uiForm middle constraint"); + [self addConstraint:c]; + [self->middles addObject:c]; + c = mkConstraint([fc view], NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + self, NSLayoutAttributeTrailing, + 1, 0, + @"uiForm trailing constraint"); + [self addConstraint:c]; + [self->trailings addObject:c]; + // TODO + c = mkConstraint(fc, NSLayoutAttributeBottom, + NSLayoutRelationLessThanOrEqual, + self, NSLayoutAttributeBottom, + 1, 0, + @"TODO"); + [self addConstraint:c]; + [self->trailings addObject:c]; + } + + // and make all stretchy controls have the same height + prev = nil; + for (fc in self->children) { + if (!uiControlVisible(fc.c)) + continue; + if (!fc.stretchy) + continue; + if (prev == nil) { + prev = [fc view]; + continue; + } + c = mkConstraint([fc view], NSLayoutAttributeHeight, + NSLayoutRelationEqual, + prev, NSLayoutAttributeHeight, + 1, 0, + @"uiForm stretchy constraint"); + [self addConstraint:c]; + // TODO make a dedicated array for this + [self->leadings addObject:c]; + } + + // we don't arrange the labels vertically; that's done when we add the control since those constraints don't need to change (they just need to be at their baseline) +} + +- (void)append:(NSString *)label c:(uiControl *)c stretchy:(int)stretchy +{ + formChild *fc; + NSLayoutPriority priority; + NSLayoutAttribute attribute; + int oldnStretchy; + + fc = [[formChild alloc] initWithLabel:newLabel(label)]; + fc.c = c; + fc.stretchy = stretchy; + fc.oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(fc.c), NSLayoutConstraintOrientationHorizontal); + fc.oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(fc.c), NSLayoutConstraintOrientationVertical); + [fc setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self addSubview:fc]; + + uiControlSetParent(fc.c, uiControl(self->f)); + uiDarwinControlSetSuperview(uiDarwinControl(fc.c), self); + uiDarwinControlSyncEnableState(uiDarwinControl(fc.c), uiControlEnabledToUser(uiControl(self->f))); + + // if a control is stretchy, it should not hug vertically + // otherwise, it should *forcibly* hug + if (fc.stretchy) + priority = NSLayoutPriorityDefaultLow; + else + // LONGTERM will default high work? + priority = NSLayoutPriorityRequired; + uiDarwinControlSetHuggingPriority(uiDarwinControl(fc.c), priority, NSLayoutConstraintOrientationVertical); + // make sure controls don't hug their horizontal direction so they fill the width of the view + uiDarwinControlSetHuggingPriority(uiDarwinControl(fc.c), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationHorizontal); + + // and constrain the baselines to position the label vertically + // if the view is a scroll view, align tops, not baselines + // this is what Interface Builder does + attribute = NSLayoutAttributeBaseline; + if ([[fc view] isKindOfClass:[NSScrollView class]]) + attribute = NSLayoutAttributeTop; + fc.baseline = mkConstraint(fc.label, attribute, + NSLayoutRelationEqual, + [fc view], attribute, + 1, 0, + @"uiForm baseline constraint"); + [self addConstraint:fc.baseline]; + + oldnStretchy = [self nStretchy]; + [self->children addObject:fc]; + + [self establishOurConstraints]; + if (fc.stretchy) + if (oldnStretchy == 0) + uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->f)); + + [fc release]; // we don't need the initial reference now +} + +- (void)delete:(int)n +{ + formChild *fc; + int stretchy; + + fc = (formChild *) [self->children objectAtIndex:n]; + stretchy = fc.stretchy; + + uiControlSetParent(fc.c, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(fc.c), nil); + + uiDarwinControlSetHuggingPriority(uiDarwinControl(fc.c), fc.oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal); + uiDarwinControlSetHuggingPriority(uiDarwinControl(fc.c), fc.oldVertHuggingPri, NSLayoutConstraintOrientationVertical); + + [fc onDestroy]; + [self->children removeObjectAtIndex:n]; + + [self establishOurConstraints]; + if (stretchy) + if ([self nStretchy] == 0) + uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->f)); +} + +- (int)isPadded +{ + return self->padded; +} + +- (void)setPadded:(int)p +{ + CGFloat padding; + NSLayoutConstraint *c; + + self->padded = p; + padding = [self paddingAmount]; + for (c in self->inBetweens) + [c setConstant:-padding]; + for (c in self->middles) + [c setConstant:-padding]; +} + +- (BOOL)hugsTrailing +{ + return YES; // always hug trailing +} + +- (BOOL)hugsBottom +{ + // only hug if we have stretchy + return [self nStretchy] != 0; +} + +- (int)nStretchy +{ + formChild *fc; + int n; + + n = 0; + for (fc in self->children) { + if (!uiControlVisible(fc.c)) + continue; + if (fc.stretchy) + n++; + } + return n; +} + +@end + +static void uiFormDestroy(uiControl *c) +{ + uiForm *f = uiForm(c); + + [f->view onDestroy]; + [f->view release]; + uiFreeControl(uiControl(f)); +} + +uiDarwinControlDefaultHandle(uiForm, view) +uiDarwinControlDefaultParent(uiForm, view) +uiDarwinControlDefaultSetParent(uiForm, view) +uiDarwinControlDefaultToplevel(uiForm, view) +uiDarwinControlDefaultVisible(uiForm, view) +uiDarwinControlDefaultShow(uiForm, view) +uiDarwinControlDefaultHide(uiForm, view) +uiDarwinControlDefaultEnabled(uiForm, view) +uiDarwinControlDefaultEnable(uiForm, view) +uiDarwinControlDefaultDisable(uiForm, view) + +static void uiFormSyncEnableState(uiDarwinControl *c, int enabled) +{ + uiForm *f = uiForm(c); + + if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(f), enabled)) + return; + [f->view syncEnableStates:enabled]; +} + +uiDarwinControlDefaultSetSuperview(uiForm, view) + +static BOOL uiFormHugsTrailingEdge(uiDarwinControl *c) +{ + uiForm *f = uiForm(c); + + return [f->view hugsTrailing]; +} + +static BOOL uiFormHugsBottom(uiDarwinControl *c) +{ + uiForm *f = uiForm(c); + + return [f->view hugsBottom]; +} + +static void uiFormChildEdgeHuggingChanged(uiDarwinControl *c) +{ + uiForm *f = uiForm(c); + + [f->view establishOurConstraints]; +} + +uiDarwinControlDefaultHuggingPriority(uiForm, view) +uiDarwinControlDefaultSetHuggingPriority(uiForm, view) + +static void uiFormChildVisibilityChanged(uiDarwinControl *c) +{ + uiForm *f = uiForm(c); + + [f->view establishOurConstraints]; +} + +void uiFormAppend(uiForm *f, const char *label, uiControl *c, int stretchy) +{ + // LONGTERM on other platforms + // or at leat allow this and implicitly turn it into a spacer + if (c == NULL) + userbug("You cannot add NULL to a uiForm."); + [f->view append:toNSString(label) c:c stretchy:stretchy]; +} + +void uiFormDelete(uiForm *f, int n) +{ + [f->view delete:n]; +} + +int uiFormPadded(uiForm *f) +{ + return [f->view isPadded]; +} + +void uiFormSetPadded(uiForm *f, int padded) +{ + [f->view setPadded:padded]; +} + +uiForm *uiNewForm(void) +{ + uiForm *f; + + uiDarwinNewControl(uiForm, f); + + f->view = [[formView alloc] initWithF:f]; + + return f; +} diff --git a/src/libui_sdl/libui/darwin/grid.m b/src/libui_sdl/libui/darwin/grid.m new file mode 100644 index 0000000..d5c5fb1 --- /dev/null +++ b/src/libui_sdl/libui/darwin/grid.m @@ -0,0 +1,800 @@ +// 11 june 2016 +#import "uipriv_darwin.h" + +// TODO the assorted test doesn't work right at all + +@interface gridChild : NSView +@property uiControl *c; +@property int left; +@property int top; +@property int xspan; +@property int yspan; +@property int hexpand; +@property uiAlign halign; +@property int vexpand; +@property uiAlign valign; + +@property (strong) NSLayoutConstraint *leadingc; +@property (strong) NSLayoutConstraint *topc; +@property (strong) NSLayoutConstraint *trailingc; +@property (strong) NSLayoutConstraint *bottomc; +@property (strong) NSLayoutConstraint *xcenterc; +@property (strong) NSLayoutConstraint *ycenterc; + +@property NSLayoutPriority oldHorzHuggingPri; +@property NSLayoutPriority oldVertHuggingPri; +- (void)setC:(uiControl *)c grid:(uiGrid *)g; +- (void)onDestroy; +- (NSView *)view; +@end + +@interface gridView : NSView { + uiGrid *g; + NSMutableArray *children; + int padded; + + NSMutableArray *edges; + NSMutableArray *inBetweens; + + NSMutableArray *emptyCellViews; +} +- (id)initWithG:(uiGrid *)gg; +- (void)onDestroy; +- (void)removeOurConstraints; +- (void)syncEnableStates:(int)enabled; +- (CGFloat)paddingAmount; +- (void)establishOurConstraints; +- (void)append:(gridChild *)gc; +- (void)insert:(gridChild *)gc after:(uiControl *)c at:(uiAt)at; +- (int)isPadded; +- (void)setPadded:(int)p; +- (BOOL)hugsTrailing; +- (BOOL)hugsBottom; +- (int)nhexpand; +- (int)nvexpand; +@end + +struct uiGrid { + uiDarwinControl c; + gridView *view; +}; + +@implementation gridChild + +- (void)setC:(uiControl *)c grid:(uiGrid *)g +{ + self.c = c; + self.oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(self.c), NSLayoutConstraintOrientationHorizontal); + self.oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(self.c), NSLayoutConstraintOrientationVertical); + + uiControlSetParent(self.c, uiControl(g)); + uiDarwinControlSetSuperview(uiDarwinControl(self.c), self); + uiDarwinControlSyncEnableState(uiDarwinControl(self.c), uiControlEnabledToUser(uiControl(g))); + + if (self.halign == uiAlignStart || self.halign == uiAlignFill) { + self.leadingc = mkConstraint(self, NSLayoutAttributeLeading, + NSLayoutRelationEqual, + [self view], NSLayoutAttributeLeading, + 1, 0, + @"uiGrid child horizontal alignment start constraint"); + [self addConstraint:self.leadingc]; + } + if (self.halign == uiAlignCenter) { + self.xcenterc = mkConstraint(self, NSLayoutAttributeCenterX, + NSLayoutRelationEqual, + [self view], NSLayoutAttributeCenterX, + 1, 0, + @"uiGrid child horizontal alignment center constraint"); + [self addConstraint:self.xcenterc]; + } + if (self.halign == uiAlignEnd || self.halign == uiAlignFill) { + self.trailingc = mkConstraint(self, NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + [self view], NSLayoutAttributeTrailing, + 1, 0, + @"uiGrid child horizontal alignment end constraint"); + [self addConstraint:self.trailingc]; + } + + if (self.valign == uiAlignStart || self.valign == uiAlignFill) { + self.topc = mkConstraint(self, NSLayoutAttributeTop, + NSLayoutRelationEqual, + [self view], NSLayoutAttributeTop, + 1, 0, + @"uiGrid child vertical alignment start constraint"); + [self addConstraint:self.topc]; + } + if (self.valign == uiAlignCenter) { + self.ycenterc = mkConstraint(self, NSLayoutAttributeCenterY, + NSLayoutRelationEqual, + [self view], NSLayoutAttributeCenterY, + 1, 0, + @"uiGrid child vertical alignment center constraint"); + [self addConstraint:self.ycenterc]; + } + if (self.valign == uiAlignEnd || self.valign == uiAlignFill) { + self.bottomc = mkConstraint(self, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + [self view], NSLayoutAttributeBottom, + 1, 0, + @"uiGrid child vertical alignment end constraint"); + [self addConstraint:self.bottomc]; + } +} + +- (void)onDestroy +{ + if (self.leadingc != nil) { + [self removeConstraint:self.leadingc]; + self.leadingc = nil; + } + if (self.topc != nil) { + [self removeConstraint:self.topc]; + self.topc = nil; + } + if (self.trailingc != nil) { + [self removeConstraint:self.trailingc]; + self.trailingc = nil; + } + if (self.bottomc != nil) { + [self removeConstraint:self.bottomc]; + self.bottomc = nil; + } + if (self.xcenterc != nil) { + [self removeConstraint:self.xcenterc]; + self.xcenterc = nil; + } + if (self.ycenterc != nil) { + [self removeConstraint:self.ycenterc]; + self.ycenterc = nil; + } + + uiControlSetParent(self.c, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(self.c), nil); + uiDarwinControlSetHuggingPriority(uiDarwinControl(self.c), self.oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal); + uiDarwinControlSetHuggingPriority(uiDarwinControl(self.c), self.oldVertHuggingPri, NSLayoutConstraintOrientationVertical); +} + +- (NSView *)view +{ + return (NSView *) uiControlHandle(self.c); +} + +@end + +@implementation gridView + +- (id)initWithG:(uiGrid *)gg +{ + self = [super initWithFrame:NSZeroRect]; + if (self != nil) { + self->g = gg; + self->padded = 0; + self->children = [NSMutableArray new]; + + self->edges = [NSMutableArray new]; + self->inBetweens = [NSMutableArray new]; + + self->emptyCellViews = [NSMutableArray new]; + } + return self; +} + +- (void)onDestroy +{ + gridChild *gc; + + [self removeOurConstraints]; + [self->edges release]; + [self->inBetweens release]; + + [self->emptyCellViews release]; + + for (gc in self->children) { + [gc onDestroy]; + uiControlDestroy(gc.c); + [gc removeFromSuperview]; + } + [self->children release]; +} + +- (void)removeOurConstraints +{ + NSView *v; + + if ([self->edges count] != 0) { + [self removeConstraints:self->edges]; + [self->edges removeAllObjects]; + } + if ([self->inBetweens count] != 0) { + [self removeConstraints:self->inBetweens]; + [self->inBetweens removeAllObjects]; + } + + for (v in self->emptyCellViews) + [v removeFromSuperview]; + [self->emptyCellViews removeAllObjects]; +} + +- (void)syncEnableStates:(int)enabled +{ + gridChild *gc; + + for (gc in self->children) + uiDarwinControlSyncEnableState(uiDarwinControl(gc.c), enabled); +} + +- (CGFloat)paddingAmount +{ + if (!self->padded) + return 0.0; + return uiDarwinPaddingAmount(NULL); +} + +// LONGTERM stop early if all controls are hidden +- (void)establishOurConstraints +{ + gridChild *gc; + CGFloat padding; + int xmin, ymin; + int xmax, ymax; + int xcount, ycount; + BOOL first; + int **gg; + NSView ***gv; + BOOL **gspan; + int x, y; + int i; + NSLayoutConstraint *c; + int firstx, firsty; + BOOL *hexpand, *vexpand; + BOOL doit; + BOOL onlyEmptyAndSpanning; + + [self removeOurConstraints]; + if ([self->children count] == 0) + return; + padding = [self paddingAmount]; + + // first, figure out the minimum and maximum row and column numbers + // ignore hidden controls + first = YES; + for (gc in self->children) { + // this bit is important: it ensures row ymin and column xmin have at least one cell to draw, so the onlyEmptyAndSpanning logic below will never run on those rows + if (!uiControlVisible(gc.c)) + continue; + if (first) { + xmin = gc.left; + ymin = gc.top; + xmax = gc.left + gc.xspan; + ymax = gc.top + gc.yspan; + first = NO; + continue; + } + if (xmin > gc.left) + xmin = gc.left; + if (ymin > gc.top) + ymin = gc.top; + if (xmax < (gc.left + gc.xspan)) + xmax = gc.left + gc.xspan; + if (ymax < (gc.top + gc.yspan)) + ymax = gc.top + gc.yspan; + } + if (first != NO) // the entire grid is hidden; do nothing + return; + xcount = xmax - xmin; + ycount = ymax - ymin; + + // now build a topological map of the grid gg[y][x] + // also figure out which cells contain spanned views so they can be ignored later + // treat hidden controls by keeping the indices -1 + gg = (int **) uiAlloc(ycount * sizeof (int *), "int[][]"); + gspan = (BOOL **) uiAlloc(ycount * sizeof (BOOL *), "BOOL[][]"); + for (y = 0; y < ycount; y++) { + gg[y] = (int *) uiAlloc(xcount * sizeof (int), "int[]"); + gspan[y] = (BOOL *) uiAlloc(xcount * sizeof (BOOL), "BOOL[]"); + for (x = 0; x < xcount; x++) + gg[y][x] = -1; // empty + } + for (i = 0; i < [self->children count]; i++) { + gc = (gridChild *) [self->children objectAtIndex:i]; + if (!uiControlVisible(gc.c)) + continue; + for (y = gc.top; y < gc.top + gc.yspan; y++) + for (x = gc.left; x < gc.left + gc.xspan; x++) { + gg[y - ymin][x - xmin] = i; + if (x != gc.left || y != gc.top) + gspan[y - ymin][x - xmin] = YES; + } + } + + // if a row or column only contains emptys and spanning cells of a opposite-direction spannings, remove it by duplicating the previous row or column + for (y = 0; y < ycount; y++) { + onlyEmptyAndSpanning = YES; + for (x = 0; x < xcount; x++) + if (gg[y][x] != -1) { + gc = (gridChild *) [self->children objectAtIndex:gg[y][x]]; + if (gc.yspan == 1 || gc.top - ymin == y) { + onlyEmptyAndSpanning = NO; + break; + } + } + if (onlyEmptyAndSpanning) + for (x = 0; x < xcount; x++) { + gg[y][x] = gg[y - 1][x]; + gspan[y][x] = YES; + } + } + for (x = 0; x < xcount; x++) { + onlyEmptyAndSpanning = YES; + for (y = 0; y < ycount; y++) + if (gg[y][x] != -1) { + gc = (gridChild *) [self->children objectAtIndex:gg[y][x]]; + if (gc.xspan == 1 || gc.left - xmin == x) { + onlyEmptyAndSpanning = NO; + break; + } + } + if (onlyEmptyAndSpanning) + for (y = 0; y < ycount; y++) { + gg[y][x] = gg[y][x - 1]; + gspan[y][x] = YES; + } + } + + // now build a topological map of the grid's views gv[y][x] + // for any empty cell, create a dummy view + gv = (NSView ***) uiAlloc(ycount * sizeof (NSView **), "NSView *[][]"); + for (y = 0; y < ycount; y++) { + gv[y] = (NSView **) uiAlloc(xcount * sizeof (NSView *), "NSView *[]"); + for (x = 0; x < xcount; x++) + if (gg[y][x] == -1) { + gv[y][x] = [[NSView alloc] initWithFrame:NSZeroRect]; + [gv[y][x] setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self addSubview:gv[y][x]]; + [self->emptyCellViews addObject:gv[y][x]]; + } else { + gc = (gridChild *) [self->children objectAtIndex:gg[y][x]]; + gv[y][x] = gc; + } + } + + // now figure out which rows and columns really expand + hexpand = (BOOL *) uiAlloc(xcount * sizeof (BOOL), "BOOL[]"); + vexpand = (BOOL *) uiAlloc(ycount * sizeof (BOOL), "BOOL[]"); + // first, which don't span + for (gc in self->children) { + if (!uiControlVisible(gc.c)) + continue; + if (gc.hexpand && gc.xspan == 1) + hexpand[gc.left - xmin] = YES; + if (gc.vexpand && gc.yspan == 1) + vexpand[gc.top - ymin] = YES; + } + // second, which do span + // the way we handle this is simple: if none of the spanned rows/columns expand, make all rows/columns expand + for (gc in self->children) { + if (!uiControlVisible(gc.c)) + continue; + if (gc.hexpand && gc.xspan != 1) { + doit = YES; + for (x = gc.left; x < gc.left + gc.xspan; x++) + if (hexpand[x - xmin]) { + doit = NO; + break; + } + if (doit) + for (x = gc.left; x < gc.left + gc.xspan; x++) + hexpand[x - xmin] = YES; + } + if (gc.vexpand && gc.yspan != 1) { + doit = YES; + for (y = gc.top; y < gc.top + gc.yspan; y++) + if (vexpand[y - ymin]) { + doit = NO; + break; + } + if (doit) + for (y = gc.top; y < gc.top + gc.yspan; y++) + vexpand[y - ymin] = YES; + } + } + + // now establish all the edge constraints + // leading and trailing edges + for (y = 0; y < ycount; y++) { + c = mkConstraint(self, NSLayoutAttributeLeading, + NSLayoutRelationEqual, + gv[y][0], NSLayoutAttributeLeading, + 1, 0, + @"uiGrid leading edge constraint"); + [self addConstraint:c]; + [self->edges addObject:c]; + c = mkConstraint(self, NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + gv[y][xcount - 1], NSLayoutAttributeTrailing, + 1, 0, + @"uiGrid trailing edge constraint"); + [self addConstraint:c]; + [self->edges addObject:c]; + } + // top and bottom edges + for (x = 0; x < xcount; x++) { + c = mkConstraint(self, NSLayoutAttributeTop, + NSLayoutRelationEqual, + gv[0][x], NSLayoutAttributeTop, + 1, 0, + @"uiGrid top edge constraint"); + [self addConstraint:c]; + [self->edges addObject:c]; + c = mkConstraint(self, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + gv[ycount - 1][x], NSLayoutAttributeBottom, + 1, 0, + @"uiGrid bottom edge constraint"); + [self addConstraint:c]; + [self->edges addObject:c]; + } + + // now align leading and top edges + // do NOT align spanning cells! + for (x = 0; x < xcount; x++) { + for (y = 0; y < ycount; y++) + if (!gspan[y][x]) + break; + firsty = y; + for (y++; y < ycount; y++) { + if (gspan[y][x]) + continue; + c = mkConstraint(gv[firsty][x], NSLayoutAttributeLeading, + NSLayoutRelationEqual, + gv[y][x], NSLayoutAttributeLeading, + 1, 0, + @"uiGrid column leading constraint"); + [self addConstraint:c]; + [self->edges addObject:c]; + } + } + for (y = 0; y < ycount; y++) { + for (x = 0; x < xcount; x++) + if (!gspan[y][x]) + break; + firstx = x; + for (x++; x < xcount; x++) { + if (gspan[y][x]) + continue; + c = mkConstraint(gv[y][firstx], NSLayoutAttributeTop, + NSLayoutRelationEqual, + gv[y][x], NSLayoutAttributeTop, + 1, 0, + @"uiGrid row top constraint"); + [self addConstraint:c]; + [self->edges addObject:c]; + } + } + + // now string adjacent views together + for (y = 0; y < ycount; y++) + for (x = 1; x < xcount; x++) + if (gv[y][x - 1] != gv[y][x]) { + c = mkConstraint(gv[y][x - 1], NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + gv[y][x], NSLayoutAttributeLeading, + 1, -padding, + @"uiGrid internal horizontal constraint"); + [self addConstraint:c]; + [self->inBetweens addObject:c]; + } + for (x = 0; x < xcount; x++) + for (y = 1; y < ycount; y++) + if (gv[y - 1][x] != gv[y][x]) { + c = mkConstraint(gv[y - 1][x], NSLayoutAttributeBottom, + NSLayoutRelationEqual, + gv[y][x], NSLayoutAttributeTop, + 1, -padding, + @"uiGrid internal vertical constraint"); + [self addConstraint:c]; + [self->inBetweens addObject:c]; + } + + // now set priorities for all widgets that expand or not + // if a cell is in an expanding row, OR If it spans, then it must be willing to stretch + // otherwise, it tries not to + // note we don't use NSLayoutPriorityRequired as that will cause things to squish when they shouldn't + for (gc in self->children) { + NSLayoutPriority priority; + + if (!uiControlVisible(gc.c)) + continue; + if (hexpand[gc.left - xmin] || gc.xspan != 1) + priority = NSLayoutPriorityDefaultLow; + else + priority = NSLayoutPriorityDefaultHigh; + uiDarwinControlSetHuggingPriority(uiDarwinControl(gc.c), priority, NSLayoutConstraintOrientationHorizontal); + // same for vertical direction + if (vexpand[gc.top - ymin] || gc.yspan != 1) + priority = NSLayoutPriorityDefaultLow; + else + priority = NSLayoutPriorityDefaultHigh; + uiDarwinControlSetHuggingPriority(uiDarwinControl(gc.c), priority, NSLayoutConstraintOrientationVertical); + } + + // TODO make all expanding rows/columns the same height/width + + // and finally clean up + uiFree(hexpand); + uiFree(vexpand); + for (y = 0; y < ycount; y++) { + uiFree(gg[y]); + uiFree(gv[y]); + uiFree(gspan[y]); + } + uiFree(gg); + uiFree(gv); + uiFree(gspan); +} + +- (void)append:(gridChild *)gc +{ + BOOL update; + int oldnh, oldnv; + + [gc setTranslatesAutoresizingMaskIntoConstraints:NO]; + [self addSubview:gc]; + + // no need to set priority here; that's done in establishOurConstraints + + oldnh = [self nhexpand]; + oldnv = [self nvexpand]; + [self->children addObject:gc]; + + [self establishOurConstraints]; + update = NO; + if (gc.hexpand) + if (oldnh == 0) + update = YES; + if (gc.vexpand) + if (oldnv == 0) + update = YES; + if (update) + uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(self->g)); + + [gc release]; // we don't need the initial reference now +} + +- (void)insert:(gridChild *)gc after:(uiControl *)c at:(uiAt)at +{ + gridChild *other; + BOOL found; + + found = NO; + for (other in self->children) + if (other.c == c) { + found = YES; + break; + } + if (!found) + userbug("Existing control %p is not in grid %p; you cannot add other controls next to it", c, self->g); + + switch (at) { + case uiAtLeading: + gc.left = other.left - gc.xspan; + gc.top = other.top; + break; + case uiAtTop: + gc.left = other.left; + gc.top = other.top - gc.yspan; + break; + case uiAtTrailing: + gc.left = other.left + other.xspan; + gc.top = other.top; + break; + case uiAtBottom: + gc.left = other.left; + gc.top = other.top + other.yspan; + break; + // TODO add error checks to ALL enums + } + + [self append:gc]; +} + +- (int)isPadded +{ + return self->padded; +} + +- (void)setPadded:(int)p +{ + CGFloat padding; + NSLayoutConstraint *c; + +#if 0 /* TODO */ +dispatch_after( +dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC), +dispatch_get_main_queue(), +^{ [[self window] visualizeConstraints:[self constraints]]; } +); +#endif + self->padded = p; + padding = [self paddingAmount]; + for (c in self->inBetweens) + switch ([c firstAttribute]) { + case NSLayoutAttributeLeading: + case NSLayoutAttributeTop: + [c setConstant:padding]; + break; + case NSLayoutAttributeTrailing: + case NSLayoutAttributeBottom: + [c setConstant:-padding]; + break; + } +} + +- (BOOL)hugsTrailing +{ + // only hug if we have horizontally expanding + return [self nhexpand] != 0; +} + +- (BOOL)hugsBottom +{ + // only hug if we have vertically expanding + return [self nvexpand] != 0; +} + +- (int)nhexpand +{ + gridChild *gc; + int n; + + n = 0; + for (gc in self->children) { + if (!uiControlVisible(gc.c)) + continue; + if (gc.hexpand) + n++; + } + return n; +} + +- (int)nvexpand +{ + gridChild *gc; + int n; + + n = 0; + for (gc in self->children) { + if (!uiControlVisible(gc.c)) + continue; + if (gc.vexpand) + n++; + } + return n; +} + +@end + +static void uiGridDestroy(uiControl *c) +{ + uiGrid *g = uiGrid(c); + + [g->view onDestroy]; + [g->view release]; + uiFreeControl(uiControl(g)); +} + +uiDarwinControlDefaultHandle(uiGrid, view) +uiDarwinControlDefaultParent(uiGrid, view) +uiDarwinControlDefaultSetParent(uiGrid, view) +uiDarwinControlDefaultToplevel(uiGrid, view) +uiDarwinControlDefaultVisible(uiGrid, view) +uiDarwinControlDefaultShow(uiGrid, view) +uiDarwinControlDefaultHide(uiGrid, view) +uiDarwinControlDefaultEnabled(uiGrid, view) +uiDarwinControlDefaultEnable(uiGrid, view) +uiDarwinControlDefaultDisable(uiGrid, view) + +static void uiGridSyncEnableState(uiDarwinControl *c, int enabled) +{ + uiGrid *g = uiGrid(c); + + if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(g), enabled)) + return; + [g->view syncEnableStates:enabled]; +} + +uiDarwinControlDefaultSetSuperview(uiGrid, view) + +static BOOL uiGridHugsTrailingEdge(uiDarwinControl *c) +{ + uiGrid *g = uiGrid(c); + + return [g->view hugsTrailing]; +} + +static BOOL uiGridHugsBottom(uiDarwinControl *c) +{ + uiGrid *g = uiGrid(c); + + return [g->view hugsBottom]; +} + +static void uiGridChildEdgeHuggingChanged(uiDarwinControl *c) +{ + uiGrid *g = uiGrid(c); + + [g->view establishOurConstraints]; +} + +uiDarwinControlDefaultHuggingPriority(uiGrid, view) +uiDarwinControlDefaultSetHuggingPriority(uiGrid, view) + +static void uiGridChildVisibilityChanged(uiDarwinControl *c) +{ + uiGrid *g = uiGrid(c); + + [g->view establishOurConstraints]; +} + +static gridChild *toChild(uiControl *c, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign, uiGrid *g) +{ + gridChild *gc; + + if (xspan < 0) + userbug("You cannot have a negative xspan in a uiGrid cell."); + if (yspan < 0) + userbug("You cannot have a negative yspan in a uiGrid cell."); + gc = [gridChild new]; + gc.xspan = xspan; + gc.yspan = yspan; + gc.hexpand = hexpand; + gc.halign = halign; + gc.vexpand = vexpand; + gc.valign = valign; + [gc setC:c grid:g]; + return gc; +} + +void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) +{ + gridChild *gc; + + // LONGTERM on other platforms + // or at leat allow this and implicitly turn it into a spacer + if (c == NULL) + userbug("You cannot add NULL to a uiGrid."); + gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign, g); + gc.left = left; + gc.top = top; + [g->view append:gc]; +} + +void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) +{ + gridChild *gc; + + gc = toChild(c, xspan, yspan, hexpand, halign, vexpand, valign, g); + [g->view insert:gc after:existing at:at]; +} + +int uiGridPadded(uiGrid *g) +{ + return [g->view isPadded]; +} + +void uiGridSetPadded(uiGrid *g, int padded) +{ + [g->view setPadded:padded]; +} + +uiGrid *uiNewGrid(void) +{ + uiGrid *g; + + uiDarwinNewControl(uiGrid, g); + + g->view = [[gridView alloc] initWithG:g]; + + return g; +} diff --git a/src/libui_sdl/libui/darwin/group.m b/src/libui_sdl/libui/darwin/group.m new file mode 100644 index 0000000..0050bbd --- /dev/null +++ b/src/libui_sdl/libui/darwin/group.m @@ -0,0 +1,194 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +struct uiGroup { + uiDarwinControl c; + NSBox *box; + uiControl *child; + NSLayoutPriority oldHorzHuggingPri; + NSLayoutPriority oldVertHuggingPri; + int margined; + struct singleChildConstraints constraints; + NSLayoutPriority horzHuggingPri; + NSLayoutPriority vertHuggingPri; +}; + +static void removeConstraints(uiGroup *g) +{ + // set to contentView instead of to the box itself, otherwise we get clipping underneath the label + singleChildConstraintsRemove(&(g->constraints), [g->box contentView]); +} + +static void uiGroupDestroy(uiControl *c) +{ + uiGroup *g = uiGroup(c); + + removeConstraints(g); + if (g->child != NULL) { + uiControlSetParent(g->child, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(g->child), nil); + uiControlDestroy(g->child); + } + [g->box release]; + uiFreeControl(uiControl(g)); +} + +uiDarwinControlDefaultHandle(uiGroup, box) +uiDarwinControlDefaultParent(uiGroup, box) +uiDarwinControlDefaultSetParent(uiGroup, box) +uiDarwinControlDefaultToplevel(uiGroup, box) +uiDarwinControlDefaultVisible(uiGroup, box) +uiDarwinControlDefaultShow(uiGroup, box) +uiDarwinControlDefaultHide(uiGroup, box) +uiDarwinControlDefaultEnabled(uiGroup, box) +uiDarwinControlDefaultEnable(uiGroup, box) +uiDarwinControlDefaultDisable(uiGroup, box) + +static void uiGroupSyncEnableState(uiDarwinControl *c, int enabled) +{ + uiGroup *g = uiGroup(c); + + if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(g), enabled)) + return; + if (g->child != NULL) + uiDarwinControlSyncEnableState(uiDarwinControl(g->child), enabled); +} + +uiDarwinControlDefaultSetSuperview(uiGroup, box) + +static void groupRelayout(uiGroup *g) +{ + NSView *childView; + + removeConstraints(g); + if (g->child == NULL) + return; + childView = (NSView *) uiControlHandle(g->child); + singleChildConstraintsEstablish(&(g->constraints), + [g->box contentView], childView, + uiDarwinControlHugsTrailingEdge(uiDarwinControl(g->child)), + uiDarwinControlHugsBottom(uiDarwinControl(g->child)), + g->margined, + @"uiGroup"); + // needed for some very rare drawing errors... + jiggleViewLayout(g->box); +} + +// TODO rename these since I'm starting to get confused by what they mean by hugging +BOOL uiGroupHugsTrailingEdge(uiDarwinControl *c) +{ + uiGroup *g = uiGroup(c); + + // TODO make a function? + return g->horzHuggingPri < NSLayoutPriorityWindowSizeStayPut; +} + +BOOL uiGroupHugsBottom(uiDarwinControl *c) +{ + uiGroup *g = uiGroup(c); + + return g->vertHuggingPri < NSLayoutPriorityWindowSizeStayPut; +} + +static void uiGroupChildEdgeHuggingChanged(uiDarwinControl *c) +{ + uiGroup *g = uiGroup(c); + + groupRelayout(g); +} + +static NSLayoutPriority uiGroupHuggingPriority(uiDarwinControl *c, NSLayoutConstraintOrientation orientation) +{ + uiGroup *g = uiGroup(c); + + if (orientation == NSLayoutConstraintOrientationHorizontal) + return g->horzHuggingPri; + return g->vertHuggingPri; +} + +static void uiGroupSetHuggingPriority(uiDarwinControl *c, NSLayoutPriority priority, NSLayoutConstraintOrientation orientation) +{ + uiGroup *g = uiGroup(c); + + if (orientation == NSLayoutConstraintOrientationHorizontal) + g->horzHuggingPri = priority; + else + g->vertHuggingPri = priority; + uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(g)); +} + +static void uiGroupChildVisibilityChanged(uiDarwinControl *c) +{ + uiGroup *g = uiGroup(c); + + groupRelayout(g); +} + +char *uiGroupTitle(uiGroup *g) +{ + return uiDarwinNSStringToText([g->box title]); +} + +void uiGroupSetTitle(uiGroup *g, const char *title) +{ + [g->box setTitle:toNSString(title)]; +} + +void uiGroupSetChild(uiGroup *g, uiControl *child) +{ + NSView *childView; + + if (g->child != NULL) { + removeConstraints(g); + uiDarwinControlSetHuggingPriority(uiDarwinControl(g->child), g->oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal); + uiDarwinControlSetHuggingPriority(uiDarwinControl(g->child), g->oldVertHuggingPri, NSLayoutConstraintOrientationVertical); + uiControlSetParent(g->child, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(g->child), nil); + } + g->child = child; + if (g->child != NULL) { + childView = (NSView *) uiControlHandle(g->child); + uiControlSetParent(g->child, uiControl(g)); + uiDarwinControlSetSuperview(uiDarwinControl(g->child), [g->box contentView]); + uiDarwinControlSyncEnableState(uiDarwinControl(g->child), uiControlEnabledToUser(uiControl(g))); + // don't hug, just in case we're a stretchy group + g->oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(g->child), NSLayoutConstraintOrientationHorizontal); + g->oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(g->child), NSLayoutConstraintOrientationVertical); + uiDarwinControlSetHuggingPriority(uiDarwinControl(g->child), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationHorizontal); + uiDarwinControlSetHuggingPriority(uiDarwinControl(g->child), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationVertical); + } + groupRelayout(g); +} + +int uiGroupMargined(uiGroup *g) +{ + return g->margined; +} + +void uiGroupSetMargined(uiGroup *g, int margined) +{ + g->margined = margined; + singleChildConstraintsSetMargined(&(g->constraints), g->margined); +} + +uiGroup *uiNewGroup(const char *title) +{ + uiGroup *g; + + uiDarwinNewControl(uiGroup, g); + + g->box = [[NSBox alloc] initWithFrame:NSZeroRect]; + [g->box setTitle:toNSString(title)]; + [g->box setBoxType:NSBoxPrimary]; + [g->box setBorderType:NSLineBorder]; + [g->box setTransparent:NO]; + [g->box setTitlePosition:NSAtTop]; + // we can't use uiDarwinSetControlFont() because the selector is different + [g->box setTitleFont:[NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSSmallControlSize]]]; + + // default to low hugging to not hug edges + g->horzHuggingPri = NSLayoutPriorityDefaultLow; + g->vertHuggingPri = NSLayoutPriorityDefaultLow; + + return g; +} diff --git a/src/libui_sdl/libui/darwin/image.m b/src/libui_sdl/libui/darwin/image.m new file mode 100644 index 0000000..b62de31 --- /dev/null +++ b/src/libui_sdl/libui/darwin/image.m @@ -0,0 +1,82 @@ +// 25 june 2016 +#import "uipriv_darwin.h" + +struct uiImage { + NSImage *i; + NSSize size; + NSMutableArray *swizzled; +}; + +uiImage *uiNewImage(double width, double height) +{ + uiImage *i; + + i = uiNew(uiImage); + i->size = NSMakeSize(width, height); + i->i = [[NSImage alloc] initWithSize:i->size]; + i->swizzled = [NSMutableArray new]; + return i; +} + +void uiFreeImage(uiImage *i) +{ + NSValue *v; + + [i->i release]; + // to be safe, do this after releasing the image + for (v in i->swizzled) + uiFree([v pointerValue]); + [i->swizzled release]; + uiFree(i); +} + +void uiImageAppend(uiImage *i, void *pixels, int pixelWidth, int pixelHeight, int pixelStride) +{ + NSBitmapImageRep *repCalibrated, *repsRGB; + uint8_t *swizzled, *bp, *sp; + int x, y; + unsigned char *pix[1]; + + // OS X demands that R and B are in the opposite order from what we expect + // we must swizzle :( + // LONGTERM test on a big-endian system + swizzled = (uint8_t *) uiAlloc((pixelStride * pixelHeight * 4) * sizeof (uint8_t), "uint8_t[]"); + bp = (uint8_t *) pixels; + sp = swizzled; + for (y = 0; y < pixelHeight * pixelStride; y += pixelStride) + for (x = 0; x < pixelStride; x++) { + sp[0] = bp[2]; + sp[1] = bp[1]; + sp[2] = bp[0]; + sp[3] = bp[3]; + sp += 4; + bp += 4; + } + + pix[0] = (unsigned char *) swizzled; + repCalibrated = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:pix + pixelsWide:pixelWidth + pixelsHigh:pixelHeight + bitsPerSample:8 + samplesPerPixel:4 + hasAlpha:YES + isPlanar:NO + colorSpaceName:NSCalibratedRGBColorSpace + bitmapFormat:0 + bytesPerRow:pixelStride + bitsPerPixel:32]; + repsRGB = [repCalibrated bitmapImageRepByRetaggingWithColorSpace:[NSColorSpace sRGBColorSpace]]; + [repCalibrated release]; + + [i->i addRepresentation:repsRGB]; + [repsRGB setSize:i->size]; + [repsRGB release]; + + // we need to keep swizzled alive for NSBitmapImageRep + [i->swizzled addObject:[NSValue valueWithPointer:swizzled]]; +} + +NSImage *imageImage(uiImage *i) +{ + return i->i; +} diff --git a/src/libui_sdl/libui/darwin/label.m b/src/libui_sdl/libui/darwin/label.m new file mode 100644 index 0000000..897bc3f --- /dev/null +++ b/src/libui_sdl/libui/darwin/label.m @@ -0,0 +1,43 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +struct uiLabel { + uiDarwinControl c; + NSTextField *textfield; +}; + +uiDarwinControlAllDefaults(uiLabel, textfield) + +char *uiLabelText(uiLabel *l) +{ + return uiDarwinNSStringToText([l->textfield stringValue]); +} + +void uiLabelSetText(uiLabel *l, const char *text) +{ + [l->textfield setStringValue:toNSString(text)]; +} + +NSTextField *newLabel(NSString *str) +{ + NSTextField *tf; + + tf = [[NSTextField alloc] initWithFrame:NSZeroRect]; + [tf setStringValue:str]; + [tf setEditable:NO]; + [tf setSelectable:NO]; + [tf setDrawsBackground:NO]; + finishNewTextField(tf, NO); + return tf; +} + +uiLabel *uiNewLabel(const char *text) +{ + uiLabel *l; + + uiDarwinNewControl(uiLabel, l); + + l->textfield = newLabel(toNSString(text)); + + return l; +} diff --git a/src/libui_sdl/libui/darwin/main.m b/src/libui_sdl/libui/darwin/main.m new file mode 100644 index 0000000..59a8683 --- /dev/null +++ b/src/libui_sdl/libui/darwin/main.m @@ -0,0 +1,239 @@ +// 6 april 2015 +#import "uipriv_darwin.h" + +static BOOL canQuit = NO; +static NSAutoreleasePool *globalPool; +static applicationClass *app; +static appDelegate *delegate; + +static BOOL (^isRunning)(void); +static BOOL stepsIsRunning; + +@implementation applicationClass + +- (void)sendEvent:(NSEvent *)e +{ + if (sendAreaEvents(e) != 0) + return; + [super sendEvent:e]; +} + +// NSColorPanel always sends changeColor: to the first responder regardless of whether there's a target set on it +// we can override it here (see colorbutton.m) +// thanks to mikeash in irc.freenode.net/#macdev for informing me this is how the first responder chain is initiated +// it turns out NSFontManager also sends changeFont: through this; let's inhibit that here too (see fontbutton.m) +- (BOOL)sendAction:(SEL)sel to:(id)to from:(id)from +{ + if (colorButtonInhibitSendAction(sel, from, to)) + return NO; + if (fontButtonInhibitSendAction(sel, from, to)) + return NO; + return [super sendAction:sel to:to from:from]; +} + +// likewise, NSFontManager also sends NSFontPanelValidation messages to the first responder, however it does NOT use sendAction:from:to:! +// instead, it uses this one (thanks swillits in irc.freenode.net/#macdev) +// we also need to override it (see fontbutton.m) +- (id)targetForAction:(SEL)sel to:(id)to from:(id)from +{ + id override; + + if (fontButtonOverrideTargetForAction(sel, from, to, &override)) + return override; + return [super targetForAction:sel to:to from:from]; +} + +// hey look! we're overriding terminate:! +// we're going to make sure we can go back to main() whether Cocoa likes it or not! +// and just how are we going to do that, hm? +// (note: this is called after applicationShouldTerminate:) +- (void)terminate:(id)sender +{ + // yes that's right folks: DO ABSOLUTELY NOTHING. + // the magic is [NSApp run] will just... stop. + + // well let's not do nothing; let's actually quit our graceful way + NSEvent *e; + + if (!canQuit) + implbug("call to [NSApp terminate:] when not ready to terminate; definitely contact andlabs"); + + [realNSApp() stop:realNSApp()]; + // stop: won't register until another event has passed; let's synthesize one + e = [NSEvent otherEventWithType:NSApplicationDefined + location:NSZeroPoint + modifierFlags:0 + timestamp:[[NSProcessInfo processInfo] systemUptime] + windowNumber:0 + context:[NSGraphicsContext currentContext] + subtype:0 + data1:0 + data2:0]; + [realNSApp() postEvent:e atStart:NO]; // let pending events take priority (this is what PostQuitMessage() on Windows does so we have to do it here too for parity; thanks to mikeash in irc.freenode.net/#macdev for confirming that this parameter should indeed be NO) + + // and in case uiMainSteps() was called + stepsIsRunning = NO; +} + +@end + +@implementation appDelegate + +- (void)dealloc +{ + // Apple docs: "Don't Use Accessor Methods in Initializer Methods and dealloc" + [_menuManager release]; + [super dealloc]; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)app +{ + // for debugging + NSLog(@"in applicationShouldTerminate:"); + if (shouldQuit()) { + canQuit = YES; + // this will call terminate:, which is the same as uiQuit() + return NSTerminateNow; + } + return NSTerminateCancel; +} + +- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)app +{ + return NO; +} + +@end + +uiInitOptions options; + +const char *uiInit(uiInitOptions *o) +{ + @autoreleasepool { + options = *o; + app = [[applicationClass sharedApplication] retain]; + // don't check for a NO return; something (launch services?) causes running from application bundles to always return NO when asking to change activation policy, even if the change is to the same activation policy! + // see https://github.com/andlabs/ui/issues/6 + [realNSApp() setActivationPolicy:NSApplicationActivationPolicyRegular]; + delegate = [appDelegate new]; + [realNSApp() setDelegate:delegate]; + + initAlloc(); + + // always do this so we always have an application menu + appDelegate().menuManager = [[menuManager new] autorelease]; + [realNSApp() setMainMenu:[appDelegate().menuManager makeMenubar]]; + + setupFontPanel(); + } + + globalPool = [[NSAutoreleasePool alloc] init]; + + return NULL; +} + +void uiUninit(void) +{ + if (!globalPool) { + userbug("You must call uiInit() first!"); + } + [globalPool release]; + + @autoreleasepool { + [delegate release]; + [realNSApp() setDelegate:nil]; + [app release]; + uninitAlloc(); + } +} + +void uiFreeInitError(const char *err) +{ +} + +void uiMain(void) +{ + isRunning = ^{ + return [realNSApp() isRunning]; + }; + [realNSApp() run]; +} + +void uiMainSteps(void) +{ + // SDL does this and it seems to be necessary for the menubar to work (see #182) + [realNSApp() finishLaunching]; + isRunning = ^{ + return stepsIsRunning; + }; + stepsIsRunning = YES; +} + +int uiMainStep(int wait) +{ + struct nextEventArgs nea; + + nea.mask = NSAnyEventMask; + + // ProPuke did this in his original PR requesting this + // I'm not sure if this will work, but I assume it will... + nea.duration = [NSDate distantPast]; + if (wait) // but this is normal so it will work + nea.duration = [NSDate distantFuture]; + + nea.mode = NSDefaultRunLoopMode; + nea.dequeue = YES; + + return mainStep(&nea, ^(NSEvent *e) { + return NO; + }); +} + +// see also: +// - http://www.cocoawithlove.com/2009/01/demystifying-nsapplication-by.html +// - https://github.com/gnustep/gui/blob/master/Source/NSApplication.m +int mainStep(struct nextEventArgs *nea, BOOL (^interceptEvent)(NSEvent *e)) +{ + NSDate *expire; + NSEvent *e; + NSEventType type; + + @autoreleasepool { + if (!isRunning()) + return 0; + + e = [realNSApp() nextEventMatchingMask:nea->mask + untilDate:nea->duration + inMode:nea->mode + dequeue:nea->dequeue]; + if (e == nil) + return 1; + + type = [e type]; + if (!interceptEvent(e)) + [realNSApp() sendEvent:e]; + [realNSApp() updateWindows]; + + // GNUstep does this + // it also updates the Services menu but there doesn't seem to be a public API for that so + if (type != NSPeriodic && type != NSMouseMoved) + [[realNSApp() mainMenu] update]; + + return 1; + } +} + +void uiQuit(void) +{ + canQuit = YES; + [realNSApp() terminate:realNSApp()]; +} + +// thanks to mikeash in irc.freenode.net/#macdev for suggesting the use of Grand Central Dispatch for this +// LONGTERM will dispatch_get_main_queue() break after _CFRunLoopSetCurrent()? +void uiQueueMain(void (*f)(void *data), void *data) +{ + // dispatch_get_main_queue() is a serial queue so it will not execute multiple uiQueueMain() functions concurrently + // the signature of f matches dispatch_function_t + dispatch_async_f(dispatch_get_main_queue(), data, f); +} diff --git a/src/libui_sdl/libui/darwin/map.m b/src/libui_sdl/libui/darwin/map.m new file mode 100644 index 0000000..46a7b8d --- /dev/null +++ b/src/libui_sdl/libui/darwin/map.m @@ -0,0 +1,59 @@ +// 17 august 2015 +#import "uipriv_darwin.h" + +// unfortunately NSMutableDictionary copies its keys, meaning we can't use it for pointers +// hence, this file +// we could expose a NSMapTable directly, but let's treat all pointers as opaque and hide the implementation, just to be safe and prevent even more rewrites later +struct mapTable { + NSMapTable *m; +}; + +struct mapTable *newMap(void) +{ + struct mapTable *m; + + m = uiNew(struct mapTable); + m->m = [[NSMapTable alloc] initWithKeyOptions:(NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality) + valueOptions:(NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality) + capacity:0]; + return m; +} + +void mapDestroy(struct mapTable *m) +{ + if ([m->m count] != 0) + implbug("attempt to destroy map with items inside"); + [m->m release]; + uiFree(m); +} + +void *mapGet(struct mapTable *m, void *key) +{ + return NSMapGet(m->m, key); +} + +void mapSet(struct mapTable *m, void *key, void *value) +{ + NSMapInsert(m->m, key, value); +} + +void mapDelete(struct mapTable *m, void *key) +{ + NSMapRemove(m->m, key); +} + +void mapWalk(struct mapTable *m, void (*f)(void *key, void *value)) +{ + NSMapEnumerator e = NSEnumerateMapTable(m->m); + void *k = NULL; + void *v = NULL; + while (NSNextMapEnumeratorPair(&e, &k, &v)) { + f(k, v); + } + NSEndMapTableEnumeration(&e); +} + +void mapReset(struct mapTable *m) +{ + NSResetMapTable(m->m); +} diff --git a/src/libui_sdl/libui/darwin/menu.m b/src/libui_sdl/libui/darwin/menu.m new file mode 100644 index 0000000..735cac5 --- /dev/null +++ b/src/libui_sdl/libui/darwin/menu.m @@ -0,0 +1,368 @@ +// 28 april 2015 +#import "uipriv_darwin.h" + +static NSMutableArray *menus = nil; +static BOOL menusFinalized = NO; + +struct uiMenu { + NSMenu *menu; + NSMenuItem *item; + NSMutableArray *items; +}; + +struct uiMenuItem { + NSMenuItem *item; + int type; + BOOL disabled; + void (*onClicked)(uiMenuItem *, uiWindow *, void *); + void *onClickedData; +}; + +enum { + typeRegular, + typeCheckbox, + typeQuit, + typePreferences, + typeAbout, + typeSeparator, +}; + +static void mapItemReleaser(void *key, void *value) +{ + uiMenuItem *item; + + item = (uiMenuItem *)value; + [item->item release]; +} + +@implementation menuManager + +- (id)init +{ + self = [super init]; + if (self) { + self->items = newMap(); + self->hasQuit = NO; + self->hasPreferences = NO; + self->hasAbout = NO; + } + return self; +} + +- (void)dealloc +{ + mapWalk(self->items, mapItemReleaser); + mapReset(self->items); + mapDestroy(self->items); + uninitMenus(); + [super dealloc]; +} + +- (IBAction)onClicked:(id)sender +{ + uiMenuItem *item; + + item = (uiMenuItem *) mapGet(self->items, sender); + if (item->type == typeCheckbox) + uiMenuItemSetChecked(item, !uiMenuItemChecked(item)); + // use the key window as the source of the menu event; it's the active window + (*(item->onClicked))(item, windowFromNSWindow([realNSApp() keyWindow]), item->onClickedData); +} + +- (IBAction)onQuitClicked:(id)sender +{ + if (shouldQuit()) + uiQuit(); +} + +- (void)register:(NSMenuItem *)item to:(uiMenuItem *)smi +{ + switch (smi->type) { + case typeQuit: + if (self->hasQuit) + userbug("You can't have multiple Quit menu items in one program."); + self->hasQuit = YES; + break; + case typePreferences: + if (self->hasPreferences) + userbug("You can't have multiple Preferences menu items in one program."); + self->hasPreferences = YES; + break; + case typeAbout: + if (self->hasAbout) + userbug("You can't have multiple About menu items in one program."); + self->hasAbout = YES; + break; + } + mapSet(self->items, item, smi); +} + +// on OS X there are two ways to handle menu items being enabled or disabled: automatically and manually +// unfortunately, the application menu requires automatic menu handling for the Hide, Hide Others, and Show All items to work correctly +// therefore, we have to handle enabling of the other options ourselves +- (BOOL)validateMenuItem:(NSMenuItem *)item +{ + uiMenuItem *smi; + + // disable the special items if they aren't present + if (item == self.quitItem && !self->hasQuit) + return NO; + if (item == self.preferencesItem && !self->hasPreferences) + return NO; + if (item == self.aboutItem && !self->hasAbout) + return NO; + // then poll the item's enabled/disabled state + smi = (uiMenuItem *) mapGet(self->items, item); + return !smi->disabled; +} + +// Cocoa constructs the default application menu by hand for each program; that's what MainMenu.[nx]ib does +- (void)buildApplicationMenu:(NSMenu *)menubar +{ + NSString *appName; + NSMenuItem *appMenuItem; + NSMenu *appMenu; + NSMenuItem *item; + NSString *title; + NSMenu *servicesMenu; + + // note: no need to call setAppleMenu: on this anymore; see https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKitOlderNotes/#X10_6Notes + appName = [[NSProcessInfo processInfo] processName]; + appMenuItem = [[[NSMenuItem alloc] initWithTitle:appName action:NULL keyEquivalent:@""] autorelease]; + appMenu = [[[NSMenu alloc] initWithTitle:appName] autorelease]; + [appMenuItem setSubmenu:appMenu]; + [menubar addItem:appMenuItem]; + + // first is About + title = [@"About " stringByAppendingString:appName]; + item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(onClicked:) keyEquivalent:@""] autorelease]; + [item setTarget:self]; + [appMenu addItem:item]; + self.aboutItem = item; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // next is Preferences + item = [[[NSMenuItem alloc] initWithTitle:@"Preferences…" action:@selector(onClicked:) keyEquivalent:@","] autorelease]; + [item setTarget:self]; + [appMenu addItem:item]; + self.preferencesItem = item; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // next is Services + item = [[[NSMenuItem alloc] initWithTitle:@"Services" action:NULL keyEquivalent:@""] autorelease]; + servicesMenu = [[[NSMenu alloc] initWithTitle:@"Services"] autorelease]; + [item setSubmenu:servicesMenu]; + [realNSApp() setServicesMenu:servicesMenu]; + [appMenu addItem:item]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // next are the three hiding options + title = [@"Hide " stringByAppendingString:appName]; + item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(hide:) keyEquivalent:@"h"] autorelease]; + // the .xib file says they go to -1 ("First Responder", which sounds wrong...) + // to do that, we simply leave the target as nil + [appMenu addItem:item]; + item = [[[NSMenuItem alloc] initWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"] autorelease]; + [item setKeyEquivalentModifierMask:(NSAlternateKeyMask | NSCommandKeyMask)]; + [appMenu addItem:item]; + item = [[[NSMenuItem alloc] initWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""] autorelease]; + [appMenu addItem:item]; + + [appMenu addItem:[NSMenuItem separatorItem]]; + + // and finally Quit + // DON'T use @selector(terminate:) as the action; we handle termination ourselves + title = [@"Quit " stringByAppendingString:appName]; + item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(onQuitClicked:) keyEquivalent:@"q"] autorelease]; + [item setTarget:self]; + [appMenu addItem:item]; + self.quitItem = item; +} + +- (NSMenu *)makeMenubar +{ + NSMenu *menubar; + + menubar = [[[NSMenu alloc] initWithTitle:@""] autorelease]; + [self buildApplicationMenu:menubar]; + return menubar; +} + +@end + +static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data) +{ + // do nothing +} + +void uiMenuItemEnable(uiMenuItem *item) +{ + item->disabled = NO; + // we don't need to explicitly update the menus here; they'll be updated the next time they're opened (thanks mikeash in irc.freenode.net/#macdev) +} + +void uiMenuItemDisable(uiMenuItem *item) +{ + item->disabled = YES; +} + +void uiMenuItemOnClicked(uiMenuItem *item, void (*f)(uiMenuItem *, uiWindow *, void *), void *data) +{ + if (item->type == typeQuit) + userbug("You can't call uiMenuItemOnClicked() on a Quit item; use uiOnShouldQuit() instead."); + item->onClicked = f; + item->onClickedData = data; +} + +int uiMenuItemChecked(uiMenuItem *item) +{ + return [item->item state] != NSOffState; +} + +void uiMenuItemSetChecked(uiMenuItem *item, int checked) +{ + NSInteger state; + + state = NSOffState; + if ([item->item state] == NSOffState) + state = NSOnState; + [item->item setState:state]; +} + +static uiMenuItem *newItem(uiMenu *m, int type, const char *name) +{ + @autoreleasepool { + + uiMenuItem *item; + + if (menusFinalized) + userbug("You can't create a new menu item after menus have been finalized."); + + item = uiNew(uiMenuItem); + + item->type = type; + switch (item->type) { + case typeQuit: + item->item = [appDelegate().menuManager.quitItem retain]; + break; + case typePreferences: + item->item = [appDelegate().menuManager.preferencesItem retain]; + break; + case typeAbout: + item->item = [appDelegate().menuManager.aboutItem retain]; + break; + case typeSeparator: + item->item = [[NSMenuItem separatorItem] retain]; + [m->menu addItem:item->item]; + break; + default: + item->item = [[NSMenuItem alloc] initWithTitle:toNSString(name) action:@selector(onClicked:) keyEquivalent:@""]; + [item->item setTarget:appDelegate().menuManager]; + [m->menu addItem:item->item]; + break; + } + + [appDelegate().menuManager register:item->item to:item]; + item->onClicked = defaultOnClicked; + + [m->items addObject:[NSValue valueWithPointer:item]]; + + return item; + + } // @autoreleasepool +} + +uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name) +{ + return newItem(m, typeRegular, name); +} + +uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name) +{ + return newItem(m, typeCheckbox, name); +} + +uiMenuItem *uiMenuAppendQuitItem(uiMenu *m) +{ + // duplicate check is in the register:to: selector + return newItem(m, typeQuit, NULL); +} + +uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m) +{ + // duplicate check is in the register:to: selector + return newItem(m, typePreferences, NULL); +} + +uiMenuItem *uiMenuAppendAboutItem(uiMenu *m) +{ + // duplicate check is in the register:to: selector + return newItem(m, typeAbout, NULL); +} + +void uiMenuAppendSeparator(uiMenu *m) +{ + newItem(m, typeSeparator, NULL); +} + +uiMenu *uiNewMenu(const char *name) +{ + @autoreleasepool { + + uiMenu *m; + + if (menusFinalized) + userbug("You can't create a new menu after menus have been finalized."); + if (menus == nil) + menus = [NSMutableArray new]; + + m = uiNew(uiMenu); + + m->menu = [[NSMenu alloc] initWithTitle:toNSString(name)]; + // use automatic menu item enabling for all menus for consistency's sake + + m->item = [[NSMenuItem alloc] initWithTitle:toNSString(name) action:NULL keyEquivalent:@""]; + [m->item setSubmenu:m->menu]; + + m->items = [NSMutableArray new]; + + [[realNSApp() mainMenu] addItem:m->item]; + + [menus addObject:[NSValue valueWithPointer:m]]; + + return m; + + } // @autoreleasepool +} + +void finalizeMenus(void) +{ + menusFinalized = YES; +} + +void uninitMenus(void) +{ + if (menus == NULL) + return; + [menus enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) { + NSValue *v; + uiMenu *m; + + v = (NSValue *) obj; + m = (uiMenu *) [v pointerValue]; + [m->items enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) { + NSValue *v; + uiMenuItem *mi; + + v = (NSValue *) obj; + mi = (uiMenuItem *) [v pointerValue]; + uiFree(mi); + }]; + [m->items release]; + uiFree(m); + }]; + [menus release]; +} diff --git a/src/libui_sdl/libui/darwin/multilineentry.m b/src/libui_sdl/libui/darwin/multilineentry.m new file mode 100644 index 0000000..605e900 --- /dev/null +++ b/src/libui_sdl/libui/darwin/multilineentry.m @@ -0,0 +1,233 @@ +// 8 december 2015 +#import "uipriv_darwin.h" + +// NSTextView has no intrinsic content size by default, which wreaks havoc on a pure-Auto Layout system +// we'll have to take over to get it to work +// see also http://stackoverflow.com/questions/24210153/nstextview-not-properly-resizing-with-auto-layout and http://stackoverflow.com/questions/11237622/using-autolayout-with-expanding-nstextviews +@interface intrinsicSizeTextView : NSTextView { + uiMultilineEntry *libui_e; +} +- (id)initWithFrame:(NSRect)r e:(uiMultilineEntry *)e; +@end + +struct uiMultilineEntry { + uiDarwinControl c; + NSScrollView *sv; + intrinsicSizeTextView *tv; + struct scrollViewData *d; + void (*onChanged)(uiMultilineEntry *, void *); + void *onChangedData; + BOOL changing; +}; + +@implementation intrinsicSizeTextView + +- (id)initWithFrame:(NSRect)r e:(uiMultilineEntry *)e +{ + self = [super initWithFrame:r]; + if (self) + self->libui_e = e; + return self; +} + +- (NSSize)intrinsicContentSize +{ + NSTextContainer *textContainer; + NSLayoutManager *layoutManager; + NSRect rect; + + textContainer = [self textContainer]; + layoutManager = [self layoutManager]; + [layoutManager ensureLayoutForTextContainer:textContainer]; + rect = [layoutManager usedRectForTextContainer:textContainer]; + return rect.size; +} + +- (void)didChangeText +{ + [super didChangeText]; + [self invalidateIntrinsicContentSize]; + if (!self->libui_e->changing) + (*(self->libui_e->onChanged))(self->libui_e, self->libui_e->onChangedData); +} + +@end + +uiDarwinControlAllDefaultsExceptDestroy(uiMultilineEntry, sv) + +static void uiMultilineEntryDestroy(uiControl *c) +{ + uiMultilineEntry *e = uiMultilineEntry(c); + + scrollViewFreeData(e->sv, e->d); + [e->tv release]; + [e->sv release]; + uiFreeControl(uiControl(e)); +} + +static void defaultOnChanged(uiMultilineEntry *e, void *data) +{ + // do nothing +} + +char *uiMultilineEntryText(uiMultilineEntry *e) +{ + return uiDarwinNSStringToText([e->tv string]); +} + +void uiMultilineEntrySetText(uiMultilineEntry *e, const char *text) +{ + [[e->tv textStorage] replaceCharactersInRange:NSMakeRange(0, [[e->tv string] length]) + withString:toNSString(text)]; + // must be called explicitly according to the documentation of shouldChangeTextInRange:replacementString: + e->changing = YES; + [e->tv didChangeText]; + e->changing = NO; +} + +// TODO scroll to end? +void uiMultilineEntryAppend(uiMultilineEntry *e, const char *text) +{ + [[e->tv textStorage] replaceCharactersInRange:NSMakeRange([[e->tv string] length], 0) + withString:toNSString(text)]; + e->changing = YES; + [e->tv didChangeText]; + e->changing = NO; +} + +void uiMultilineEntryOnChanged(uiMultilineEntry *e, void (*f)(uiMultilineEntry *e, void *data), void *data) +{ + e->onChanged = f; + e->onChangedData = data; +} + +int uiMultilineEntryReadOnly(uiMultilineEntry *e) +{ + return [e->tv isEditable] == NO; +} + +void uiMultilineEntrySetReadOnly(uiMultilineEntry *e, int readonly) +{ + BOOL editable; + + editable = YES; + if (readonly) + editable = NO; + [e->tv setEditable:editable]; +} + +static uiMultilineEntry *finishMultilineEntry(BOOL hscroll) +{ + uiMultilineEntry *e; + NSFont *font; + struct scrollViewCreateParams p; + + uiDarwinNewControl(uiMultilineEntry, e); + + e->tv = [[intrinsicSizeTextView alloc] initWithFrame:NSZeroRect e:e]; + + // verified against Interface Builder for a sufficiently customized text view + + // NSText properties: + // this is what Interface Builder sets the background color to + [e->tv setBackgroundColor:[NSColor colorWithCalibratedWhite:1.0 alpha:1.0]]; + [e->tv setDrawsBackground:YES]; + [e->tv setEditable:YES]; + [e->tv setSelectable:YES]; + [e->tv setFieldEditor:NO]; + [e->tv setRichText:NO]; + [e->tv setImportsGraphics:NO]; + [e->tv setUsesFontPanel:NO]; + [e->tv setRulerVisible:NO]; + // we'll handle font last + // while setAlignment: has been around since 10.0, the named constant "NSTextAlignmentNatural" seems to have only been introduced in 10.11 +#define ourNSTextAlignmentNatural 4 + [e->tv setAlignment:ourNSTextAlignmentNatural]; + // textColor is set to nil, just keep the dfault + [e->tv setBaseWritingDirection:NSWritingDirectionNatural]; + [e->tv setHorizontallyResizable:NO]; + [e->tv setVerticallyResizable:YES]; + + // NSTextView properties: + [e->tv setAllowsDocumentBackgroundColorChange:NO]; + [e->tv setAllowsUndo:YES]; + // default paragraph style is nil; keep default + [e->tv setAllowsImageEditing:NO]; + [e->tv setAutomaticQuoteSubstitutionEnabled:NO]; + [e->tv setAutomaticLinkDetectionEnabled:NO]; + [e->tv setDisplaysLinkToolTips:YES]; + [e->tv setUsesRuler:NO]; + [e->tv setUsesInspectorBar:NO]; + [e->tv setSelectionGranularity:NSSelectByCharacter]; + // there is a dedicated named insertion point color but oh well + [e->tv setInsertionPointColor:[NSColor controlTextColor]]; + // typing attributes is nil; keep default (we change it below for fonts though) + [e->tv setSmartInsertDeleteEnabled:NO]; + [e->tv setContinuousSpellCheckingEnabled:NO]; + [e->tv setGrammarCheckingEnabled:NO]; + [e->tv setUsesFindPanel:YES]; + [e->tv setEnabledTextCheckingTypes:0]; + [e->tv setAutomaticDashSubstitutionEnabled:NO]; + [e->tv setAutomaticDataDetectionEnabled:NO]; + [e->tv setAutomaticSpellingCorrectionEnabled:NO]; + [e->tv setAutomaticTextReplacementEnabled:NO]; + [e->tv setUsesFindBar:NO]; + [e->tv setIncrementalSearchingEnabled:NO]; + + // NSTextContainer properties: + [[e->tv textContainer] setWidthTracksTextView:YES]; + [[e->tv textContainer] setHeightTracksTextView:NO]; + + // NSLayoutManager properties: + [[e->tv layoutManager] setAllowsNonContiguousLayout:YES]; + + // now just to be safe; this will do some of the above but whatever + disableAutocorrect(e->tv); + + // see https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextUILayer/Tasks/TextInScrollView.html + // notice we don't use the Auto Layout code; see scrollview.m for more details + [e->tv setMaxSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)]; + [e->tv setVerticallyResizable:YES]; + [e->tv setHorizontallyResizable:hscroll]; + if (hscroll) { + [e->tv setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; + [[e->tv textContainer] setWidthTracksTextView:NO]; + } else { + [e->tv setAutoresizingMask:NSViewWidthSizable]; + [[e->tv textContainer] setWidthTracksTextView:YES]; + } + [[e->tv textContainer] setContainerSize:NSMakeSize(CGFLOAT_MAX, CGFLOAT_MAX)]; + + // don't use uiDarwinSetControlFont() directly; we have to do a little extra work to set the font + font = [NSFont systemFontOfSize:[NSFont systemFontSizeForControlSize:NSRegularControlSize]]; + [e->tv setTypingAttributes:[NSDictionary + dictionaryWithObject:font + forKey:NSFontAttributeName]]; + // e->tv font from Interface Builder is nil, but setFont:nil throws an exception + // let's just set it to the standard control font anyway, just to be safe + [e->tv setFont:font]; + + memset(&p, 0, sizeof (struct scrollViewCreateParams)); + p.DocumentView = e->tv; + // this is what Interface Builder sets it to + p.BackgroundColor = [NSColor colorWithCalibratedWhite:1.0 alpha:1.0]; + p.DrawsBackground = YES; + p.Bordered = YES; + p.HScroll = hscroll; + p.VScroll = YES; + e->sv = mkScrollView(&p, &(e->d)); + + uiMultilineEntryOnChanged(e, defaultOnChanged, NULL); + + return e; +} + +uiMultilineEntry *uiNewMultilineEntry(void) +{ + return finishMultilineEntry(NO); +} + +uiMultilineEntry *uiNewNonWrappingMultilineEntry(void) +{ + return finishMultilineEntry(YES); +} diff --git a/src/libui_sdl/libui/darwin/progressbar.m b/src/libui_sdl/libui/darwin/progressbar.m new file mode 100644 index 0000000..b538228 --- /dev/null +++ b/src/libui_sdl/libui/darwin/progressbar.m @@ -0,0 +1,78 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +// NSProgressIndicator has no intrinsic width by default; use the default width in Interface Builder +#define progressIndicatorWidth 100 + +@interface intrinsicWidthNSProgressIndicator : NSProgressIndicator +@end + +@implementation intrinsicWidthNSProgressIndicator + +- (NSSize)intrinsicContentSize +{ + NSSize s; + + s = [super intrinsicContentSize]; + s.width = progressIndicatorWidth; + return s; +} + +@end + +struct uiProgressBar { + uiDarwinControl c; + NSProgressIndicator *pi; +}; + +uiDarwinControlAllDefaults(uiProgressBar, pi) + +int uiProgressBarValue(uiProgressBar *p) +{ + if ([p->pi isIndeterminate]) + return -1; + return [p->pi doubleValue]; +} + +void uiProgressBarSetValue(uiProgressBar *p, int value) +{ + if (value == -1) { + [p->pi setIndeterminate:YES]; + [p->pi startAnimation:p->pi]; + return; + } + + if ([p->pi isIndeterminate]) { + [p->pi setIndeterminate:NO]; + [p->pi stopAnimation:p->pi]; + } + + if (value < 0 || value > 100) + userbug("Value %d out of range for a uiProgressBar.", value); + + // on 10.8 there's an animation when the progress bar increases, just like with Aero + if (value == 100) { + [p->pi setMaxValue:101]; + [p->pi setDoubleValue:101]; + [p->pi setDoubleValue:100]; + [p->pi setMaxValue:100]; + return; + } + [p->pi setDoubleValue:((double) (value + 1))]; + [p->pi setDoubleValue:((double) value)]; +} + +uiProgressBar *uiNewProgressBar(void) +{ + uiProgressBar *p; + + uiDarwinNewControl(uiProgressBar, p); + + p->pi = [[intrinsicWidthNSProgressIndicator alloc] initWithFrame:NSZeroRect]; + [p->pi setControlSize:NSRegularControlSize]; + [p->pi setBezeled:YES]; + [p->pi setStyle:NSProgressIndicatorBarStyle]; + [p->pi setIndeterminate:NO]; + + return p; +} diff --git a/src/libui_sdl/libui/darwin/radiobuttons.m b/src/libui_sdl/libui/darwin/radiobuttons.m new file mode 100644 index 0000000..25d773c --- /dev/null +++ b/src/libui_sdl/libui/darwin/radiobuttons.m @@ -0,0 +1,207 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +// TODO resizing the controlgallery vertically causes the third button to still resize :| + +// In the old days you would use a NSMatrix for this; as of OS X 10.8 this was deprecated and now you need just a bunch of NSButtons with the same superview AND same action method. +// This is documented on the NSMatrix page, but the rest of the OS X documentation says to still use NSMatrix. +// NSMatrix has weird quirks anyway... + +// LONGTERM 6 units of spacing between buttons, as suggested by Interface Builder? + +@interface radioButtonsDelegate : NSObject { + uiRadioButtons *libui_r; +} +- (id)initWithR:(uiRadioButtons *)r; +- (IBAction)onClicked:(id)sender; +@end + +struct uiRadioButtons { + uiDarwinControl c; + NSView *view; + NSMutableArray *buttons; + NSMutableArray *constraints; + NSLayoutConstraint *lastv; + radioButtonsDelegate *delegate; + void (*onSelected)(uiRadioButtons *, void *); + void *onSelectedData; +}; + +@implementation radioButtonsDelegate + +- (id)initWithR:(uiRadioButtons *)r +{ + self = [super init]; + if (self) + self->libui_r = r; + return self; +} + +- (IBAction)onClicked:(id)sender +{ + uiRadioButtons *r = self->libui_r; + + (*(r->onSelected))(r, r->onSelectedData); +} + +@end + +uiDarwinControlAllDefaultsExceptDestroy(uiRadioButtons, view) + +static void defaultOnSelected(uiRadioButtons *r, void *data) +{ + // do nothing +} + +static void uiRadioButtonsDestroy(uiControl *c) +{ + uiRadioButtons *r = uiRadioButtons(c); + NSButton *b; + + // drop the constraints + [r->view removeConstraints:r->constraints]; + [r->constraints release]; + if (r->lastv != nil) + [r->lastv release]; + // destroy the buttons + for (b in r->buttons) { + [b setTarget:nil]; + [b removeFromSuperview]; + } + [r->buttons release]; + // destroy the delegate + [r->delegate release]; + // and destroy ourselves + [r->view release]; + uiFreeControl(uiControl(r)); +} + +static NSButton *buttonAt(uiRadioButtons *r, int n) +{ + return (NSButton *) [r->buttons objectAtIndex:n]; +} + +void uiRadioButtonsAppend(uiRadioButtons *r, const char *text) +{ + NSButton *b, *b2; + NSLayoutConstraint *constraint; + + b = [[NSButton alloc] initWithFrame:NSZeroRect]; + [b setTitle:toNSString(text)]; + [b setButtonType:NSRadioButton]; + // doesn't seem to have an associated bezel style + [b setBordered:NO]; + [b setTransparent:NO]; + uiDarwinSetControlFont(b, NSRegularControlSize); + [b setTranslatesAutoresizingMaskIntoConstraints:NO]; + + [b setTarget:r->delegate]; + [b setAction:@selector(onClicked:)]; + + [r->buttons addObject:b]; + [r->view addSubview:b]; + + // pin horizontally to the edges of the superview + constraint = mkConstraint(b, NSLayoutAttributeLeading, + NSLayoutRelationEqual, + r->view, NSLayoutAttributeLeading, + 1, 0, + @"uiRadioButtons button leading constraint"); + [r->view addConstraint:constraint]; + [r->constraints addObject:constraint]; + constraint = mkConstraint(b, NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + r->view, NSLayoutAttributeTrailing, + 1, 0, + @"uiRadioButtons button trailing constraint"); + [r->view addConstraint:constraint]; + [r->constraints addObject:constraint]; + + // if this is the first view, pin it to the top + // otherwise pin to the bottom of the last + if ([r->buttons count] == 1) + constraint = mkConstraint(b, NSLayoutAttributeTop, + NSLayoutRelationEqual, + r->view, NSLayoutAttributeTop, + 1, 0, + @"uiRadioButtons first button top constraint"); + else { + b2 = buttonAt(r, [r->buttons count] - 2); + constraint = mkConstraint(b, NSLayoutAttributeTop, + NSLayoutRelationEqual, + b2, NSLayoutAttributeBottom, + 1, 0, + @"uiRadioButtons non-first button top constraint"); + } + [r->view addConstraint:constraint]; + [r->constraints addObject:constraint]; + + // if there is a previous bottom constraint, remove it + if (r->lastv != nil) { + [r->view removeConstraint:r->lastv]; + [r->constraints removeObject:r->lastv]; + [r->lastv release]; + } + + // and make the new bottom constraint + r->lastv = mkConstraint(b, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + r->view, NSLayoutAttributeBottom, + 1, 0, + @"uiRadioButtons last button bottom constraint"); + [r->view addConstraint:r->lastv]; + [r->constraints addObject:r->lastv]; + [r->lastv retain]; +} + +int uiRadioButtonsSelected(uiRadioButtons *r) +{ + NSButton *b; + NSUInteger i; + + for (i = 0; i < [r->buttons count]; i++) { + b = (NSButton *) [r->buttons objectAtIndex:i]; + if ([b state] == NSOnState) + return i; + } + return -1; +} + +void uiRadioButtonsSetSelected(uiRadioButtons *r, int n) +{ + NSButton *b; + NSInteger state; + + state = NSOnState; + if (n == -1) { + n = uiRadioButtonsSelected(r); + if (n == -1) // from nothing to nothing; do nothing + return; + state = NSOffState; + } + b = (NSButton *) [r->buttons objectAtIndex:n]; + [b setState:state]; +} + +void uiRadioButtonsOnSelected(uiRadioButtons *r, void (*f)(uiRadioButtons *, void *), void *data) +{ + r->onSelected = f; + r->onSelectedData = data; +} + +uiRadioButtons *uiNewRadioButtons(void) +{ + uiRadioButtons *r; + + uiDarwinNewControl(uiRadioButtons, r); + + r->view = [[NSView alloc] initWithFrame:NSZeroRect]; + r->buttons = [NSMutableArray new]; + r->constraints = [NSMutableArray new]; + + r->delegate = [[radioButtonsDelegate alloc] initWithR:r]; + + uiRadioButtonsOnSelected(r, defaultOnSelected, NULL); + + return r; +} diff --git a/src/libui_sdl/libui/darwin/scrollview.m b/src/libui_sdl/libui/darwin/scrollview.m new file mode 100644 index 0000000..b0b4040 --- /dev/null +++ b/src/libui_sdl/libui/darwin/scrollview.m @@ -0,0 +1,61 @@ +// 27 may 2016 +#include "uipriv_darwin.h" + +// see http://stackoverflow.com/questions/37979445/how-do-i-properly-set-up-a-scrolling-nstableview-using-auto-layout-what-ive-tr for why we don't use auto layout +// TODO do the same with uiGroup and uiTab? + +struct scrollViewData { + BOOL hscroll; + BOOL vscroll; +}; + +NSScrollView *mkScrollView(struct scrollViewCreateParams *p, struct scrollViewData **dout) +{ + NSScrollView *sv; + NSBorderType border; + struct scrollViewData *d; + + sv = [[NSScrollView alloc] initWithFrame:NSZeroRect]; + if (p->BackgroundColor != nil) + [sv setBackgroundColor:p->BackgroundColor]; + [sv setDrawsBackground:p->DrawsBackground]; + border = NSNoBorder; + if (p->Bordered) + border = NSBezelBorder; + // document view seems to set the cursor properly + [sv setBorderType:border]; + [sv setAutohidesScrollers:YES]; + [sv setHasHorizontalRuler:NO]; + [sv setHasVerticalRuler:NO]; + [sv setRulersVisible:NO]; + [sv setScrollerKnobStyle:NSScrollerKnobStyleDefault]; + // the scroller style is documented as being set by default for us + // LONGTERM verify line and page for programmatically created NSTableView + [sv setScrollsDynamically:YES]; + [sv setFindBarPosition:NSScrollViewFindBarPositionAboveContent]; + [sv setUsesPredominantAxisScrolling:NO]; + [sv setHorizontalScrollElasticity:NSScrollElasticityAutomatic]; + [sv setVerticalScrollElasticity:NSScrollElasticityAutomatic]; + [sv setAllowsMagnification:NO]; + + [sv setDocumentView:p->DocumentView]; + d = uiNew(struct scrollViewData); + scrollViewSetScrolling(sv, d, p->HScroll, p->VScroll); + + *dout = d; + return sv; +} + +// based on http://blog.bjhomer.com/2014/08/nsscrollview-and-autolayout.html because (as pointed out there) Apple's official guide is really only for iOS +void scrollViewSetScrolling(NSScrollView *sv, struct scrollViewData *d, BOOL hscroll, BOOL vscroll) +{ + d->hscroll = hscroll; + [sv setHasHorizontalScroller:d->hscroll]; + d->vscroll = vscroll; + [sv setHasVerticalScroller:d->vscroll]; +} + +void scrollViewFreeData(NSScrollView *sv, struct scrollViewData *d) +{ + uiFree(d); +} diff --git a/src/libui_sdl/libui/darwin/separator.m b/src/libui_sdl/libui/darwin/separator.m new file mode 100644 index 0000000..a37a376 --- /dev/null +++ b/src/libui_sdl/libui/darwin/separator.m @@ -0,0 +1,45 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +// TODO make this intrinsic +#define separatorWidth 96 +#define separatorHeight 96 + +struct uiSeparator { + uiDarwinControl c; + NSBox *box; +}; + +uiDarwinControlAllDefaults(uiSeparator, box) + +uiSeparator *uiNewHorizontalSeparator(void) +{ + uiSeparator *s; + + uiDarwinNewControl(uiSeparator, s); + + // make the initial width >= initial height to force horizontal + s->box = [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, 100, 1)]; + [s->box setBoxType:NSBoxSeparator]; + [s->box setBorderType:NSGrooveBorder]; + [s->box setTransparent:NO]; + [s->box setTitlePosition:NSNoTitle]; + + return s; +} + +uiSeparator *uiNewVerticalSeparator(void) +{ + uiSeparator *s; + + uiDarwinNewControl(uiSeparator, s); + + // make the initial height >= initial width to force vertical + s->box = [[NSBox alloc] initWithFrame:NSMakeRect(0, 0, 1, 100)]; + [s->box setBoxType:NSBoxSeparator]; + [s->box setBorderType:NSGrooveBorder]; + [s->box setTransparent:NO]; + [s->box setTitlePosition:NSNoTitle]; + + return s; +} diff --git a/src/libui_sdl/libui/darwin/slider.m b/src/libui_sdl/libui/darwin/slider.m new file mode 100644 index 0000000..f00da50 --- /dev/null +++ b/src/libui_sdl/libui/darwin/slider.m @@ -0,0 +1,147 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +// Horizontal sliders have no intrinsic width; we'll use the default Interface Builder width for them. +// This will also be used for the initial frame size, to ensure the slider is always horizontal (see below). +#define sliderWidth 92 + +@interface libui_intrinsicWidthNSSlider : NSSlider +@end + +@implementation libui_intrinsicWidthNSSlider + +- (NSSize)intrinsicContentSize +{ + NSSize s; + + s = [super intrinsicContentSize]; + s.width = sliderWidth; + return s; +} + +@end + +struct uiSlider { + uiDarwinControl c; + NSSlider *slider; + void (*onChanged)(uiSlider *, void *); + void *onChangedData; +}; + +@interface sliderDelegateClass : NSObject { + struct mapTable *sliders; +} +- (IBAction)onChanged:(id)sender; +- (void)registerSlider:(uiSlider *)b; +- (void)unregisterSlider:(uiSlider *)b; +@end + +@implementation sliderDelegateClass + +- (id)init +{ + self = [super init]; + if (self) + self->sliders = newMap(); + return self; +} + +- (void)dealloc +{ + mapDestroy(self->sliders); + [super dealloc]; +} + +- (IBAction)onChanged:(id)sender +{ + uiSlider *s; + + s = (uiSlider *) mapGet(self->sliders, sender); + (*(s->onChanged))(s, s->onChangedData); +} + +- (void)registerSlider:(uiSlider *)s +{ + mapSet(self->sliders, s->slider, s); + [s->slider setTarget:self]; + [s->slider setAction:@selector(onChanged:)]; +} + +- (void)unregisterSlider:(uiSlider *)s +{ + [s->slider setTarget:nil]; + mapDelete(self->sliders, s->slider); +} + +@end + +static sliderDelegateClass *sliderDelegate = nil; + +uiDarwinControlAllDefaultsExceptDestroy(uiSlider, slider) + +static void uiSliderDestroy(uiControl *c) +{ + uiSlider *s = uiSlider(c); + + [sliderDelegate unregisterSlider:s]; + [s->slider release]; + uiFreeControl(uiControl(s)); +} + +int uiSliderValue(uiSlider *s) +{ + return [s->slider integerValue]; +} + +void uiSliderSetValue(uiSlider *s, int value) +{ + [s->slider setIntegerValue:value]; +} + +void uiSliderOnChanged(uiSlider *s, void (*f)(uiSlider *, void *), void *data) +{ + s->onChanged = f; + s->onChangedData = data; +} + +static void defaultOnChanged(uiSlider *s, void *data) +{ + // do nothing +} + +uiSlider *uiNewSlider(int min, int max) +{ + uiSlider *s; + NSSliderCell *cell; + int temp; + + if (min >= max) { + temp = min; + min = max; + max = temp; + } + + uiDarwinNewControl(uiSlider, s); + + // a horizontal slider is defined as one where the width > height, not by a flag + // to be safe, don't use NSZeroRect, but make it horizontal from the get-go + s->slider = [[libui_intrinsicWidthNSSlider alloc] + initWithFrame:NSMakeRect(0, 0, sliderWidth, 2)]; + [s->slider setMinValue:min]; + [s->slider setMaxValue:max]; + [s->slider setAllowsTickMarkValuesOnly:NO]; + [s->slider setNumberOfTickMarks:0]; + [s->slider setTickMarkPosition:NSTickMarkAbove]; + + cell = (NSSliderCell *) [s->slider cell]; + [cell setSliderType:NSLinearSlider]; + + if (sliderDelegate == nil) { + sliderDelegate = [[sliderDelegateClass new] autorelease]; + [delegates addObject:sliderDelegate]; + } + [sliderDelegate registerSlider:s]; + uiSliderOnChanged(s, defaultOnChanged, NULL); + + return s; +} diff --git a/src/libui_sdl/libui/darwin/spinbox.m b/src/libui_sdl/libui/darwin/spinbox.m new file mode 100644 index 0000000..73474d0 --- /dev/null +++ b/src/libui_sdl/libui/darwin/spinbox.m @@ -0,0 +1,214 @@ +// 14 august 2015 +#import "uipriv_darwin.h" + +@interface libui_spinbox : NSView<NSTextFieldDelegate> { + NSTextField *tf; + NSNumberFormatter *formatter; + NSStepper *stepper; + + NSInteger value; + NSInteger minimum; + NSInteger maximum; + + uiSpinbox *spinbox; +} +- (id)initWithFrame:(NSRect)r spinbox:(uiSpinbox *)sb; +// see https://github.com/andlabs/ui/issues/82 +- (NSInteger)libui_value; +- (void)libui_setValue:(NSInteger)val; +- (void)setMinimum:(NSInteger)min; +- (void)setMaximum:(NSInteger)max; +- (IBAction)stepperClicked:(id)sender; +- (void)controlTextDidChange:(NSNotification *)note; +@end + +struct uiSpinbox { + uiDarwinControl c; + libui_spinbox *spinbox; + void (*onChanged)(uiSpinbox *, void *); + void *onChangedData; +}; + +// yes folks, this varies by operating system! woo! +// 10.10 started drawing the NSStepper one point too low, so we have to fix it up conditionally +// TODO test this; we'll probably have to substitute 10_9 +static CGFloat stepperYDelta(void) +{ + // via https://developer.apple.com/library/mac/releasenotes/AppKit/RN-AppKit/ + if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_9) + return 0; + return -1; +} + +@implementation libui_spinbox + +- (id)initWithFrame:(NSRect)r spinbox:(uiSpinbox *)sb +{ + self = [super initWithFrame:r]; + if (self) { + self->tf = newEditableTextField(); + [self->tf setTranslatesAutoresizingMaskIntoConstraints:NO]; + + self->formatter = [NSNumberFormatter new]; + [self->formatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + [self->formatter setLocalizesFormat:NO]; + [self->formatter setUsesGroupingSeparator:NO]; + [self->formatter setHasThousandSeparators:NO]; + [self->formatter setAllowsFloats:NO]; + [self->tf setFormatter:self->formatter]; + + self->stepper = [[NSStepper alloc] initWithFrame:NSZeroRect]; + [self->stepper setIncrement:1]; + [self->stepper setValueWraps:NO]; + [self->stepper setAutorepeat:YES]; // hold mouse button to step repeatedly + [self->stepper setTranslatesAutoresizingMaskIntoConstraints:NO]; + + [self->tf setDelegate:self]; + [self->stepper setTarget:self]; + [self->stepper setAction:@selector(stepperClicked:)]; + + [self addSubview:self->tf]; + [self addSubview:self->stepper]; + + [self addConstraint:mkConstraint(self->tf, NSLayoutAttributeLeading, + NSLayoutRelationEqual, + self, NSLayoutAttributeLeading, + 1, 0, + @"uiSpinbox left edge")]; + [self addConstraint:mkConstraint(self->stepper, NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + self, NSLayoutAttributeTrailing, + 1, 0, + @"uiSpinbox right edge")]; + [self addConstraint:mkConstraint(self->tf, NSLayoutAttributeTop, + NSLayoutRelationEqual, + self, NSLayoutAttributeTop, + 1, 0, + @"uiSpinbox top edge text field")]; + [self addConstraint:mkConstraint(self->tf, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + self, NSLayoutAttributeBottom, + 1, 0, + @"uiSpinbox bottom edge text field")]; + [self addConstraint:mkConstraint(self->stepper, NSLayoutAttributeTop, + NSLayoutRelationEqual, + self, NSLayoutAttributeTop, + 1, stepperYDelta(), + @"uiSpinbox top edge stepper")]; + [self addConstraint:mkConstraint(self->stepper, NSLayoutAttributeBottom, + NSLayoutRelationEqual, + self, NSLayoutAttributeBottom, + 1, stepperYDelta(), + @"uiSpinbox bottom edge stepper")]; + [self addConstraint:mkConstraint(self->tf, NSLayoutAttributeTrailing, + NSLayoutRelationEqual, + self->stepper, NSLayoutAttributeLeading, + 1, -3, // arbitrary amount; good enough visually (and it seems to match NSDatePicker too, at least on 10.11, which is even better) + @"uiSpinbox space between text field and stepper")]; + + self->spinbox = sb; + } + return self; +} + +- (void)dealloc +{ + [self->tf setDelegate:nil]; + [self->tf removeFromSuperview]; + [self->tf release]; + [self->formatter release]; + [self->stepper setTarget:nil]; + [self->stepper removeFromSuperview]; + [self->stepper release]; + [super dealloc]; +} + +- (NSInteger)libui_value +{ + return self->value; +} + +- (void)libui_setValue:(NSInteger)val +{ + self->value = val; + if (self->value < self->minimum) + self->value = self->minimum; + if (self->value > self->maximum) + self->value = self->maximum; + [self->tf setIntegerValue:self->value]; + [self->stepper setIntegerValue:self->value]; +} + +- (void)setMinimum:(NSInteger)min +{ + self->minimum = min; + [self->formatter setMinimum:[NSNumber numberWithInteger:self->minimum]]; + [self->stepper setMinValue:((double) (self->minimum))]; +} + +- (void)setMaximum:(NSInteger)max +{ + self->maximum = max; + [self->formatter setMaximum:[NSNumber numberWithInteger:self->maximum]]; + [self->stepper setMaxValue:((double) (self->maximum))]; +} + +- (IBAction)stepperClicked:(id)sender +{ + [self libui_setValue:[self->stepper integerValue]]; + (*(self->spinbox->onChanged))(self->spinbox, self->spinbox->onChangedData); +} + +- (void)controlTextDidChange:(NSNotification *)note +{ + [self libui_setValue:[self->tf integerValue]]; + (*(self->spinbox->onChanged))(self->spinbox, self->spinbox->onChangedData); +} + +@end + +uiDarwinControlAllDefaults(uiSpinbox, spinbox) + +int uiSpinboxValue(uiSpinbox *s) +{ + return [s->spinbox libui_value]; +} + +void uiSpinboxSetValue(uiSpinbox *s, int value) +{ + [s->spinbox libui_setValue:value]; +} + +void uiSpinboxOnChanged(uiSpinbox *s, void (*f)(uiSpinbox *, void *), void *data) +{ + s->onChanged = f; + s->onChangedData = data; +} + +static void defaultOnChanged(uiSpinbox *s, void *data) +{ + // do nothing +} + +uiSpinbox *uiNewSpinbox(int min, int max) +{ + uiSpinbox *s; + int temp; + + if (min >= max) { + temp = min; + min = max; + max = temp; + } + + uiDarwinNewControl(uiSpinbox, s); + + s->spinbox = [[libui_spinbox alloc] initWithFrame:NSZeroRect spinbox:s]; + [s->spinbox setMinimum:min]; + [s->spinbox setMaximum:max]; + [s->spinbox libui_setValue:min]; + + uiSpinboxOnChanged(s, defaultOnChanged, NULL); + + return s; +} diff --git a/src/libui_sdl/libui/darwin/stddialogs.m b/src/libui_sdl/libui/darwin/stddialogs.m new file mode 100644 index 0000000..57ce959 --- /dev/null +++ b/src/libui_sdl/libui/darwin/stddialogs.m @@ -0,0 +1,123 @@ +// 26 june 2015 +#import "uipriv_darwin.h" + +// LONGTERM restructure this whole file +// LONGTERM explicitly document this works as we want +// LONGTERM note that font and color buttons also do this + +#define windowWindow(w) ((NSWindow *) uiControlHandle(uiControl(w))) + +// source of code modal logic: http://stackoverflow.com/questions/604768/wait-for-nsalert-beginsheetmodalforwindow + +// note: whether extensions are actually shown depends on a user setting in Finder; we can't control it here +static void setupSavePanel(NSSavePanel *s) +{ + [s setCanCreateDirectories:YES]; + [s setShowsHiddenFiles:YES]; + [s setExtensionHidden:NO]; + [s setCanSelectHiddenExtension:NO]; + [s setTreatsFilePackagesAsDirectories:YES]; +} + +static char *runSavePanel(NSWindow *parent, NSSavePanel *s) +{ + char *filename; + + [s beginSheetModalForWindow:parent completionHandler:^(NSInteger result) { + [realNSApp() stopModalWithCode:result]; + }]; + if ([realNSApp() runModalForWindow:s] != NSFileHandlingPanelOKButton) + return NULL; + filename = uiDarwinNSStringToText([[s URL] path]); + return filename; +} + +char *uiOpenFile(uiWindow *parent) +{ + NSOpenPanel *o; + + o = [NSOpenPanel openPanel]; + [o setCanChooseFiles:YES]; + [o setCanChooseDirectories:NO]; + [o setResolvesAliases:NO]; + [o setAllowsMultipleSelection:NO]; + setupSavePanel(o); + // panel is autoreleased + return runSavePanel(windowWindow(parent), o); +} + +char *uiSaveFile(uiWindow *parent) +{ + NSSavePanel *s; + + s = [NSSavePanel savePanel]; + setupSavePanel(s); + // panel is autoreleased + return runSavePanel(windowWindow(parent), s); +} + +// I would use a completion handler for NSAlert as well, but alas NSAlert's are 10.9 and higher only +@interface libuiCodeModalAlertPanel : NSObject { + NSAlert *panel; + NSWindow *parent; +} +- (id)initWithPanel:(NSAlert *)p parent:(NSWindow *)w; +- (NSInteger)run; +- (void)panelEnded:(NSAlert *)panel result:(NSInteger)result data:(void *)data; +@end + +@implementation libuiCodeModalAlertPanel + +- (id)initWithPanel:(NSAlert *)p parent:(NSWindow *)w +{ + self = [super init]; + if (self) { + self->panel = p; + self->parent = w; + } + return self; +} + +- (NSInteger)run +{ + [self->panel beginSheetModalForWindow:self->parent + modalDelegate:self + didEndSelector:@selector(panelEnded:result:data:) + contextInfo:NULL]; + return [realNSApp() runModalForWindow:[self->panel window]]; +} + +- (void)panelEnded:(NSAlert *)panel result:(NSInteger)result data:(void *)data +{ + [realNSApp() stopModalWithCode:result]; +} + +@end + +static void msgbox(NSWindow *parent, const char *title, const char *description, NSAlertStyle style) +{ + NSAlert *a; + libuiCodeModalAlertPanel *cm; + + a = [NSAlert new]; + [a setAlertStyle:style]; + [a setShowsHelp:NO]; + [a setShowsSuppressionButton:NO]; + [a setMessageText:toNSString(title)]; + [a setInformativeText:toNSString(description)]; + [a addButtonWithTitle:@"OK"]; + cm = [[libuiCodeModalAlertPanel alloc] initWithPanel:a parent:parent]; + [cm run]; + [cm release]; + [a release]; +} + +void uiMsgBox(uiWindow *parent, const char *title, const char *description) +{ + msgbox(windowWindow(parent), title, description, NSInformationalAlertStyle); +} + +void uiMsgBoxError(uiWindow *parent, const char *title, const char *description) +{ + msgbox(windowWindow(parent), title, description, NSCriticalAlertStyle); +} diff --git a/src/libui_sdl/libui/darwin/tab.m b/src/libui_sdl/libui/darwin/tab.m new file mode 100644 index 0000000..3d2ca9f --- /dev/null +++ b/src/libui_sdl/libui/darwin/tab.m @@ -0,0 +1,292 @@ +// 15 august 2015 +#import "uipriv_darwin.h" + +// TODO need to jiggle on tab change too (second page disabled tab label initially ambiguous) + +@interface tabPage : NSObject { + struct singleChildConstraints constraints; + int margined; + NSView *view; // the NSTabViewItem view itself + NSObject *pageID; +} +@property uiControl *c; +@property NSLayoutPriority oldHorzHuggingPri; +@property NSLayoutPriority oldVertHuggingPri; +- (id)initWithView:(NSView *)v pageID:(NSObject *)o; +- (NSView *)childView; +- (void)establishChildConstraints; +- (void)removeChildConstraints; +- (int)isMargined; +- (void)setMargined:(int)m; +@end + +struct uiTab { + uiDarwinControl c; + NSTabView *tabview; + NSMutableArray *pages; + NSLayoutPriority horzHuggingPri; + NSLayoutPriority vertHuggingPri; +}; + +@implementation tabPage + +- (id)initWithView:(NSView *)v pageID:(NSObject *)o +{ + self = [super init]; + if (self != nil) { + self->view = [v retain]; + self->pageID = [o retain]; + } + return self; +} + +- (void)dealloc +{ + [self removeChildConstraints]; + [self->view release]; + [self->pageID release]; + [super dealloc]; +} + +- (NSView *)childView +{ + return (NSView *) uiControlHandle(self.c); +} + +- (void)establishChildConstraints +{ + [self removeChildConstraints]; + if (self.c == NULL) + return; + singleChildConstraintsEstablish(&(self->constraints), + self->view, [self childView], + uiDarwinControlHugsTrailingEdge(uiDarwinControl(self.c)), + uiDarwinControlHugsBottom(uiDarwinControl(self.c)), + self->margined, + @"uiTab page"); +} + +- (void)removeChildConstraints +{ + singleChildConstraintsRemove(&(self->constraints), self->view); +} + +- (int)isMargined +{ + return self->margined; +} + +- (void)setMargined:(int)m +{ + self->margined = m; + singleChildConstraintsSetMargined(&(self->constraints), self->margined); +} + +@end + +static void uiTabDestroy(uiControl *c) +{ + uiTab *t = uiTab(c); + tabPage *page; + + // first remove all tab pages so we can destroy all the children + while ([t->tabview numberOfTabViewItems] != 0) + [t->tabview removeTabViewItem:[t->tabview tabViewItemAtIndex:0]]; + // then destroy all the children + for (page in t->pages) { + [page removeChildConstraints]; + uiControlSetParent(page.c, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(page.c), nil); + uiControlDestroy(page.c); + } + // and finally destroy ourselves + [t->pages release]; + [t->tabview release]; + uiFreeControl(uiControl(t)); +} + +uiDarwinControlDefaultHandle(uiTab, tabview) +uiDarwinControlDefaultParent(uiTab, tabview) +uiDarwinControlDefaultSetParent(uiTab, tabview) +uiDarwinControlDefaultToplevel(uiTab, tabview) +uiDarwinControlDefaultVisible(uiTab, tabview) +uiDarwinControlDefaultShow(uiTab, tabview) +uiDarwinControlDefaultHide(uiTab, tabview) +uiDarwinControlDefaultEnabled(uiTab, tabview) +uiDarwinControlDefaultEnable(uiTab, tabview) +uiDarwinControlDefaultDisable(uiTab, tabview) + +static void uiTabSyncEnableState(uiDarwinControl *c, int enabled) +{ + uiTab *t = uiTab(c); + tabPage *page; + + if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(t), enabled)) + return; + for (page in t->pages) + uiDarwinControlSyncEnableState(uiDarwinControl(page.c), enabled); +} + +uiDarwinControlDefaultSetSuperview(uiTab, tabview) + +static void tabRelayout(uiTab *t) +{ + tabPage *page; + + for (page in t->pages) + [page establishChildConstraints]; + // and this gets rid of some weird issues with regards to box alignment + jiggleViewLayout(t->tabview); +} + +BOOL uiTabHugsTrailingEdge(uiDarwinControl *c) +{ + uiTab *t = uiTab(c); + + return t->horzHuggingPri < NSLayoutPriorityWindowSizeStayPut; +} + +BOOL uiTabHugsBottom(uiDarwinControl *c) +{ + uiTab *t = uiTab(c); + + return t->vertHuggingPri < NSLayoutPriorityWindowSizeStayPut; +} + +static void uiTabChildEdgeHuggingChanged(uiDarwinControl *c) +{ + uiTab *t = uiTab(c); + + tabRelayout(t); +} + +static NSLayoutPriority uiTabHuggingPriority(uiDarwinControl *c, NSLayoutConstraintOrientation orientation) +{ + uiTab *t = uiTab(c); + + if (orientation == NSLayoutConstraintOrientationHorizontal) + return t->horzHuggingPri; + return t->vertHuggingPri; +} + +static void uiTabSetHuggingPriority(uiDarwinControl *c, NSLayoutPriority priority, NSLayoutConstraintOrientation orientation) +{ + uiTab *t = uiTab(c); + + if (orientation == NSLayoutConstraintOrientationHorizontal) + t->horzHuggingPri = priority; + else + t->vertHuggingPri = priority; + uiDarwinNotifyEdgeHuggingChanged(uiDarwinControl(t)); +} + +static void uiTabChildVisibilityChanged(uiDarwinControl *c) +{ + uiTab *t = uiTab(c); + + tabRelayout(t); +} + +void uiTabAppend(uiTab *t, const char *name, uiControl *child) +{ + uiTabInsertAt(t, name, [t->pages count], child); +} + +void uiTabInsertAt(uiTab *t, const char *name, int n, uiControl *child) +{ + tabPage *page; + NSView *view; + NSTabViewItem *i; + NSObject *pageID; + + uiControlSetParent(child, uiControl(t)); + + view = [[[NSView alloc] initWithFrame:NSZeroRect] autorelease]; + // note: if we turn off the autoresizing mask, nothing shows up + uiDarwinControlSetSuperview(uiDarwinControl(child), view); + uiDarwinControlSyncEnableState(uiDarwinControl(child), uiControlEnabledToUser(uiControl(t))); + + // the documentation says these can be nil but the headers say these must not be; let's be safe and make them non-nil anyway + pageID = [NSObject new]; + page = [[[tabPage alloc] initWithView:view pageID:pageID] autorelease]; + page.c = child; + + // don't hug, just in case we're a stretchy tab + page.oldHorzHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(page.c), NSLayoutConstraintOrientationHorizontal); + page.oldVertHuggingPri = uiDarwinControlHuggingPriority(uiDarwinControl(page.c), NSLayoutConstraintOrientationVertical); + uiDarwinControlSetHuggingPriority(uiDarwinControl(page.c), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationHorizontal); + uiDarwinControlSetHuggingPriority(uiDarwinControl(page.c), NSLayoutPriorityDefaultLow, NSLayoutConstraintOrientationVertical); + + [t->pages insertObject:page atIndex:n]; + + i = [[[NSTabViewItem alloc] initWithIdentifier:pageID] autorelease]; + [i setLabel:toNSString(name)]; + [i setView:view]; + [t->tabview insertTabViewItem:i atIndex:n]; + + tabRelayout(t); +} + +void uiTabDelete(uiTab *t, int n) +{ + tabPage *page; + uiControl *child; + NSTabViewItem *i; + + page = (tabPage *) [t->pages objectAtIndex:n]; + + uiDarwinControlSetHuggingPriority(uiDarwinControl(page.c), page.oldHorzHuggingPri, NSLayoutConstraintOrientationHorizontal); + uiDarwinControlSetHuggingPriority(uiDarwinControl(page.c), page.oldVertHuggingPri, NSLayoutConstraintOrientationVertical); + + child = page.c; + [page removeChildConstraints]; + [t->pages removeObjectAtIndex:n]; + + uiControlSetParent(child, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(child), nil); + + i = [t->tabview tabViewItemAtIndex:n]; + [t->tabview removeTabViewItem:i]; + + tabRelayout(t); +} + +int uiTabNumPages(uiTab *t) +{ + return [t->pages count]; +} + +int uiTabMargined(uiTab *t, int n) +{ + tabPage *page; + + page = (tabPage *) [t->pages objectAtIndex:n]; + return [page isMargined]; +} + +void uiTabSetMargined(uiTab *t, int n, int margined) +{ + tabPage *page; + + page = (tabPage *) [t->pages objectAtIndex:n]; + [page setMargined:margined]; +} + +uiTab *uiNewTab(void) +{ + uiTab *t; + + uiDarwinNewControl(uiTab, t); + + t->tabview = [[NSTabView alloc] initWithFrame:NSZeroRect]; + // also good for NSTabView (same selector and everything) + uiDarwinSetControlFont((NSControl *) (t->tabview), NSRegularControlSize); + + t->pages = [NSMutableArray new]; + + // default to low hugging to not hug edges + t->horzHuggingPri = NSLayoutPriorityDefaultLow; + t->vertHuggingPri = NSLayoutPriorityDefaultLow; + + return t; +} diff --git a/src/libui_sdl/libui/darwin/text.m b/src/libui_sdl/libui/darwin/text.m new file mode 100644 index 0000000..f0d3dab --- /dev/null +++ b/src/libui_sdl/libui/darwin/text.m @@ -0,0 +1,19 @@ +// 10 april 2015 +#import "uipriv_darwin.h" + +char *uiDarwinNSStringToText(NSString *s) +{ + char *out; + + out = strdup([s UTF8String]); + if (out == NULL) { + fprintf(stderr, "memory exhausted in uiDarwinNSStringToText()\n"); + abort(); + } + return out; +} + +void uiFreeText(char *s) +{ + free(s); +} diff --git a/src/libui_sdl/libui/darwin/uipriv_darwin.h b/src/libui_sdl/libui/darwin/uipriv_darwin.h new file mode 100644 index 0000000..6bca87b --- /dev/null +++ b/src/libui_sdl/libui/darwin/uipriv_darwin.h @@ -0,0 +1,146 @@ +// 6 january 2015 +#define MAC_OS_X_VERSION_MIN_REQUIRED MAC_OS_X_VERSION_10_8 +#define MAC_OS_X_VERSION_MAX_ALLOWED MAC_OS_X_VERSION_10_8 +#import <Cocoa/Cocoa.h> +#import "../ui.h" +#import "../ui_darwin.h" +#import "../common/uipriv.h" + +#if __has_feature(objc_arc) +#error Sorry, libui cannot be compiled with ARC. +#endif + +#define toNSString(str) [NSString stringWithUTF8String:(str)] +#define fromNSString(str) [(str) UTF8String] + +#ifndef NSAppKitVersionNumber10_9 +#define NSAppKitVersionNumber10_9 1265 +#endif + +/*TODO remove this*/typedef struct uiImage uiImage; + +// menu.m +@interface menuManager : NSObject { + struct mapTable *items; + BOOL hasQuit; + BOOL hasPreferences; + BOOL hasAbout; +} +@property (strong) NSMenuItem *quitItem; +@property (strong) NSMenuItem *preferencesItem; +@property (strong) NSMenuItem *aboutItem; +// NSMenuValidation is only informal +- (BOOL)validateMenuItem:(NSMenuItem *)item; +- (NSMenu *)makeMenubar; +@end +extern void finalizeMenus(void); +extern void uninitMenus(void); + +// main.m +@interface applicationClass : NSApplication +@end +// this is needed because NSApp is of type id, confusing clang +#define realNSApp() ((applicationClass *) NSApp) +@interface appDelegate : NSObject <NSApplicationDelegate> +@property (strong) menuManager *menuManager; +@end +#define appDelegate() ((appDelegate *) [realNSApp() delegate]) +struct nextEventArgs { + NSEventMask mask; + NSDate *duration; + // LONGTERM no NSRunLoopMode? + NSString *mode; + BOOL dequeue; +}; +extern int mainStep(struct nextEventArgs *nea, BOOL (^interceptEvent)(NSEvent *)); + +// util.m +extern void disableAutocorrect(NSTextView *); + +// entry.m +extern void finishNewTextField(NSTextField *, BOOL); +extern NSTextField *newEditableTextField(void); + +// window.m +@interface libuiNSWindow : NSWindow +- (void)libui_doMove:(NSEvent *)initialEvent; +- (void)libui_doResize:(NSEvent *)initialEvent on:(uiWindowResizeEdge)edge; +@end +extern uiWindow *windowFromNSWindow(NSWindow *); + +// alloc.m +extern NSMutableArray *delegates; +extern void initAlloc(void); +extern void uninitAlloc(void); + +// autolayout.m +extern NSLayoutConstraint *mkConstraint(id view1, NSLayoutAttribute attr1, NSLayoutRelation relation, id view2, NSLayoutAttribute attr2, CGFloat multiplier, CGFloat c, NSString *desc); +extern void jiggleViewLayout(NSView *view); +struct singleChildConstraints { + NSLayoutConstraint *leadingConstraint; + NSLayoutConstraint *topConstraint; + NSLayoutConstraint *trailingConstraintGreater; + NSLayoutConstraint *trailingConstraintEqual; + NSLayoutConstraint *bottomConstraintGreater; + NSLayoutConstraint *bottomConstraintEqual; +}; +extern void singleChildConstraintsEstablish(struct singleChildConstraints *c, NSView *contentView, NSView *childView, BOOL hugsTrailing, BOOL hugsBottom, int margined, NSString *desc); +extern void singleChildConstraintsRemove(struct singleChildConstraints *c, NSView *cv); +extern void singleChildConstraintsSetMargined(struct singleChildConstraints *c, int margined); + +// map.m +extern struct mapTable *newMap(void); +extern void mapDestroy(struct mapTable *m); +extern void *mapGet(struct mapTable *m, void *key); +extern void mapSet(struct mapTable *m, void *key, void *value); +extern void mapDelete(struct mapTable *m, void *key); +extern void mapWalk(struct mapTable *m, void (*f)(void *key, void *value)); +extern void mapReset(struct mapTable *m); + +// area.m +extern int sendAreaEvents(NSEvent *); + +// areaevents.m +extern BOOL fromKeycode(unsigned short keycode, uiAreaKeyEvent *ke); +extern BOOL keycodeModifier(unsigned short keycode, uiModifiers *mod); + +// draw.m +extern uiDrawContext *newContext(CGContextRef, CGFloat); +extern void freeContext(uiDrawContext *); + +// drawtext.m +extern uiDrawTextFont *mkTextFont(CTFontRef f, BOOL retain); +extern uiDrawTextFont *mkTextFontFromNSFont(NSFont *f); +extern void doDrawText(CGContextRef c, CGFloat cheight, double x, double y, uiDrawTextLayout *layout); + +// fontbutton.m +extern BOOL fontButtonInhibitSendAction(SEL sel, id from, id to); +extern BOOL fontButtonOverrideTargetForAction(SEL sel, id from, id to, id *override); +extern void setupFontPanel(void); + +// colorbutton.m +extern BOOL colorButtonInhibitSendAction(SEL sel, id from, id to); + +// scrollview.m +struct scrollViewCreateParams { + NSView *DocumentView; + NSColor *BackgroundColor; + BOOL DrawsBackground; + BOOL Bordered; + BOOL HScroll; + BOOL VScroll; +}; +struct scrollViewData; +extern NSScrollView *mkScrollView(struct scrollViewCreateParams *p, struct scrollViewData **dout); +extern void scrollViewSetScrolling(NSScrollView *sv, struct scrollViewData *d, BOOL hscroll, BOOL vscroll); +extern void scrollViewFreeData(NSScrollView *sv, struct scrollViewData *d); + +// label.m +extern NSTextField *newLabel(NSString *str); + +// image.m +extern NSImage *imageImage(uiImage *); + +// winmoveresize.m +extern void doManualMove(NSWindow *w, NSEvent *initialEvent); +extern void doManualResize(NSWindow *w, NSEvent *initialEvent, uiWindowResizeEdge edge); diff --git a/src/libui_sdl/libui/darwin/util.m b/src/libui_sdl/libui/darwin/util.m new file mode 100644 index 0000000..ab87390 --- /dev/null +++ b/src/libui_sdl/libui/darwin/util.m @@ -0,0 +1,15 @@ +// 7 april 2015 +#import "uipriv_darwin.h" + +// LONGTERM do we really want to do this? make it an option? +void disableAutocorrect(NSTextView *tv) +{ + [tv setEnabledTextCheckingTypes:0]; + [tv setAutomaticDashSubstitutionEnabled:NO]; + // don't worry about automatic data detection; it won't change stringValue (thanks pretty_function in irc.freenode.net/#macdev) + [tv setAutomaticSpellingCorrectionEnabled:NO]; + [tv setAutomaticTextReplacementEnabled:NO]; + [tv setAutomaticQuoteSubstitutionEnabled:NO]; + [tv setAutomaticLinkDetectionEnabled:NO]; + [tv setSmartInsertDeleteEnabled:NO]; +} diff --git a/src/libui_sdl/libui/darwin/window.m b/src/libui_sdl/libui/darwin/window.m new file mode 100644 index 0000000..97c22e6 --- /dev/null +++ b/src/libui_sdl/libui/darwin/window.m @@ -0,0 +1,407 @@ +// 15 august 2015 +#import "uipriv_darwin.h" + +#define defaultStyleMask (NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask) + +struct uiWindow { + uiDarwinControl c; + NSWindow *window; + uiControl *child; + int margined; + int (*onClosing)(uiWindow *, void *); + void *onClosingData; + struct singleChildConstraints constraints; + void (*onContentSizeChanged)(uiWindow *, void *); + void *onContentSizeChangedData; + BOOL suppressSizeChanged; + int fullscreen; + int borderless; +}; + +@implementation libuiNSWindow + +- (void)libui_doMove:(NSEvent *)initialEvent +{ + doManualMove(self, initialEvent); +} + +- (void)libui_doResize:(NSEvent *)initialEvent on:(uiWindowResizeEdge)edge +{ + doManualResize(self, initialEvent, edge); +} + +@end + +@interface windowDelegateClass : NSObject<NSWindowDelegate> { + struct mapTable *windows; +} +- (BOOL)windowShouldClose:(id)sender; +- (void)windowDidResize:(NSNotification *)note; +- (void)windowDidEnterFullScreen:(NSNotification *)note; +- (void)windowDidExitFullScreen:(NSNotification *)note; +- (void)registerWindow:(uiWindow *)w; +- (void)unregisterWindow:(uiWindow *)w; +- (uiWindow *)lookupWindow:(NSWindow *)w; +@end + +@implementation windowDelegateClass + +- (id)init +{ + self = [super init]; + if (self) + self->windows = newMap(); + return self; +} + +- (void)dealloc +{ + mapDestroy(self->windows); + [super dealloc]; +} + +- (BOOL)windowShouldClose:(id)sender +{ + uiWindow *w; + + w = [self lookupWindow:((NSWindow *) sender)]; + // w should not be NULL; we are only the delegate of registered windows + if ((*(w->onClosing))(w, w->onClosingData)) + uiControlDestroy(uiControl(w)); + return NO; +} + +- (void)windowDidResize:(NSNotification *)note +{ + uiWindow *w; + + w = [self lookupWindow:((NSWindow *) [note object])]; + if (!w->suppressSizeChanged) + (*(w->onContentSizeChanged))(w, w->onContentSizeChangedData); +} + +- (void)windowDidEnterFullScreen:(NSNotification *)note +{ + uiWindow *w; + + w = [self lookupWindow:((NSWindow *) [note object])]; + if (!w->suppressSizeChanged) + w->fullscreen = 1; +} + +- (void)windowDidExitFullScreen:(NSNotification *)note +{ + uiWindow *w; + + w = [self lookupWindow:((NSWindow *) [note object])]; + if (!w->suppressSizeChanged) + w->fullscreen = 0; +} + +- (void)registerWindow:(uiWindow *)w +{ + mapSet(self->windows, w->window, w); + [w->window setDelegate:self]; +} + +- (void)unregisterWindow:(uiWindow *)w +{ + [w->window setDelegate:nil]; + mapDelete(self->windows, w->window); +} + +- (uiWindow *)lookupWindow:(NSWindow *)w +{ + uiWindow *v; + + v = uiWindow(mapGet(self->windows, w)); + // this CAN (and IS ALLOWED TO) return NULL, just in case we're called with some OS X-provided window as the key window + return v; +} + +@end + +static windowDelegateClass *windowDelegate = nil; + +static void removeConstraints(uiWindow *w) +{ + NSView *cv; + + cv = [w->window contentView]; + singleChildConstraintsRemove(&(w->constraints), cv); +} + +static void uiWindowDestroy(uiControl *c) +{ + uiWindow *w = uiWindow(c); + + // hide the window + [w->window orderOut:w->window]; + removeConstraints(w); + if (w->child != NULL) { + uiControlSetParent(w->child, NULL); + uiDarwinControlSetSuperview(uiDarwinControl(w->child), nil); + uiControlDestroy(w->child); + } + [windowDelegate unregisterWindow:w]; + [w->window release]; + uiFreeControl(uiControl(w)); +} + +uiDarwinControlDefaultHandle(uiWindow, window) + +uiControl *uiWindowParent(uiControl *c) +{ + return NULL; +} + +void uiWindowSetParent(uiControl *c, uiControl *parent) +{ + uiUserBugCannotSetParentOnToplevel("uiWindow"); +} + +static int uiWindowToplevel(uiControl *c) +{ + return 1; +} + +static int uiWindowVisible(uiControl *c) +{ + uiWindow *w = uiWindow(c); + + return [w->window isVisible]; +} + +static void uiWindowShow(uiControl *c) +{ + uiWindow *w = (uiWindow *) c; + + [w->window makeKeyAndOrderFront:w->window]; +} + +static void uiWindowHide(uiControl *c) +{ + uiWindow *w = (uiWindow *) c; + + [w->window orderOut:w->window]; +} + +uiDarwinControlDefaultEnabled(uiWindow, window) +uiDarwinControlDefaultEnable(uiWindow, window) +uiDarwinControlDefaultDisable(uiWindow, window) + +static void uiWindowSyncEnableState(uiDarwinControl *c, int enabled) +{ + uiWindow *w = uiWindow(c); + + if (uiDarwinShouldStopSyncEnableState(uiDarwinControl(w), enabled)) + return; + if (w->child != NULL) + uiDarwinControlSyncEnableState(uiDarwinControl(w->child), enabled); +} + +static void uiWindowSetSuperview(uiDarwinControl *c, NSView *superview) +{ + // TODO +} + +static void windowRelayout(uiWindow *w) +{ + NSView *childView; + NSView *contentView; + + removeConstraints(w); + if (w->child == NULL) + return; + childView = (NSView *) uiControlHandle(w->child); + contentView = [w->window contentView]; + singleChildConstraintsEstablish(&(w->constraints), + contentView, childView, + uiDarwinControlHugsTrailingEdge(uiDarwinControl(w->child)), + uiDarwinControlHugsBottom(uiDarwinControl(w->child)), + w->margined, + @"uiWindow"); +} + +uiDarwinControlDefaultHugsTrailingEdge(uiWindow, window) +uiDarwinControlDefaultHugsBottom(uiWindow, window) + +static void uiWindowChildEdgeHuggingChanged(uiDarwinControl *c) +{ + uiWindow *w = uiWindow(c); + + windowRelayout(w); +} + +// TODO +uiDarwinControlDefaultHuggingPriority(uiWindow, window) +uiDarwinControlDefaultSetHuggingPriority(uiWindow, window) +// end TODO + +static void uiWindowChildVisibilityChanged(uiDarwinControl *c) +{ + uiWindow *w = uiWindow(c); + + windowRelayout(w); +} + +char *uiWindowTitle(uiWindow *w) +{ + return uiDarwinNSStringToText([w->window title]); +} + +void uiWindowSetTitle(uiWindow *w, const char *title) +{ + [w->window setTitle:toNSString(title)]; +} + +void uiWindowContentSize(uiWindow *w, int *width, int *height) +{ + NSRect r; + + r = [w->window contentRectForFrameRect:[w->window frame]]; + *width = r.size.width; + *height = r.size.height; +} + +void uiWindowSetContentSize(uiWindow *w, int width, int height) +{ + w->suppressSizeChanged = YES; + [w->window setContentSize:NSMakeSize(width, height)]; + w->suppressSizeChanged = NO; +} + +int uiWindowFullscreen(uiWindow *w) +{ + return w->fullscreen; +} + +void uiWindowSetFullscreen(uiWindow *w, int fullscreen) +{ + if (w->fullscreen && fullscreen) + return; + if (!w->fullscreen && !fullscreen) + return; + w->fullscreen = fullscreen; + if (w->fullscreen && w->borderless) // borderless doesn't play nice with fullscreen; don't toggle while borderless + return; + w->suppressSizeChanged = YES; + [w->window toggleFullScreen:w->window]; + w->suppressSizeChanged = NO; + if (!w->fullscreen && w->borderless) // borderless doesn't play nice with fullscreen; restore borderless after removing + [w->window setStyleMask:NSBorderlessWindowMask]; +} + +void uiWindowOnContentSizeChanged(uiWindow *w, void (*f)(uiWindow *, void *), void *data) +{ + w->onContentSizeChanged = f; + w->onContentSizeChangedData = data; +} + +void uiWindowOnClosing(uiWindow *w, int (*f)(uiWindow *, void *), void *data) +{ + w->onClosing = f; + w->onClosingData = data; +} + +int uiWindowBorderless(uiWindow *w) +{ + return w->borderless; +} + +void uiWindowSetBorderless(uiWindow *w, int borderless) +{ + w->borderless = borderless; + if (w->borderless) { + // borderless doesn't play nice with fullscreen; wait for later + if (!w->fullscreen) + [w->window setStyleMask:NSBorderlessWindowMask]; + } else { + [w->window setStyleMask:defaultStyleMask]; + // borderless doesn't play nice with fullscreen; restore state + if (w->fullscreen) { + w->suppressSizeChanged = YES; + [w->window toggleFullScreen:w->window]; + w->suppressSizeChanged = NO; + } + } +} + +void uiWindowSetChild(uiWindow *w, uiControl *child) +{ + NSView *childView; + + if (w->child != NULL) { + childView = (NSView *) uiControlHandle(w->child); + [childView removeFromSuperview]; + uiControlSetParent(w->child, NULL); + } + w->child = child; + if (w->child != NULL) { + uiControlSetParent(w->child, uiControl(w)); + childView = (NSView *) uiControlHandle(w->child); + uiDarwinControlSetSuperview(uiDarwinControl(w->child), [w->window contentView]); + uiDarwinControlSyncEnableState(uiDarwinControl(w->child), uiControlEnabledToUser(uiControl(w))); + } + windowRelayout(w); +} + +int uiWindowMargined(uiWindow *w) +{ + return w->margined; +} + +void uiWindowSetMargined(uiWindow *w, int margined) +{ + w->margined = margined; + singleChildConstraintsSetMargined(&(w->constraints), w->margined); +} + +static int defaultOnClosing(uiWindow *w, void *data) +{ + return 0; +} + +static void defaultOnPositionContentSizeChanged(uiWindow *w, void *data) +{ + // do nothing +} + +uiWindow *uiNewWindow(const char *title, int width, int height, int hasMenubar) +{ + uiWindow *w; + + finalizeMenus(); + + uiDarwinNewControl(uiWindow, w); + + w->window = [[libuiNSWindow alloc] initWithContentRect:NSMakeRect(0, 0, (CGFloat) width, (CGFloat) height) + styleMask:defaultStyleMask + backing:NSBackingStoreBuffered + defer:YES]; + [w->window setTitle:toNSString(title)]; + + // do NOT release when closed + // we manually do this in uiWindowDestroy() above + [w->window setReleasedWhenClosed:NO]; + + if (windowDelegate == nil) { + windowDelegate = [[windowDelegateClass new] autorelease]; + [delegates addObject:windowDelegate]; + } + [windowDelegate registerWindow:w]; + uiWindowOnClosing(w, defaultOnClosing, NULL); + uiWindowOnContentSizeChanged(w, defaultOnPositionContentSizeChanged, NULL); + + return w; +} + +// utility function for menus +uiWindow *windowFromNSWindow(NSWindow *w) +{ + if (w == nil) + return NULL; + if (windowDelegate == nil) // no windows were created yet; we're called with some OS X-provided window + return NULL; + return [windowDelegate lookupWindow:w]; +} diff --git a/src/libui_sdl/libui/darwin/winmoveresize.m b/src/libui_sdl/libui/darwin/winmoveresize.m new file mode 100644 index 0000000..9145b7b --- /dev/null +++ b/src/libui_sdl/libui/darwin/winmoveresize.m @@ -0,0 +1,253 @@ +// 1 november 2016 +#import "uipriv_darwin.h" + +// because we are changing the window frame each time the mouse moves, the successive -[NSEvent locationInWindow]s cannot be meaningfully used together +// make sure they are all following some sort of standard to avoid this problem; the screen is the most obvious possibility since it requires only one conversion (the only one that a NSWindow provides) +static NSPoint makeIndependent(NSPoint p, NSWindow *w) +{ + NSRect r; + + r.origin = p; + // mikeash in irc.freenode.net/#macdev confirms both that any size will do and that we can safely ignore the resultant size + r.size = NSZeroSize; + return [w convertRectToScreen:r].origin; +} + +struct onMoveDragParams { + NSWindow *w; + // using the previous point causes weird issues like the mouse seeming to fall behind the window edge... so do this instead + // TODO will this make things like the menubar and dock easier too? + NSRect initialFrame; + NSPoint initialPoint; +}; + +void onMoveDrag(struct onMoveDragParams *p, NSEvent *e) +{ + NSPoint new; + NSRect frame; + CGFloat offx, offy; + + new = makeIndependent([e locationInWindow], p->w); + frame = p->initialFrame; + + offx = new.x - p->initialPoint.x; + offy = new.y - p->initialPoint.y; + frame.origin.x += offx; + frame.origin.y += offy; + + // TODO handle the menubar + // TODO wait the system does this for us already?! + + [p->w setFrameOrigin:frame.origin]; +} + +void doManualMove(NSWindow *w, NSEvent *initialEvent) +{ + __block struct onMoveDragParams mdp; + struct nextEventArgs nea; + BOOL (^handleEvent)(NSEvent *e); + __block BOOL done; + + // this is only available on 10.11 and newer (LONGTERM FUTURE) + // but use it if available; this lets us use the real OS dragging code, which means we can take advantage of OS features like Spaces + if ([w respondsToSelector:@selector(performWindowDragWithEvent:)]) { + [((id) w) performWindowDragWithEvent:initialEvent]; + return; + } + + mdp.w = w; + mdp.initialFrame = [mdp.w frame]; + mdp.initialPoint = makeIndependent([initialEvent locationInWindow], mdp.w); + + nea.mask = NSLeftMouseDraggedMask | NSLeftMouseUpMask; + nea.duration = [NSDate distantFuture]; + nea.mode = NSEventTrackingRunLoopMode; // nextEventMatchingMask: docs suggest using this for manual mouse tracking + nea.dequeue = YES; + handleEvent = ^(NSEvent *e) { + if ([e type] == NSLeftMouseUp) { + done = YES; + return YES; // do not send + } + onMoveDrag(&mdp, e); + return YES; // do not send + }; + done = NO; + while (mainStep(&nea, handleEvent)) + if (done) + break; +} + +// see http://stackoverflow.com/a/40352996/3408572 +static void minMaxAutoLayoutSizes(NSWindow *w, NSSize *min, NSSize *max) +{ + NSLayoutConstraint *cw, *ch; + NSView *contentView; + NSRect prevFrame; + + // if adding these constraints causes the window to change size somehow, don't show it to the user and change it back afterwards + NSDisableScreenUpdates(); + prevFrame = [w frame]; + + // minimum: encourage the window to be as small as possible + contentView = [w contentView]; + cw = mkConstraint(contentView, NSLayoutAttributeWidth, + NSLayoutRelationEqual, + nil, NSLayoutAttributeNotAnAttribute, + 0, 0, + @"window minimum width finding constraint"); + [cw setPriority:NSLayoutPriorityDragThatCanResizeWindow]; + [contentView addConstraint:cw]; + ch = mkConstraint(contentView, NSLayoutAttributeHeight, + NSLayoutRelationEqual, + nil, NSLayoutAttributeNotAnAttribute, + 0, 0, + @"window minimum height finding constraint"); + [ch setPriority:NSLayoutPriorityDragThatCanResizeWindow]; + [contentView addConstraint:ch]; + *min = [contentView fittingSize]; + [contentView removeConstraint:cw]; + [contentView removeConstraint:ch]; + + // maximum: encourage the window to be as large as possible + contentView = [w contentView]; + cw = mkConstraint(contentView, NSLayoutAttributeWidth, + NSLayoutRelationEqual, + nil, NSLayoutAttributeNotAnAttribute, + 0, CGFLOAT_MAX, + @"window maximum width finding constraint"); + [cw setPriority:NSLayoutPriorityDragThatCanResizeWindow]; + [contentView addConstraint:cw]; + ch = mkConstraint(contentView, NSLayoutAttributeHeight, + NSLayoutRelationEqual, + nil, NSLayoutAttributeNotAnAttribute, + 0, CGFLOAT_MAX, + @"window maximum height finding constraint"); + [ch setPriority:NSLayoutPriorityDragThatCanResizeWindow]; + [contentView addConstraint:ch]; + *max = [contentView fittingSize]; + [contentView removeConstraint:cw]; + [contentView removeConstraint:ch]; + + [w setFrame:prevFrame display:YES]; // TODO really YES? + NSEnableScreenUpdates(); +} + +static void handleResizeLeft(NSRect *frame, NSPoint old, NSPoint new) +{ + frame->origin.x += new.x - old.x; + frame->size.width -= new.x - old.x; +} + +// TODO properly handle the menubar +// TODO wait, OS X does it for us?! +static void handleResizeTop(NSRect *frame, NSPoint old, NSPoint new) +{ + frame->size.height += new.y - old.y; +} + +static void handleResizeRight(NSRect *frame, NSPoint old, NSPoint new) +{ + frame->size.width += new.x - old.x; +} + + +// TODO properly handle the menubar +static void handleResizeBottom(NSRect *frame, NSPoint old, NSPoint new) +{ + frame->origin.y += new.y - old.y; + frame->size.height -= new.y - old.y; +} + +struct onResizeDragParams { + NSWindow *w; + // using the previous point causes weird issues like the mouse seeming to fall behind the window edge... so do this instead + // TODO will this make things like the menubar and dock easier too? + NSRect initialFrame; + NSPoint initialPoint; + uiWindowResizeEdge edge; + NSSize min; + NSSize max; +}; + +static void onResizeDrag(struct onResizeDragParams *p, NSEvent *e) +{ + NSPoint new; + NSRect frame; + + new = makeIndependent([e locationInWindow], p->w); + frame = p->initialFrame; + + // horizontal + switch (p->edge) { + case uiWindowResizeEdgeLeft: + case uiWindowResizeEdgeTopLeft: + case uiWindowResizeEdgeBottomLeft: + handleResizeLeft(&frame, p->initialPoint, new); + break; + case uiWindowResizeEdgeRight: + case uiWindowResizeEdgeTopRight: + case uiWindowResizeEdgeBottomRight: + handleResizeRight(&frame, p->initialPoint, new); + break; + } + // vertical + switch (p->edge) { + case uiWindowResizeEdgeTop: + case uiWindowResizeEdgeTopLeft: + case uiWindowResizeEdgeTopRight: + handleResizeTop(&frame, p->initialPoint, new); + break; + case uiWindowResizeEdgeBottom: + case uiWindowResizeEdgeBottomLeft: + case uiWindowResizeEdgeBottomRight: + handleResizeBottom(&frame, p->initialPoint, new); + break; + } + + // constrain + // TODO should we constrain against anything else as well? minMaxAutoLayoutSizes() already gives us nonnegative sizes, but... + if (frame.size.width < p->min.width) + frame.size.width = p->min.width; + if (frame.size.height < p->min.height) + frame.size.height = p->min.height; + // TODO > or >= ? + if (frame.size.width > p->max.width) + frame.size.width = p->max.width; + if (frame.size.height > p->max.height) + frame.size.height = p->max.height; + + [p->w setFrame:frame display:YES]; // and do reflect the new frame immediately +} + +// TODO do our events get fired with this? *should* they? +void doManualResize(NSWindow *w, NSEvent *initialEvent, uiWindowResizeEdge edge) +{ + __block struct onResizeDragParams rdp; + struct nextEventArgs nea; + BOOL (^handleEvent)(NSEvent *e); + __block BOOL done; + + rdp.w = w; + rdp.initialFrame = [rdp.w frame]; + rdp.initialPoint = makeIndependent([initialEvent locationInWindow], rdp.w); + rdp.edge = edge; + // TODO what happens if these change during the loop? + minMaxAutoLayoutSizes(rdp.w, &(rdp.min), &(rdp.max)); + + nea.mask = NSLeftMouseDraggedMask | NSLeftMouseUpMask; + nea.duration = [NSDate distantFuture]; + nea.mode = NSEventTrackingRunLoopMode; // nextEventMatchingMask: docs suggest using this for manual mouse tracking + nea.dequeue = YES; + handleEvent = ^(NSEvent *e) { + if ([e type] == NSLeftMouseUp) { + done = YES; + return YES; // do not send + } + onResizeDrag(&rdp, e); + return YES; // do not send + }; + done = NO; + while (mainStep(&nea, handleEvent)) + if (done) + break; +} |