From 11e00760d39c483d14968f1a0a0e5b07faea98d4 Mon Sep 17 00:00:00 2001 From: Tae Won Ha Date: Sat, 28 Nov 2020 15:58:12 +0100 Subject: [PATCH] Add selected --- Tabs/Package.swift | 16 +- Tabs/Sources/Tabs/Defs.swift | 17 -- .../Tabs/DraggingSingleRowStackView.swift | 2 + Tabs/Sources/Tabs/Tab.swift | 159 +++++++++++++++--- Tabs/Sources/Tabs/TabBar.swift | 107 ++++++++---- Tabs/Sources/Tabs/Theme.swift | 35 ++++ .../TabsSupport.xcodeproj/project.pbxproj | 8 +- Tabs/Support/TabsSupport/AppDelegate.swift | 4 +- 8 files changed, 263 insertions(+), 85 deletions(-) delete mode 100644 Tabs/Sources/Tabs/Defs.swift create mode 100644 Tabs/Sources/Tabs/Theme.swift diff --git a/Tabs/Package.swift b/Tabs/Package.swift index dd6e82aa..b2e87318 100644 --- a/Tabs/Package.swift +++ b/Tabs/Package.swift @@ -4,17 +4,17 @@ import PackageDescription let package = Package( - name: "Tabs", + name: "Components", platforms: [.macOS(.v10_13)], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "Tabs", - targets: ["Tabs"] - ), + .library(name: "Tabs", targets: ["Tabs"]), ], dependencies: [ - .package(name: "MaterialIcons", url: "https://github.com/qvacua/material-icons", .upToNextMinor(from: "0.1.0")), + .package( + name: "MaterialIcons", + url: "https://github.com/qvacua/material-icons", + .upToNextMinor(from: "0.1.0") + ), .package( name: "PureLayout", url: "https://github.com/PureLayout/PureLayout", @@ -22,8 +22,6 @@ let package = Package( ), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "Tabs", dependencies: ["PureLayout", "MaterialIcons"] diff --git a/Tabs/Sources/Tabs/Defs.swift b/Tabs/Sources/Tabs/Defs.swift deleted file mode 100644 index 73b80277..00000000 --- a/Tabs/Sources/Tabs/Defs.swift +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Tae Won Ha - http://taewon.de - @hataewon - * See LICENSE - */ - -import Cocoa - -public enum Defs { - public static let tabHeight = CGFloat(28) - public static let tabMinWidth = CGFloat(100) - public static let tabMaxWidth = CGFloat(400) - public static let tabTitleFont = NSFont.systemFont(ofSize: 11) - - public static let tabPadding = CGFloat(0) - - public static let tabBarHeight = CGFloat(Self.tabHeight + 2 * Self.tabPadding) -} diff --git a/Tabs/Sources/Tabs/DraggingSingleRowStackView.swift b/Tabs/Sources/Tabs/DraggingSingleRowStackView.swift index aa15d905..2981c22a 100644 --- a/Tabs/Sources/Tabs/DraggingSingleRowStackView.swift +++ b/Tabs/Sources/Tabs/DraggingSingleRowStackView.swift @@ -13,6 +13,7 @@ import Cocoa class DraggingSingleRowStackView: NSStackView { var isDraggingEnabled = true + var postDraggingAction: ((NSStackView) -> Void)? override func mouseDragged(with event: NSEvent) { guard self.isDraggingEnabled else { @@ -23,6 +24,7 @@ class DraggingSingleRowStackView: NSStackView { let location = convert(event.locationInWindow, from: nil) if let dragged = views.first(where: { $0.hitTest(location) != nil }) { self.reorder(view: dragged, event: event) + self.postDraggingAction?(self) } } diff --git a/Tabs/Sources/Tabs/Tab.swift b/Tabs/Sources/Tabs/Tab.swift index e898187b..5d0e5d52 100644 --- a/Tabs/Sources/Tabs/Tab.swift +++ b/Tabs/Sources/Tabs/Tab.swift @@ -4,42 +4,159 @@ */ import Cocoa +import MaterialIcons -public class Tab: NSView { +protocol TabDelegate: AnyObject { + func select(tab: Tab) +} - public var title: String - - public init(withTitle title: String) { +class Tab: NSView { + enum Position { + case first + case inBetween + case last + } + + var title: String { + didSet { + self.titleView.stringValue = self.title + self.adjustWidth() + } + } + + init(withTitle title: String, in tabBar: TabBar) { self.title = title - self.attributedTitle = NSAttributedString(string: title, attributes: [ - .font: Defs.tabTitleFont - ]) + self.theme = tabBar.theme super.init(frame: .zero) self.configureForAutoLayout() self.wantsLayer = true - #if DEBUG - self.layer?.backgroundColor = NSColor.cyan.cgColor - #endif - - self.autoSetDimensions(to: CGSize(width: 200, height: Defs.tabHeight)) + self.layer?.backgroundColor = self.theme.backgroundColor.cgColor + self.autoSetDimension(.height, toSize: self.theme.tabHeight) + self.titleView.stringValue = title + + self.addViews() + self.adjustWidth() } - public override func mouseDown(with event: NSEvent) { - Swift.print("mouse down in tab") + override func mouseUp(with _: NSEvent) { + self.delegate?.select(tab: self) } - public override func mouseUp(with event: NSEvent) { - Swift.print("mouse up in tab") - } - - public override func draw(_ dirtyRect: NSRect) { - self.attributedTitle.draw(in: self.bounds) + override func draw(_: NSRect) { + self.drawSeparators() + self.drawSelectionIndicator() } @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } - private var attributedTitle: NSAttributedString + var isSelected: Bool = false { + didSet { self.needsDisplay = true } + } + + weak var delegate: TabDelegate? + var position = Position.inBetween { + didSet { self.needsDisplay = true } + } + + private let closeButton = NSButton(forAutoLayout: ()) + private let iconView = NSImageView(forAutoLayout: ()) + private let titleView = NSTextField(forAutoLayout: ()) + + private var theme: Theme +} + +extension Tab { + private func adjustWidth() { + let idealWidth = 4 * self.theme.tabHorizontalPadding + 2 * self.theme.iconDimension.width + + self.titleView.intrinsicContentSize.width + let targetWidth = min(max(self.theme.tabMinWidth, idealWidth), self.theme.tabMaxWidth) + self.autoSetDimension(.width, toSize: targetWidth) + } + + private func addViews() { + let close = self.closeButton + let icon = self.iconView + let title = self.titleView + + self.addSubview(close) + self.addSubview(icon) + self.addSubview(title) + + close.imagePosition = .imageOnly + close.image = Icon.close.asImage( + dimension: self.theme.iconDimension.width, + color: self.theme.foregroundColor + ) + close.isBordered = false + (close.cell as? NSButtonCell)?.highlightsBy = .contentsCellMask + + icon.image = NSImage(named: NSImage.actionTemplateName) + + title.drawsBackground = false + title.font = self.theme.titleFont + title.textColor = self.theme.foregroundColor + title.isEditable = false + title.isBordered = false + title.isSelectable = false + title.usesSingleLineMode = true + title.lineBreakMode = .byTruncatingTail + + close.autoSetDimensions(to: self.theme.iconDimension) + close.autoPinEdge(toSuperviewEdge: .left, withInset: self.theme.tabHorizontalPadding) + close.autoAlignAxis(toSuperviewAxis: .horizontal) + + icon.autoSetDimensions(to: self.theme.iconDimension) + icon.autoPinEdge(.left, to: .right, of: close, withOffset: self.theme.tabHorizontalPadding) + icon.autoAlignAxis(toSuperviewAxis: .horizontal) + + title.autoPinEdge(.left, to: .right, of: icon, withOffset: self.theme.tabHorizontalPadding) + title.autoPinEdge(toSuperviewEdge: .right, withInset: self.theme.tabHorizontalPadding) + title.autoAlignAxis(toSuperviewAxis: .horizontal) + } + + private func drawSeparators() { + let b = self.bounds + let left = CGRect(x: 0, y: 0, width: self.theme.separatorThickness, height: b.height) + let right = CGRect(x: b.maxX - 1, y: 0, width: self.theme.separatorThickness, height: b.height) + let bottom = CGRect( + x: 0, + y: 0, + width: b.width, + height: self.theme.separatorThickness + ) + + guard let context = NSGraphicsContext.current?.cgContext else { return } + context.saveGState() + defer { context.restoreGState() } + self.theme.separatorColor.set() + + switch self.position { + case .first: + right.fill() + case .inBetween: + left.fill() + right.fill() + case .last: + left.fill() + } + + if !self.isSelected { bottom.fill() } + } + + private func drawSelectionIndicator() { + guard self.isSelected else { return } + + let b = self.bounds + let rect = CGRect(x: 0, y: 0, width: b.width, height: self.theme.tabSelectionIndicatorThickness) + + guard let context = NSGraphicsContext.current?.cgContext else { return } + context.saveGState() + defer { context.restoreGState() } + self.theme.tabSelectedIndicatorColor.set() + + rect.fill() + } } diff --git a/Tabs/Sources/Tabs/TabBar.swift b/Tabs/Sources/Tabs/TabBar.swift index 8d69f962..7aad2640 100644 --- a/Tabs/Sources/Tabs/TabBar.swift +++ b/Tabs/Sources/Tabs/TabBar.swift @@ -7,57 +7,100 @@ import Cocoa import PureLayout public class TabBar: NSView { - public private(set) var tabs = [Tab]() - - public init() { + public var theme: Theme { self._theme } + + public init(withTheme theme: Theme) { + self._theme = theme + super.init(frame: .zero) self.configureForAutoLayout() self.wantsLayer = true - - #if DEBUG - self.layer?.backgroundColor = NSColor.yellow.cgColor - #endif + self.layer?.backgroundColor = theme.backgroundColor.cgColor self.addViews() self.addTestTabs() } + override public func draw(_: NSRect) { + self.drawSeparator() + } + @available(*, unavailable) required init?(coder _: NSCoder) { fatalError("init(coder:) has not been implemented") } + var tabs = [Tab]() + + private var _theme: Theme private let scrollView = HorizontalOnlyScrollView(forAutoLayout: ()) private let stackView = DraggingSingleRowStackView(forAutoLayout: ()) +} + +extension TabBar: TabDelegate { + func select(tab: Tab) { + self.stackView.arrangedSubviews.forEach { ($0 as? Tab)?.isSelected = false } + tab.isSelected = true + } +} + +extension TabBar { + private func drawSeparator() { + let b = self.bounds + let rect = CGRect(x: 0, y: 0, width: b.width, height: self._theme.separatorThickness) + + guard let context = NSGraphicsContext.current?.cgContext else { return } + context.saveGState() + defer { context.restoreGState() } + self._theme.separatorColor.set() + + rect.fill() + } private func addViews() { - #if DEBUG - self.scrollView.backgroundColor = .brown - #endif + let scroll = self.scrollView + let stack = self.stackView - self.scrollView.hasHorizontalScroller = false - - self.addSubview(self.scrollView) - self.scrollView.autoPinEdgesToSuperviewEdges() - - self.scrollView.documentView = self.stackView - - self.stackView.autoPinEdge(toSuperviewEdge: .top) - self.stackView.autoPinEdge(toSuperviewEdge: .left) - self.stackView.autoPinEdge(toSuperviewEdge: .bottom) - - self.stackView.spacing = Defs.tabPadding + self.addSubview(scroll) + scroll.autoPinEdgesToSuperviewEdges() + + scroll.drawsBackground = false + scroll.hasHorizontalScroller = false + scroll.documentView = stack + + stack.autoPinEdge(toSuperviewEdge: .top) + stack.autoPinEdge(toSuperviewEdge: .left) + stack.autoPinEdge(toSuperviewEdge: .bottom) + + stack.spacing = self._theme.tabSpacing + stack.postDraggingAction = { stackView in + let endIndex = stackView.arrangedSubviews.endIndex - 1 + stackView.arrangedSubviews.enumerated().forEach { index, view in + guard let tab = view as? Tab else { return } + + if index == 0 { tab.position = .first } + else if index == endIndex { tab.position = .last } + else { tab.position = .inBetween } + } + } } private func addTestTabs() { - let tab1 = Tab(withTitle: "Test 1") - let tab2 = Tab(withTitle: "Test 2") - let tab3 = Tab(withTitle: "Test 3") - let tab4 = Tab(withTitle: "Test 4") - - tab1.layer?.backgroundColor = NSColor.red.cgColor - tab2.layer?.backgroundColor = NSColor.blue.cgColor - tab3.layer?.backgroundColor = NSColor.green.cgColor - tab4.layer?.backgroundColor = NSColor.white.cgColor - + let tab1 = Tab(withTitle: "Test 1", in: self) + let tab2 = Tab(withTitle: "Test 2 Some", in: self) + let tab3 = Tab(withTitle: "Test 3", in: self) + let tab4 = Tab( + withTitle: "Test 4 More Text More Less Really??? More Text How long should it be?", + in: self + ) + + tab1.delegate = self + tab2.delegate = self + tab3.delegate = self + tab4.delegate = self + + tab1.position = .first + tab4.position = .last + tab2.isSelected = true + self.stackView.addArrangedSubview(tab1) self.stackView.addArrangedSubview(tab2) self.stackView.addArrangedSubview(tab3) diff --git a/Tabs/Sources/Tabs/Theme.swift b/Tabs/Sources/Tabs/Theme.swift new file mode 100644 index 00000000..a9c31aeb --- /dev/null +++ b/Tabs/Sources/Tabs/Theme.swift @@ -0,0 +1,35 @@ +/** + * Tae Won Ha - http://taewon.de - @hataewon + * See LICENSE + */ + +import Cocoa + +public struct Theme { + public static let `default` = Self() + + public var foregroundColor = NSColor.textColor + public var backgroundColor = NSColor.controlBackgroundColor + public var separatorColor = NSColor.controlShadowColor + + public var tabSelectedIndicatorColor = NSColor.selectedControlColor + + public var titleFont = NSFont.systemFont(ofSize: 11) + + public var tabHeight = CGFloat(28) + + public var tabMaxWidth = CGFloat(250) + public var separatorThickness = CGFloat(1) + public var tabHorizontalPadding = CGFloat(4) + public var tabSelectionIndicatorThickness = CGFloat(4) + public var iconDimension = CGSize(width: 16, height: 16) + + public var tabMinWidth: CGFloat { + 4 * self.tabHorizontalPadding + 2 * self.iconDimension.width + 32 + } + + public var tabBarHeight: CGFloat { self.tabHeight } + public var tabSpacing = CGFloat(-1) + + public init() {} +} diff --git a/Tabs/Support/TabsSupport.xcodeproj/project.pbxproj b/Tabs/Support/TabsSupport.xcodeproj/project.pbxproj index 2e6ae544..60daf30f 100644 --- a/Tabs/Support/TabsSupport.xcodeproj/project.pbxproj +++ b/Tabs/Support/TabsSupport.xcodeproj/project.pbxproj @@ -7,10 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + 4B2A75652572869C0002D722 /* Tabs in Frameworks */ = {isa = PBXBuildFile; productRef = 4B2A75642572869C0002D722 /* Tabs */; }; 4BEBD4BA256A76BB002218F8 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BEBD4B9256A76BB002218F8 /* AppDelegate.swift */; }; 4BEBD4BC256A76BB002218F8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4BEBD4BB256A76BB002218F8 /* Assets.xcassets */; }; 4BEBD4BF256A76BB002218F8 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4BEBD4BD256A76BB002218F8 /* MainMenu.xib */; }; - 4BEBD4CC256A7740002218F8 /* Tabs in Frameworks */ = {isa = PBXBuildFile; productRef = 4BEBD4CB256A7740002218F8 /* Tabs */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -26,7 +26,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4BEBD4CC256A7740002218F8 /* Tabs in Frameworks */, + 4B2A75652572869C0002D722 /* Tabs in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,7 +85,7 @@ ); name = TabsSupport; packageProductDependencies = ( - 4BEBD4CB256A7740002218F8 /* Tabs */, + 4B2A75642572869C0002D722 /* Tabs */, ); productName = TabsSupport; productReference = 4BEBD4B6256A76BB002218F8 /* TabsSupport.app */; @@ -339,7 +339,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 4BEBD4CB256A7740002218F8 /* Tabs */ = { + 4B2A75642572869C0002D722 /* Tabs */ = { isa = XCSwiftPackageProductDependency; productName = Tabs; }; diff --git a/Tabs/Support/TabsSupport/AppDelegate.swift b/Tabs/Support/TabsSupport/AppDelegate.swift index dc660bda..0c68a3fb 100644 --- a/Tabs/Support/TabsSupport/AppDelegate.swift +++ b/Tabs/Support/TabsSupport/AppDelegate.swift @@ -14,7 +14,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBOutlet var window: NSWindow! override init() { - self.tabBar = TabBar() + self.tabBar = TabBar(withTheme: .default) super.init() } @@ -24,7 +24,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { self.tabBar.autoPinEdge(toSuperviewEdge: .top) self.tabBar.autoPinEdge(toSuperviewEdge: .left) self.tabBar.autoPinEdge(toSuperviewEdge: .right) - self.tabBar.autoSetDimension(.height, toSize: Defs.tabBarHeight) + self.tabBar.autoSetDimension(.height, toSize: Theme().tabBarHeight) } func applicationWillTerminate(_: Notification) {