└── app.swift /app.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xcrun swift 2 | import Cocoa 3 | 4 | class MyAppDelegate: NSObject, NSApplicationDelegate { 5 | let window = NSWindow() 6 | var didFinishLaunching: NSWindow -> () = { _ in () } 7 | func applicationDidFinishLaunching(aNotification: NSNotification) { 8 | didFinishLaunching(window) 9 | } 10 | } 11 | 12 | public class App { 13 | private var application: NSApplication 14 | 15 | init(_ theApplication: NSApplication) { 16 | application = theApplication 17 | } 18 | 19 | func exit() { 20 | application.terminate(nil) 21 | } 22 | } 23 | 24 | public func app(title: String, width: Int = 400, height: Int = 200, rootView: App -> View) { 25 | let app = NSApplication.sharedApplication() 26 | let appDelegate = MyAppDelegate() 27 | app.setActivationPolicy(.Regular) 28 | let view = rootView(App(app)) 29 | appDelegate.didFinishLaunching = { window in 30 | window.setContentSize(NSSize(width:width, height:height)) 31 | window.styleMask = NSTitledWindowMask | NSClosableWindowMask | NSMiniaturizableWindowMask | NSResizableWindowMask 32 | window.opaque = false 33 | window.center() 34 | window.title = title 35 | window.contentView!.wantsLayer = true 36 | 37 | window.makeKeyAndOrderFront(window) 38 | let contentView = window.contentView! 39 | 40 | contentView.addSubview(view.rootView) 41 | if view.rootView.frame == CGRectZero { 42 | view.rootView.sizeToParent() 43 | } 44 | window.layoutIfNeeded() 45 | view.afterAdding() 46 | app.activateIgnoringOtherApps(true) 47 | } 48 | 49 | app.delegate = appDelegate 50 | app.run() 51 | print(view) // Make sure we keep a reference around 52 | } 53 | 54 | extension NSView { 55 | func sizeToParent() { 56 | frame = superview!.bounds 57 | autoresizingMask = NSAutoresizingMaskOptions([.ViewWidthSizable, .ViewMaxXMargin, .ViewMinYMargin, .ViewHeightSizable, .ViewMaxYMargin]) 58 | } 59 | } 60 | 61 | public struct TextViewConfiguration { 62 | var text: String = "" 63 | var size: NSSize? = NSMakeSize(180,160) 64 | var origin: NSPoint? = NSMakePoint(20,10) 65 | var editable: Bool = false 66 | var selectable: Bool = true 67 | } 68 | 69 | public protocol View { 70 | var rootView: NSView { get } 71 | var afterAdding: () -> () { get } 72 | } 73 | 74 | public class TextView: View { 75 | public var rootView: NSView 76 | var textView: NSTextView 77 | public var afterAdding: () -> () 78 | var text: String { 79 | get { 80 | return textView.string ?? "" 81 | } 82 | set { 83 | textView.string = newValue 84 | } 85 | } 86 | init(rootView: NSView, textView: NSTextView, afterAdding: () -> () = { _ in () } ) { 87 | self.rootView = rootView 88 | self.afterAdding = afterAdding 89 | self.textView = textView 90 | } 91 | } 92 | 93 | public class SimpleView: View { 94 | public var rootView: NSView 95 | public var afterAdding: () -> () 96 | var delegate: AnyObject? 97 | init(rootView: NSView, delegate: AnyObject? = nil, afterAdding: () -> () = { _ in () } ) { 98 | self.rootView = rootView 99 | self.delegate = delegate 100 | self.afterAdding = afterAdding 101 | } 102 | } 103 | 104 | public func textView(text: String, editable: Bool) -> TextView { 105 | var configuration = TextViewConfiguration() 106 | configuration.text = text 107 | configuration.editable = editable 108 | return textView(configuration) 109 | } 110 | 111 | public func textView(configuration: TextViewConfiguration) -> TextView { 112 | let scrollView = NSScrollView(frame: CGRectZero) 113 | scrollView.borderType = .NoBorder 114 | scrollView.hasVerticalScroller = true 115 | scrollView.hasHorizontalScroller = false 116 | 117 | let ed = NSTextView(frame: CGRectZero) 118 | 119 | let afterAdding = { 120 | ed.frame = scrollView.bounds 121 | ed.minSize = scrollView.bounds.size 122 | ed.maxSize = NSSize(width: CGFloat.max, height: CGFloat.max) 123 | ed.string = configuration.text 124 | ed.editable = configuration.editable 125 | ed.selectable = configuration.selectable 126 | ed.verticallyResizable = true 127 | ed.horizontallyResizable = false 128 | ed.textContainer!.containerSize = NSSize(width: scrollView.bounds.size.width, height: CGFloat.max) 129 | ed.textContainer!.widthTracksTextView = true 130 | scrollView.documentView = ed 131 | } 132 | 133 | return TextView(rootView: scrollView, textView: ed, afterAdding: afterAdding) 134 | } 135 | 136 | class ButtonDelegate: NSObject { 137 | var callback: () -> () 138 | init(_ callback: () -> ()) { 139 | self.callback = callback 140 | } 141 | @objc func buttonClicked() { 142 | callback() 143 | } 144 | } 145 | 146 | public func button(text: String, onClick: () -> ()) -> View { 147 | let button = NSButton(frame: CGRectZero) 148 | let delegate = ButtonDelegate(onClick) 149 | button.title = text 150 | button.target = delegate 151 | button.action = "buttonClicked" 152 | button.bezelStyle = .SmallSquareBezelStyle 153 | return SimpleView(rootView: button, delegate: delegate) 154 | } 155 | 156 | public func label(text: String) -> View { 157 | let field = NSTextField(frame: CGRectZero) 158 | field.bezeled = false 159 | field.drawsBackground = false 160 | field.editable = false 161 | field.selectable = false 162 | field.stringValue = text 163 | return SimpleView(rootView: field) 164 | } 165 | 166 | final class Box: NSObject { 167 | var unbox: A 168 | init(_ value: A) { unbox = value } 169 | } 170 | 171 | public func stack(views: [View], orientation: NSUserInterfaceLayoutOrientation = .Vertical) -> View { 172 | let stackView = NSStackView(frame: CGRectZero) 173 | stackView.orientation = orientation 174 | stackView.autoresizingMask = NSAutoresizingMaskOptions([.ViewWidthSizable, .ViewHeightSizable]) 175 | for view in views { 176 | stackView.addView(view.rootView, inGravity: .Top) 177 | } 178 | 179 | let afterAdding = { 180 | views.forEach { $0.afterAdding() } 181 | } 182 | return SimpleView(rootView: stackView, delegate: Box(views), afterAdding: afterAdding) 183 | } 184 | 185 | // TODO: left-hand side finder-like tree tructure 186 | // TODO: toolbar at the top. 187 | // Goal: build something like notes? 188 | 189 | app("My app") { theApp in 190 | let text = Array(count: 3, repeatedValue: "Hello, world").joinWithSeparator("\n") 191 | let tv = textView(text, editable: true) 192 | let theButton = button("Hello") { tv.text += "\nHello!"} 193 | let buttons = stack([theButton, button("Exit", onClick: theApp.exit)], orientation: .Horizontal) 194 | return stack([label("Add some text"), tv, buttons]) 195 | } 196 | --------------------------------------------------------------------------------