From 6f023e04ab4c2a4c72bd9308949e50d576d0564d Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Wed, 24 Apr 2024 10:58:45 -1000 Subject: [PATCH 1/5] feat: Differentiate tags using colors This change also simplifies the UI a little to focus more on writing. --- Thoughts.xcodeproj/project.pbxproj | 53 +++ Thoughts/Layouts/CenteredFlowLayout.swift | 100 ++++++ Thoughts/Model/ApplicationModel.swift | 8 +- Thoughts/Model/Document.swift | 7 +- Thoughts/Model/LocationDetails.swift | 24 +- Thoughts/Model/TokenViewModel.swift | 115 +++++++ Thoughts/Views/ComposeView.swift | 38 ++- Thoughts/Views/PickerTextField.swift | 140 ++++++++ Thoughts/Views/TagView.swift | 393 ++++++++++++++++++++++ Thoughts/Views/TokenView.swift | 118 +++++++ 10 files changed, 959 insertions(+), 37 deletions(-) create mode 100644 Thoughts/Layouts/CenteredFlowLayout.swift create mode 100644 Thoughts/Model/TokenViewModel.swift create mode 100644 Thoughts/Views/PickerTextField.swift create mode 100644 Thoughts/Views/TagView.swift create mode 100644 Thoughts/Views/TokenView.swift diff --git a/Thoughts.xcodeproj/project.pbxproj b/Thoughts.xcodeproj/project.pbxproj index 80f3177..f9ed50f 100644 --- a/Thoughts.xcodeproj/project.pbxproj +++ b/Thoughts.xcodeproj/project.pbxproj @@ -31,6 +31,12 @@ D89224A42BD7368F00DA0932 /* TimeZoneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A32BD7368F00DA0932 /* TimeZoneTests.swift */; }; D89224A72BD736B900DA0932 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A62BD736B900DA0932 /* Date.swift */; }; D89224A92BD736DF00DA0932 /* TimeZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = D89224A82BD736DF00DA0932 /* TimeZone.swift */; }; + D8C283E52BD9939E00161C82 /* TokenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C283E42BD9939E00161C82 /* TokenView.swift */; }; + D8C283E92BD9954F00161C82 /* HashRainbow in Frameworks */ = {isa = PBXBuildFile; productRef = D8C283E82BD9954F00161C82 /* HashRainbow */; }; + D8C283EE2BD9957700161C82 /* TokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C283ED2BD9957700161C82 /* TokenViewModel.swift */; }; + D8C283F02BD995AD00161C82 /* PickerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C283EF2BD995AD00161C82 /* PickerTextField.swift */; }; + D8C283F22BD995CC00161C82 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C283F12BD995CC00161C82 /* TagView.swift */; }; + D8C283F52BD9988200161C82 /* CenteredFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C283F42BD9988200161C82 /* CenteredFlowLayout.swift */; }; D8DFFCC82BD82DE900C9532A /* CLAuthorizationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */; }; D8F06D4B2B67275B0039AEF4 /* Diligence in Frameworks */ = {isa = PBXBuildFile; productRef = D8F06D4A2B67275B0039AEF4 /* Diligence */; }; D8F06D4E2B6727630039AEF4 /* Interact in Frameworks */ = {isa = PBXBuildFile; productRef = D8F06D4D2B6727630039AEF4 /* Interact */; }; @@ -85,6 +91,11 @@ D89224A32BD7368F00DA0932 /* TimeZoneTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZoneTests.swift; sourceTree = ""; }; D89224A62BD736B900DA0932 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; D89224A82BD736DF00DA0932 /* TimeZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeZone.swift; sourceTree = ""; }; + D8C283E42BD9939E00161C82 /* TokenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenView.swift; sourceTree = ""; }; + D8C283ED2BD9957700161C82 /* TokenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenViewModel.swift; sourceTree = ""; }; + D8C283EF2BD995AD00161C82 /* PickerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerTextField.swift; sourceTree = ""; }; + D8C283F12BD995CC00161C82 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; + D8C283F42BD9988200161C82 /* CenteredFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenteredFlowLayout.swift; sourceTree = ""; }; D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLAuthorizationStatus.swift; sourceTree = ""; }; D8F06D502B6731CE0039AEF4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D8F06D532B6733650039AEF4 /* thoughts-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "thoughts-license"; sourceTree = ""; }; @@ -98,6 +109,7 @@ buildActionMask = 2147483647; files = ( D8F06D4E2B6727630039AEF4 /* Interact in Frameworks */, + D8C283E92BD9954F00161C82 /* HashRainbow in Frameworks */, D892249A2BD70CC300DA0932 /* Yams in Frameworks */, D8F06D4B2B67275B0039AEF4 /* Diligence in Frameworks */, ); @@ -129,6 +141,7 @@ D892249B2BD735A100DA0932 /* Metadata.swift */, D892249F2BD735DA00DA0932 /* RegionalDate.swift */, D892249D2BD735BE00DA0932 /* ThoughtsError.swift */, + D8C283ED2BD9957700161C82 /* TokenViewModel.swift */, ); path = Model; sourceTree = ""; @@ -151,6 +164,7 @@ D852AF122B6027CD00B77A3D /* ThoughtsTests */, D852AF1C2B6027CD00B77A3D /* ThoughtsUITests */, D852AEFF2B6027CA00B77A3D /* Products */, + D8C283EA2BD9955D00161C82 /* Frameworks */, ); sourceTree = ""; }; @@ -173,6 +187,7 @@ D852AF012B6027CA00B77A3D /* ThoughtsApp.swift */, D852AF052B6027CC00B77A3D /* Assets.xcassets */, D80E08A22B675AD40023DD4F /* Extensions */, + D8C283F32BD9987500161C82 /* Layouts */, D8F06D522B6733440039AEF4 /* Licenses */, D80E089F2B67572E0023DD4F /* Model */, D852AF072B6027CC00B77A3D /* Preview Content */, @@ -217,6 +232,21 @@ path = Extensions; sourceTree = ""; }; + D8C283EA2BD9955D00161C82 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + D8C283F32BD9987500161C82 /* Layouts */ = { + isa = PBXGroup; + children = ( + D8C283F42BD9988200161C82 /* CenteredFlowLayout.swift */, + ); + path = Layouts; + sourceTree = ""; + }; D8F06D4F2B6731C00039AEF4 /* Views */ = { isa = PBXGroup; children = ( @@ -225,6 +255,9 @@ D850C2D52B69D8D500338546 /* ContentView.swift */, D852AF682B604B3E00B77A3D /* MainMenu.swift */, D8F06D502B6731CE0039AEF4 /* SettingsView.swift */, + D8C283E42BD9939E00161C82 /* TokenView.swift */, + D8C283EF2BD995AD00161C82 /* PickerTextField.swift */, + D8C283F12BD995CC00161C82 /* TagView.swift */, ); path = Views; sourceTree = ""; @@ -259,6 +292,7 @@ D8F06D4A2B67275B0039AEF4 /* Diligence */, D8F06D4D2B6727630039AEF4 /* Interact */, D89224992BD70CC300DA0932 /* Yams */, + D8C283E82BD9954F00161C82 /* HashRainbow */, ); productName = "Disk Stickies"; productReference = D852AEFE2B6027CA00B77A3D /* Thoughts.app */; @@ -336,6 +370,7 @@ D8F06D492B67275B0039AEF4 /* XCLocalSwiftPackageReference "diligence" */, D8F06D4C2B6727630039AEF4 /* XCLocalSwiftPackageReference "interact" */, D89224982BD70CC300DA0932 /* XCRemoteSwiftPackageReference "Yams" */, + D8C283E72BD9954F00161C82 /* XCRemoteSwiftPackageReference "HashRainbow" */, ); productRefGroup = D852AEFF2B6027CA00B77A3D /* Products */; projectDirPath = ""; @@ -384,16 +419,21 @@ files = ( D852AF642B604AF500B77A3D /* ApplicationModel.swift in Sources */, D850C2D62B69D8D500338546 /* ContentView.swift in Sources */, + D8C283EE2BD9957700161C82 /* TokenViewModel.swift in Sources */, D840967F2BD73CBE00926C85 /* LocationDetails.swift in Sources */, D8DFFCC82BD82DE900C9532A /* CLAuthorizationStatus.swift in Sources */, D852AF692B604B3E00B77A3D /* MainMenu.swift in Sources */, D83C2F882B857B8C00CE60F7 /* URL.swift in Sources */, + D8C283E52BD9939E00161C82 /* TokenView.swift in Sources */, D89224A02BD735DA00DA0932 /* RegionalDate.swift in Sources */, + D8C283F02BD995AD00161C82 /* PickerTextField.swift in Sources */, D892249E2BD735BE00DA0932 /* ThoughtsError.swift in Sources */, + D8C283F22BD995CC00161C82 /* TagView.swift in Sources */, D850C2D42B69D85400338546 /* KeyedDefaults.swift in Sources */, D852AF662B604B1200B77A3D /* ComposeView.swift in Sources */, D8F06D512B6731CE0039AEF4 /* SettingsView.swift in Sources */, D80E08A62B6761370023DD4F /* Document.swift in Sources */, + D8C283F52BD9988200161C82 /* CenteredFlowLayout.swift in Sources */, D852AF022B6027CA00B77A3D /* ThoughtsApp.swift in Sources */, D80E08A12B675AAC0023DD4F /* ComposeWindow.swift in Sources */, D89224A22BD7365C00DA0932 /* TimeZone.swift in Sources */, @@ -766,6 +806,14 @@ minimumVersion = 5.1.2; }; }; + D8C283E72BD9954F00161C82 /* XCRemoteSwiftPackageReference "HashRainbow" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/saramah/HashRainbow.git"; + requirement = { + branch = main; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -774,6 +822,11 @@ package = D89224982BD70CC300DA0932 /* XCRemoteSwiftPackageReference "Yams" */; productName = Yams; }; + D8C283E82BD9954F00161C82 /* HashRainbow */ = { + isa = XCSwiftPackageProductDependency; + package = D8C283E72BD9954F00161C82 /* XCRemoteSwiftPackageReference "HashRainbow" */; + productName = HashRainbow; + }; D8F06D4A2B67275B0039AEF4 /* Diligence */ = { isa = XCSwiftPackageProductDependency; productName = Diligence; diff --git a/Thoughts/Layouts/CenteredFlowLayout.swift b/Thoughts/Layouts/CenteredFlowLayout.swift new file mode 100644 index 0000000..23a7301 --- /dev/null +++ b/Thoughts/Layouts/CenteredFlowLayout.swift @@ -0,0 +1,100 @@ +// Copyright (c) 2022-2024 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +extension LayoutSubviews { + + typealias SubviewDetails = (LayoutSubviews.Element, CGSize) + + func rows(proposal: ProposedViewSize, spacing: CGFloat) -> [[SubviewDetails]] { + guard let width = proposal.width else { + return [[]] + } + + let details = self.map { + return ($0, $0.sizeThatFits(.unspecified)) + } + + var rows: [[SubviewDetails]] = [] + var row: [SubviewDetails] = [] + var rowWidth: CGFloat = -spacing + for (subview, size) in details { + if rowWidth + spacing + size.width > width { + rows.append(row) + row = [] + rowWidth = 0.0 + } + row.append((subview, size)) + rowWidth += spacing + size.width + } + if !row.isEmpty { + rows.append(row) + } + return rows + } + +} + +struct CenteredFlowLayout: Layout { + + let spacing: CGFloat = 4.0 + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { + let rows = subviews.rows(proposal: proposal, spacing: spacing) + + let width = rows + .map { row in + row.map { subview, size in + size.width + } + .reduce(0.0, +) + } + .reduce(0.0, max) + + let height = rows + .map { row in + row.map { subview, size in + size.height + } + .reduce(0.0, max) + } + .reduce(0.0, +) + + print("size = (\(width), \(height))") + + return CGSize(width: width, height: height) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) { + var posY = bounds.minY + for row in subviews.rows(proposal: proposal, spacing: spacing) { + let rowHeight = row.map({ $1.height }).reduce(0.0, max) + var posX = bounds.minX + for (subview, size) in row { + subview.place(at: CGPoint(x: posX, y: posY + rowHeight), + anchor: .bottomLeading, + proposal: ProposedViewSize(size)) + posX += size.width + spacing + } + posY += rowHeight + } + } +} diff --git a/Thoughts/Model/ApplicationModel.swift b/Thoughts/Model/ApplicationModel.swift index 46ae22c..b58b25c 100644 --- a/Thoughts/Model/ApplicationModel.swift +++ b/Thoughts/Model/ApplicationModel.swift @@ -163,13 +163,11 @@ extension ApplicationModel: CLLocationManagerDelegate { func resolveLocation(_ location: CLLocation) async -> LocationDetails { let geocoder = CLGeocoder() do { - guard let placemark = try await geocoder.reverseGeocodeLocation(location).first else { - return LocationDetails(location) - } - return LocationDetails(placemark) + return LocationDetails(location: location, + placemark: try await geocoder.reverseGeocodeLocation(location).first) } catch { print("Failed to geocode location with error \(error).") - return LocationDetails(location) + return LocationDetails(location: location) } } diff --git a/Thoughts/Model/Document.swift b/Thoughts/Model/Document.swift index e294974..e6081c5 100644 --- a/Thoughts/Model/Document.swift +++ b/Thoughts/Model/Document.swift @@ -27,7 +27,7 @@ struct Document { var date: Date var content: String - var tags: String + var tags: [String] var location: LocationDetails? = nil var isEmpty: Bool { @@ -37,14 +37,11 @@ struct Document { init(date: Date = Date()) { self.date = date self.content = "" - self.tags = "" + self.tags = [] } func header() -> String { do { - let tags = tags - .split(separator: /\s+/) - .map { String($0)} let metadata = Metadata(date: RegionalDate(date, timeZone: .current), tags: tags, location: location) diff --git a/Thoughts/Model/LocationDetails.swift b/Thoughts/Model/LocationDetails.swift index 919174f..f8e2655 100644 --- a/Thoughts/Model/LocationDetails.swift +++ b/Thoughts/Model/LocationDetails.swift @@ -23,20 +23,24 @@ import Foundation struct LocationDetails: Codable { - var latitude: CLLocationDegrees? - var longitude: CLLocationDegrees? + var summary: String { + if let name, let locality { + return [name, locality].joined(separator: ", ") + } else { + return "(\(String(format: "%.3f", latitude)), \(String(format: "%.3f", longitude)))" + } + } + + var latitude: CLLocationDegrees + var longitude: CLLocationDegrees + var name: String? var locality: String? - init(_ location: CLLocation) { + init(location: CLLocation, placemark: CLPlacemark? = nil) { self.latitude = location.coordinate.latitude self.longitude = location.coordinate.longitude - self.locality = nil - } - - init(_ placemark: CLPlacemark) { - self.latitude = placemark.location?.coordinate.latitude - self.longitude = placemark.location?.coordinate.longitude - self.locality = placemark.locality + self.name = placemark?.name + self.locality = placemark?.locality } } diff --git a/Thoughts/Model/TokenViewModel.swift b/Thoughts/Model/TokenViewModel.swift new file mode 100644 index 0000000..67726f0 --- /dev/null +++ b/Thoughts/Model/TokenViewModel.swift @@ -0,0 +1,115 @@ +// Copyright (c) 2020-2024 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Combine +import SwiftUI + +import Interact + +extension String { + public func containsWhitespaceAndNewlines() -> Bool { + return rangeOfCharacter(from: .whitespacesAndNewlines) != nil + } +} + +class TokenViewModel: ObservableObject, Runnable { + + struct Token: Identifiable { + + let id = UUID() + let text: String + + init(_ text: String) { + self.text = text + } + + } + + @Binding var tokens: [String] + + @Published var items: [Token] = [] + @Published var input: String = "" + @Published var suggestions: [String] = [] + + let suggestion: (String, [String], Int) -> [String] + var cancellables: [AnyCancellable] = [] + + init(tokens: Binding<[String]>, suggestion: @escaping (String, [String], Int) -> [String]) { + self._tokens = tokens + self.suggestion = suggestion + self.items = tokens.wrappedValue + .map { Token($0) } + } + + func start() { + + $input + .receive(on: DispatchQueue.main) + .filter { $0.containsWhitespaceAndNewlines() } + .sink { [weak self] tags in + guard let self else { return } + self.commit() + } + .store(in: &cancellables) + + $input + .combineLatest($items) + .receive(on: DispatchQueue.main) + .map { [suggestion] input, items in + return suggestion(input, items.map({ $0.text }), 8) + } + .assign(to: \.suggestions, on: self) + .store(in: &cancellables) + + $items + .dropFirst() + .receive(on: DispatchQueue.main) + .map { $0.map { $0.text } } + .assign(to: \.tokens, on: self) + .store(in: &cancellables) + } + + func stop() { + cancellables = [] + } + + func commit() { + let tags = input + .components(separatedBy: .whitespacesAndNewlines) + .filter { !$0.isEmpty } + for tag in tags { + self.items.append(Token(tag)) + } + input = "" + } + + @MainActor func acceptSuggestion(_ token: String) { + items.append(TokenViewModel.Token(token)) + input = "" + } + + func deleteBackwards() { + guard !items.isEmpty else { + return + } + items.removeLast() + } + +} diff --git a/Thoughts/Views/ComposeView.swift b/Thoughts/Views/ComposeView.swift index 0f68b1a..c12e63b 100644 --- a/Thoughts/Views/ComposeView.swift +++ b/Thoughts/Views/ComposeView.swift @@ -28,6 +28,16 @@ struct ComposeView: View { self.applicationModel = applicationModel } + var subtitle: String { + var components: [String] = [ + applicationModel.document.date.formatted(date: .omitted, time: .standard) + ] + if let locationSummary = applicationModel.document.location?.summary { + components.append(locationSummary) + } + return components.joined(separator: ", ") + } + var body: some View { @Bindable var applicationModel = applicationModel VStack(spacing: 0) { @@ -38,31 +48,25 @@ struct ComposeView: View { .monospaced() .edgesIgnoringSafeArea(.all) Divider() + .padding(.horizontal) HStack { - TextField("Tags", text: $applicationModel.document.tags) - .textFieldStyle(.plain) + TokenView("Add tags...", tokens: $applicationModel.document.tags) Spacer() - HStack { - if let location = applicationModel.document.location { - if let locality = location.locality { - Text(locality) - } else if let latitude = location.latitude, - let longitude = location.longitude { - Text("\(latitude), \(longitude)") - } - } else if applicationModel.shouldSaveLocation { - ProgressView() - .controlSize(.small) - } - } - .foregroundStyle(.secondary) } .padding() - .background(.windowBackground) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(.background) .navigationTitle(applicationModel.document.date.formatted(date: .complete, time: .standard)) + .navigationSubtitle(applicationModel.document.location?.summary ?? "") + .toolbar { + if applicationModel.shouldSaveLocation && applicationModel.document.location == nil { + ToolbarItem(placement: .navigation) { + ProgressView() + .controlSize(.small) + } + } + } } } diff --git a/Thoughts/Views/PickerTextField.swift b/Thoughts/Views/PickerTextField.swift new file mode 100644 index 0000000..28e72cb --- /dev/null +++ b/Thoughts/Views/PickerTextField.swift @@ -0,0 +1,140 @@ +// Copyright (c) 2020-2024 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#if os(macOS) + +import Carbon +import SwiftUI + +struct PickerTextField: NSViewRepresentable { + + class Coordinator: NSObject, NSTextFieldDelegate, NSControlTextEditingDelegate { + + var isDeleting = false + + let parent: PickerTextField + + var lastUserInput = "" + var lastSuggestion = "" + + init(_ parent: PickerTextField) { + self.parent = parent + } + + func control(_ control: NSControl, textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool { + guard let event = textView.window?.currentEvent else { + return false + } + + if event.keyCode == kVK_Delete { + + isDeleting = true + + // We're only intereseted in delete events when the cursor is at the beginning of the text. + guard textView.selectedRanges.count == 1, + let range = textView.selectedRanges.first as? NSRange, + range.location == 0, + range.length == 0 + else { + return false + } + parent.onDelete() + } else if event.keyCode == kVK_Return || event.keyCode == kVK_Tab { + parent.onCommit() + } + + return false + } + + func controlTextDidChange(_ notification: Notification) { + guard let textField = notification.object as? NSTextField else { + print("unexpected control in update notification") + return + } + + self.parent.text = textField.stringValue + + guard let textView = textField.currentEditor() as? NSTextView else { + return + } + + guard !textField.stringValue.isEmpty, + !isDeleting, + let suggestion = parent.suggestion(textField.stringValue).first, + suggestion.starts(with: textField.stringValue) + else { + isDeleting = false + return + } + lastSuggestion = suggestion + textView.insertCompletion(suggestion, + forPartialWordRange: NSRange(location: 0, length: textField.stringValue.count), + movement: 0, + isFinal: false) + } + + } + + let prompt: String + + @Binding var text: String + + var onDelete: () -> Void + var onCommit: () -> Void + var suggestion: (String) -> [String] + + init(_ prompt: String, + text: Binding, + onCommit: @escaping () -> Void, + onDelete: @escaping () -> Void, + suggestion: @escaping (String) -> [String]) { + self.prompt = prompt + _text = text + self.onCommit = onCommit + self.onDelete = onDelete + self.suggestion = suggestion + } + + func makeNSView(context: Context) -> NSTextField { + let textField = NSTextField() + textField.delegate = context.coordinator + textField.isBordered = false + textField.isBezeled = false + textField.focusRingType = .none + textField.placeholderString = prompt + textField.backgroundColor = .clear + textField.font = NSFont.preferredFont(forTextStyle: .body) + return textField + } + + func updateNSView(_ textField: NSTextField, context: Context) { + if textField.stringValue != text { + textField.stringValue = text + } + + } + + func makeCoordinator() -> Coordinator { + return Coordinator(self) + } + +} + +#endif diff --git a/Thoughts/Views/TagView.swift b/Thoughts/Views/TagView.swift new file mode 100644 index 0000000..6fbf72c --- /dev/null +++ b/Thoughts/Views/TagView.swift @@ -0,0 +1,393 @@ +// Copyright (c) 2020-2024 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +import HashRainbow + +extension Color { + + struct HTML { + + public static let aliceBlue = Color(0xF0F8FF) + public static let antiqueWhite = Color(0xFAEBD7) + public static let aqua = Color(0x00FFFF) + public static let aquamarine = Color(0x7FFFD4) + public static let azure = Color(0xF0FFFF) + public static let beige = Color(0xF5F5DC) + public static let bisque = Color(0xFFE4C4) + public static let black = Color(0x000000) + public static let blanchedAlmond = Color(0xFFEBCD) + public static let blue = Color(0x0000FF) + public static let blueViolet = Color(0x8A2BE2) + public static let brown = Color(0xA52A2A) + public static let burlyWood = Color(0xDEB887) + public static let cadetBlue = Color(0x5F9EA0) + public static let chartreuse = Color(0x7FFF00) + public static let chocolate = Color(0xD2691E) + public static let coral = Color(0xFF7F50) + public static let cornflowerBlue = Color(0x6495ED) + public static let cornsilk = Color(0xFFF8DC) + public static let crimson = Color(0xDC143C) + public static let cyan = Color(0x00FFFF) + public static let darkBlue = Color(0x00008B) + public static let darkCyan = Color(0x008B8B) + public static let darkGoldenRod = Color(0xB8860B) + public static let darkGray = Color(0xA9A9A9) + public static let darkGrey = Color(0xA9A9A9) + public static let darkGreen = Color(0x006400) + public static let darkKhaki = Color(0xBDB76B) + public static let darkMagenta = Color(0x8B008B) + public static let darkOliveGreen = Color(0x556B2F) + public static let darkOrange = Color(0xFF8C00) + public static let darkOrchid = Color(0x9932CC) + public static let darkRed = Color(0x8B0000) + public static let darkSalmon = Color(0xE9967A) + public static let darkSeaGreen = Color(0x8FBC8F) + public static let darkSlateBlue = Color(0x483D8B) + public static let darkSlateGray = Color(0x2F4F4F) + public static let darkSlateGrey = Color(0x2F4F4F) + public static let darkTurquoise = Color(0x00CED1) + public static let darkViolet = Color(0x9400D3) + public static let deepPink = Color(0xFF1493) + public static let deepSkyBlue = Color(0x00BFFF) + public static let dimGray = Color(0x696969) + public static let dimGrey = Color(0x696969) + public static let dodgerBlue = Color(0x1E90FF) + public static let fireBrick = Color(0xB22222) + public static let floralWhite = Color(0xFFFAF0) + public static let forestGreen = Color(0x228B22) + public static let fuchsia = Color(0xFF00FF) + public static let gainsboro = Color(0xDCDCDC) + public static let ghostWhite = Color(0xF8F8FF) + public static let gold = Color(0xFFD700) + public static let goldenRod = Color(0xDAA520) + public static let gray = Color(0x808080) + public static let grey = Color(0x808080) + public static let green = Color(0x008000) + public static let greenYellow = Color(0xADFF2F) + public static let honeyDew = Color(0xF0FFF0) + public static let hotPink = Color(0xFF69B4) + public static let indianRed = Color(0xCD5C5C) + public static let indigo = Color(0x4B0082) + public static let ivory = Color(0xFFFFF0) + public static let khaki = Color(0xF0E68C) + public static let lavender = Color(0xE6E6FA) + public static let lavenderBlush = Color(0xFFF0F5) + public static let lawnGreen = Color(0x7CFC00) + public static let lemonChiffon = Color(0xFFFACD) + public static let lightBlue = Color(0xADD8E6) + public static let lightCoral = Color(0xF08080) + public static let lightCyan = Color(0xE0FFFF) + public static let lightGoldenRodYellow = Color(0xFAFAD2) + public static let lightGray = Color(0xD3D3D3) + public static let lightGrey = Color(0xD3D3D3) + public static let lightGreen = Color(0x90EE90) + public static let lightPink = Color(0xFFB6C1) + public static let lightSalmon = Color(0xFFA07A) + public static let lightSeaGreen = Color(0x20B2AA) + public static let lightSkyBlue = Color(0x87CEFA) + public static let lightSlateGray = Color(0x778899) + public static let lightSlateGrey = Color(0x778899) + public static let lightSteelBlue = Color(0xB0C4DE) + public static let lightYellow = Color(0xFFFFE0) + public static let lime = Color(0x00FF00) + public static let limeGreen = Color(0x32CD32) + public static let linen = Color(0xFAF0E6) + public static let magenta = Color(0xFF00FF) + public static let maroon = Color(0x800000) + public static let mediumAquaMarine = Color(0x66CDAA) + public static let mediumBlue = Color(0x0000CD) + public static let mediumOrchid = Color(0xBA55D3) + public static let mediumPurple = Color(0x9370DB) + public static let mediumSeaGreen = Color(0x3CB371) + public static let mediumSlateBlue = Color(0x7B68EE) + public static let mediumSpringGreen = Color(0x00FA9A) + public static let mediumTurquoise = Color(0x48D1CC) + public static let mediumVioletRed = Color(0xC71585) + public static let midnightBlue = Color(0x191970) + public static let mintCream = Color(0xF5FFFA) + public static let mistyRose = Color(0xFFE4E1) + public static let moccasin = Color(0xFFE4B5) + public static let navajoWhite = Color(0xFFDEAD) + public static let navy = Color(0x000080) + public static let oldLace = Color(0xFDF5E6) + public static let olive = Color(0x808000) + public static let oliveDrab = Color(0x6B8E23) + public static let orange = Color(0xFFA500) + public static let orangeRed = Color(0xFF4500) + public static let orchid = Color(0xDA70D6) + public static let paleGoldenRod = Color(0xEEE8AA) + public static let paleGreen = Color(0x98FB98) + public static let paleTurquoise = Color(0xAFEEEE) + public static let paleVioletRed = Color(0xDB7093) + public static let papayaWhip = Color(0xFFEFD5) + public static let peachPuff = Color(0xFFDAB9) + public static let peru = Color(0xCD853F) + public static let pink = Color(0xFFC0CB) + public static let plum = Color(0xDDA0DD) + public static let powderBlue = Color(0xB0E0E6) + public static let purple = Color(0x800080) + public static let rebeccaPurple = Color(0x663399) + public static let red = Color(0xFF0000) + public static let rosyBrown = Color(0xBC8F8F) + public static let royalBlue = Color(0x4169E1) + public static let saddleBrown = Color(0x8B4513) + public static let salmon = Color(0xFA8072) + public static let sandyBrown = Color(0xF4A460) + public static let seaGreen = Color(0x2E8B57) + public static let seaShell = Color(0xFFF5EE) + public static let sienna = Color(0xA0522D) + public static let silver = Color(0xC0C0C0) + public static let skyBlue = Color(0x87CEEB) + public static let slateBlue = Color(0x6A5ACD) + public static let slateGray = Color(0x708090) + public static let slateGrey = Color(0x708090) + public static let snow = Color(0xFFFAFA) + public static let springGreen = Color(0x00FF7F) + public static let steelBlue = Color(0x4682B4) + public static let tan = Color(0xD2B48C) + public static let teal = Color(0x008080) + public static let thistle = Color(0xD8BFD8) + public static let tomato = Color(0xFF6347) + public static let turquoise = Color(0x40E0D0) + public static let violet = Color(0xEE82EE) + public static let wheat = Color(0xF5DEB3) + public static let white = Color(0xFFFFFF) + public static let whiteSmoke = Color(0xF5F5F5) + public static let yellow = Color(0xFFFF00) + public static let yellowGreen = Color(0x9ACD32) + + } + + init(_ value: Int32) { + let red = Double(((0xff << 16) & value) >> 16) + let green = Double(((0xff << 8) & value) >> 8) + let blue = Double(0xff & value) + self.init(red: red / 255, green: green / 255, blue: blue / 255) + } + + var gray: CGFloat? { + return cgColor?.converted(to: CGColorSpaceCreateDeviceGray(), + intent: CGColorRenderingIntent.defaultIntent, + options: nil)?.components?.first + } + +} + +extension Array where Element == Color { + + static let html: [Color] = [ + .HTML.aliceBlue, + .HTML.antiqueWhite, + .HTML.aqua, + .HTML.aquamarine, + .HTML.azure, + .HTML.beige, + .HTML.bisque, + .HTML.black, + .HTML.blanchedAlmond, + .HTML.blue, + .HTML.blueViolet, + .HTML.brown, + .HTML.burlyWood, + .HTML.cadetBlue, + .HTML.chartreuse, + .HTML.chocolate, + .HTML.coral, + .HTML.cornflowerBlue, + .HTML.cornsilk, + .HTML.crimson, + .HTML.cyan, + .HTML.darkBlue, + .HTML.darkCyan, + .HTML.darkGoldenRod, + .HTML.darkGray, + .HTML.darkGrey, + .HTML.darkGreen, + .HTML.darkKhaki, + .HTML.darkMagenta, + .HTML.darkOliveGreen, + .HTML.darkOrange, + .HTML.darkOrchid, + .HTML.darkRed, + .HTML.darkSalmon, + .HTML.darkSeaGreen, + .HTML.darkSlateBlue, + .HTML.darkSlateGray, + .HTML.darkSlateGrey, + .HTML.darkTurquoise, + .HTML.darkViolet, + .HTML.deepPink, + .HTML.deepSkyBlue, + .HTML.dimGray, + .HTML.dimGrey, + .HTML.dodgerBlue, + .HTML.fireBrick, + .HTML.floralWhite, + .HTML.forestGreen, + .HTML.fuchsia, + .HTML.gainsboro, + .HTML.ghostWhite, + .HTML.gold, + .HTML.goldenRod, + .HTML.gray, + .HTML.grey, + .HTML.green, + .HTML.greenYellow, + .HTML.honeyDew, + .HTML.hotPink, + .HTML.indianRed, + .HTML.indigo, + .HTML.ivory, + .HTML.khaki, + .HTML.lavender, + .HTML.lavenderBlush, + .HTML.lawnGreen, + .HTML.lemonChiffon, + .HTML.lightBlue, + .HTML.lightCoral, + .HTML.lightCyan, + .HTML.lightGoldenRodYellow, + .HTML.lightGray, + .HTML.lightGrey, + .HTML.lightGreen, + .HTML.lightPink, + .HTML.lightSalmon, + .HTML.lightSeaGreen, + .HTML.lightSkyBlue, + .HTML.lightSlateGray, + .HTML.lightSlateGrey, + .HTML.lightSteelBlue, + .HTML.lightYellow, + .HTML.lime, + .HTML.limeGreen, + .HTML.linen, + .HTML.magenta, + .HTML.maroon, + .HTML.mediumAquaMarine, + .HTML.mediumBlue, + .HTML.mediumOrchid, + .HTML.mediumPurple, + .HTML.mediumSeaGreen, + .HTML.mediumSlateBlue, + .HTML.mediumSpringGreen, + .HTML.mediumTurquoise, + .HTML.mediumVioletRed, + .HTML.midnightBlue, + .HTML.mintCream, + .HTML.mistyRose, + .HTML.moccasin, + .HTML.navajoWhite, + .HTML.navy, + .HTML.oldLace, + .HTML.olive, + .HTML.oliveDrab, + .HTML.orange, + .HTML.orangeRed, + .HTML.orchid, + .HTML.paleGoldenRod, + .HTML.paleGreen, + .HTML.paleTurquoise, + .HTML.paleVioletRed, + .HTML.papayaWhip, + .HTML.peachPuff, + .HTML.peru, + .HTML.pink, + .HTML.plum, + .HTML.powderBlue, + .HTML.purple, + .HTML.rebeccaPurple, + .HTML.red, + .HTML.rosyBrown, + .HTML.royalBlue, + .HTML.saddleBrown, + .HTML.salmon, + .HTML.sandyBrown, + .HTML.seaGreen, + .HTML.seaShell, + .HTML.sienna, + .HTML.silver, + .HTML.skyBlue, + .HTML.slateBlue, + .HTML.slateGray, + .HTML.slateGrey, + .HTML.snow, + .HTML.springGreen, + .HTML.steelBlue, + .HTML.tan, + .HTML.teal, + .HTML.thistle, + .HTML.tomato, + .HTML.turquoise, + .HTML.violet, + .HTML.wheat, + .HTML.white, + .HTML.whiteSmoke, + .HTML.yellow, + .HTML.yellowGreen, + ] + + static let tags: [Color] = { + return html.filter { color in + guard let gray = color.gray, + gray < 0.9, + gray > 0.1 + else { + return false + } + return true + } + }() + +} + + +public struct TagView: View { + + struct LayoutMetrics { + static let cornerRadius = 4.0 + } + + let text: String + let color: Color + + public init(_ text: String) { + self.text = text + self.color = HashRainbow.colorForString(text, colors: .tags) // TODO: Inject the color palette. + } + + public var body: some View { + HStack(spacing: 0) { + Text(text) + .lineLimit(1) + .padding([.top, .bottom], 2) + .padding([.leading, .trailing], 6) + } + .padding(2) + .foregroundColor(color) + .background(color + .opacity(0.3) + .background(.background)) + .clipShape(RoundedRectangle(cornerRadius: LayoutMetrics.cornerRadius)) + .fixedSize(horizontal: true, vertical: true) + } + +} diff --git a/Thoughts/Views/TokenView.swift b/Thoughts/Views/TokenView.swift new file mode 100644 index 0000000..dbb01bf --- /dev/null +++ b/Thoughts/Views/TokenView.swift @@ -0,0 +1,118 @@ +// Copyright (c) 2020-2024 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Combine +import SwiftUI + +public struct TokenView: View { + +#if os(iOS) + enum SheetType: Identifiable { + + public var id: Self { + return self + } + + case addTag + } +#endif + + let prompt: String + + @Binding var tokens: [String] + + let suggestion: (String, [String], Int) -> [String] + + @StateObject var model: TokenViewModel +#if os(iOS) + @State var sheet: SheetType? +#endif + + public init(_ prompt: String, + tokens: Binding<[String]>, + suggestion: @escaping (String, [String], Int) -> [String] = { _, _, _ in [] } ) { + self.prompt = prompt + _tokens = tokens + self.suggestion = suggestion + _model = StateObject(wrappedValue: TokenViewModel(tokens: tokens, suggestion: suggestion)) + } + + public var body: some View { +#if os(macOS) + VStack { + CenteredFlowLayout { + ForEach(model.items) { item in + TagView(item.text) + } + PickerTextField(prompt, text: $model.input) { + model.commit() + } onDelete: { + model.deleteBackwards() + } suggestion: { candidate in + return suggestion(candidate, model.items.map({ $0.text }), 1) + } + .frame(width: 100) + .padding([.top, .bottom], 4) + } + } + .runs(model) + .onChange(of: tokens) { oldValue, newValue in + // I find it really hard to believe that this is the idiomatic way of bidirectional binding. + guard model.items.map({ $0.text }) != newValue else { + return + } + model.items = newValue.map({ TokenViewModel.Token($0) }) + } +#else + + VStack { + Button { + sheet = .addTag + } label: { + if !tokens.isEmpty { + WrappingHStack(alignment: .leading) { + ForEach(tokens.sorted()) { tag in + TagView(tag) + } + } + } else { + Text("Tags") + .foregroundColor(.secondary) + } + } + } + .sheet(item: $sheet) { sheet in + switch sheet { + case .addTag: + PhoneEditTagsView(tokenViewModel: model, tags: $tokens) + } + } + .runs(model) + .onChange(of: tokens) { tokens in + // I find it really hard to believe that this is the idiomatic way of bidirectional binding. + guard model.items.map({ $0.text }) != tokens else { + return + } + model.items = tokens.map({ TokenViewModel.Token($0) }) + } +#endif + } + +} From 088a49c28ab61130cd1ec157ab488c44761f573d Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Wed, 24 Apr 2024 11:11:17 -1000 Subject: [PATCH 2/5] Layout tweaks --- Thoughts.xcodeproj/project.pbxproj | 4 ++-- Thoughts/Layouts/CenteredFlowLayout.swift | 6 +++++- Thoughts/Views/ComposeView.swift | 1 - Thoughts/Views/TokenView.swift | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Thoughts.xcodeproj/project.pbxproj b/Thoughts.xcodeproj/project.pbxproj index f9ed50f..98ae19d 100644 --- a/Thoughts.xcodeproj/project.pbxproj +++ b/Thoughts.xcodeproj/project.pbxproj @@ -254,10 +254,10 @@ D80E08A02B675AAC0023DD4F /* ComposeWindow.swift */, D850C2D52B69D8D500338546 /* ContentView.swift */, D852AF682B604B3E00B77A3D /* MainMenu.swift */, - D8F06D502B6731CE0039AEF4 /* SettingsView.swift */, - D8C283E42BD9939E00161C82 /* TokenView.swift */, D8C283EF2BD995AD00161C82 /* PickerTextField.swift */, + D8F06D502B6731CE0039AEF4 /* SettingsView.swift */, D8C283F12BD995CC00161C82 /* TagView.swift */, + D8C283E42BD9939E00161C82 /* TokenView.swift */, ); path = Views; sourceTree = ""; diff --git a/Thoughts/Layouts/CenteredFlowLayout.swift b/Thoughts/Layouts/CenteredFlowLayout.swift index 23a7301..234bb53 100644 --- a/Thoughts/Layouts/CenteredFlowLayout.swift +++ b/Thoughts/Layouts/CenteredFlowLayout.swift @@ -55,7 +55,11 @@ extension LayoutSubviews { struct CenteredFlowLayout: Layout { - let spacing: CGFloat = 4.0 + let spacing: CGFloat + + init(spacing: CGFloat) { + self.spacing = spacing + } func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize { let rows = subviews.rows(proposal: proposal, spacing: spacing) diff --git a/Thoughts/Views/ComposeView.swift b/Thoughts/Views/ComposeView.swift index c12e63b..ea83a3b 100644 --- a/Thoughts/Views/ComposeView.swift +++ b/Thoughts/Views/ComposeView.swift @@ -48,7 +48,6 @@ struct ComposeView: View { .monospaced() .edgesIgnoringSafeArea(.all) Divider() - .padding(.horizontal) HStack { TokenView("Add tags...", tokens: $applicationModel.document.tags) Spacer() diff --git a/Thoughts/Views/TokenView.swift b/Thoughts/Views/TokenView.swift index dbb01bf..31bd0fd 100644 --- a/Thoughts/Views/TokenView.swift +++ b/Thoughts/Views/TokenView.swift @@ -57,7 +57,7 @@ public struct TokenView: View { public var body: some View { #if os(macOS) VStack { - CenteredFlowLayout { + CenteredFlowLayout(spacing: 4.0) { ForEach(model.items) { item in TagView(item.text) } From 4d5ae5af0ad9d1ee9464315ba3d113e597c4879e Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Wed, 24 Apr 2024 11:13:33 -1000 Subject: [PATCH 3/5] Update the copyright --- Thoughts/Layouts/CenteredFlowLayout.swift | 2 +- Thoughts/Model/TokenViewModel.swift | 2 +- Thoughts/Views/PickerTextField.swift | 2 +- Thoughts/Views/TagView.swift | 2 +- Thoughts/Views/TokenView.swift | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Thoughts/Layouts/CenteredFlowLayout.swift b/Thoughts/Layouts/CenteredFlowLayout.swift index 234bb53..20d910c 100644 --- a/Thoughts/Layouts/CenteredFlowLayout.swift +++ b/Thoughts/Layouts/CenteredFlowLayout.swift @@ -1,4 +1,4 @@ -// Copyright (c) 2022-2024 Jason Morley +// Copyright (c) 2024 Jason Morley // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/Thoughts/Model/TokenViewModel.swift b/Thoughts/Model/TokenViewModel.swift index 67726f0..681b0e0 100644 --- a/Thoughts/Model/TokenViewModel.swift +++ b/Thoughts/Model/TokenViewModel.swift @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Jason Morley +// Copyright (c) 2024 Jason Morley // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/Thoughts/Views/PickerTextField.swift b/Thoughts/Views/PickerTextField.swift index 28e72cb..f6979c0 100644 --- a/Thoughts/Views/PickerTextField.swift +++ b/Thoughts/Views/PickerTextField.swift @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Jason Morley +// Copyright (c) 2024 Jason Morley // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/Thoughts/Views/TagView.swift b/Thoughts/Views/TagView.swift index 6fbf72c..6289db0 100644 --- a/Thoughts/Views/TagView.swift +++ b/Thoughts/Views/TagView.swift @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Jason Morley +// Copyright (c) 2024 Jason Morley // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal diff --git a/Thoughts/Views/TokenView.swift b/Thoughts/Views/TokenView.swift index 31bd0fd..77edd75 100644 --- a/Thoughts/Views/TokenView.swift +++ b/Thoughts/Views/TokenView.swift @@ -1,4 +1,4 @@ -// Copyright (c) 2020-2024 Jason Morley +// Copyright (c) 2024 Jason Morley // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal From 27a6cf1f3a03be320795c5a3840465e98b049a0a Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Wed, 24 Apr 2024 11:17:49 -1000 Subject: [PATCH 4/5] Add the HashRainbow license --- Thoughts.xcodeproj/project.pbxproj | 4 ++++ Thoughts/Licenses/hashrainbow-license | 21 +++++++++++++++++++++ Thoughts/ThoughtsApp.swift | 3 ++- 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 Thoughts/Licenses/hashrainbow-license diff --git a/Thoughts.xcodeproj/project.pbxproj b/Thoughts.xcodeproj/project.pbxproj index 98ae19d..935b54d 100644 --- a/Thoughts.xcodeproj/project.pbxproj +++ b/Thoughts.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ D8C283F02BD995AD00161C82 /* PickerTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C283EF2BD995AD00161C82 /* PickerTextField.swift */; }; D8C283F22BD995CC00161C82 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C283F12BD995CC00161C82 /* TagView.swift */; }; D8C283F52BD9988200161C82 /* CenteredFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8C283F42BD9988200161C82 /* CenteredFlowLayout.swift */; }; + D8C283F72BD9ADC400161C82 /* hashrainbow-license in Resources */ = {isa = PBXBuildFile; fileRef = D8C283F62BD9ADC400161C82 /* hashrainbow-license */; }; D8DFFCC82BD82DE900C9532A /* CLAuthorizationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */; }; D8F06D4B2B67275B0039AEF4 /* Diligence in Frameworks */ = {isa = PBXBuildFile; productRef = D8F06D4A2B67275B0039AEF4 /* Diligence */; }; D8F06D4E2B6727630039AEF4 /* Interact in Frameworks */ = {isa = PBXBuildFile; productRef = D8F06D4D2B6727630039AEF4 /* Interact */; }; @@ -96,6 +97,7 @@ D8C283EF2BD995AD00161C82 /* PickerTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickerTextField.swift; sourceTree = ""; }; D8C283F12BD995CC00161C82 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = ""; }; D8C283F42BD9988200161C82 /* CenteredFlowLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenteredFlowLayout.swift; sourceTree = ""; }; + D8C283F62BD9ADC400161C82 /* hashrainbow-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "hashrainbow-license"; sourceTree = ""; }; D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLAuthorizationStatus.swift; sourceTree = ""; }; D8F06D502B6731CE0039AEF4 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; D8F06D532B6733650039AEF4 /* thoughts-license */ = {isa = PBXFileReference; lastKnownFileType = text; path = "thoughts-license"; sourceTree = ""; }; @@ -268,6 +270,7 @@ D8F06D552B6734540039AEF4 /* material-icons-license */, D8F06D532B6733650039AEF4 /* thoughts-license */, D84010C42BD74C760049715E /* yams-license */, + D8C283F62BD9ADC400161C82 /* hashrainbow-license */, ); path = Licenses; sourceTree = ""; @@ -393,6 +396,7 @@ D8F06D562B6734540039AEF4 /* material-icons-license in Resources */, D852AF062B6027CC00B77A3D /* Assets.xcassets in Resources */, D84010C52BD74C760049715E /* yams-license in Resources */, + D8C283F72BD9ADC400161C82 /* hashrainbow-license in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Thoughts/Licenses/hashrainbow-license b/Thoughts/Licenses/hashrainbow-license new file mode 100644 index 0000000..6da8e5f --- /dev/null +++ b/Thoughts/Licenses/hashrainbow-license @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Sarah Barbour + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Thoughts/ThoughtsApp.swift b/Thoughts/ThoughtsApp.swift index 5a1d1e8..1a37fbf 100644 --- a/Thoughts/ThoughtsApp.swift +++ b/Thoughts/ThoughtsApp.swift @@ -59,7 +59,8 @@ struct ThoughtsApp: App { Credit("Sarah Barbour") } } licenses: { - .interact + (.interact) + License("HashRainbow", author: "Sarah Barbour", filename: "hashrainbow-license") License("Material Icons", author: "Google", filename: "material-icons-license") License("Thoughts", author: "Jason Morley", filename: "thoughts-license") License("Yams", author: "JP Simard", filename: "yams-license") From 1c637b1a7d453807a57396f823c14f58554e173c Mon Sep 17 00:00:00 2001 From: Jason Morley Date: Wed, 24 Apr 2024 11:27:02 -1000 Subject: [PATCH 5/5] Color extension --- Thoughts.xcodeproj/project.pbxproj | 4 + Thoughts/Extensions/Color.swift | 360 +++++++++++++++++++++++++++++ Thoughts/Views/TagView.swift | 338 --------------------------- 3 files changed, 364 insertions(+), 338 deletions(-) create mode 100644 Thoughts/Extensions/Color.swift diff --git a/Thoughts.xcodeproj/project.pbxproj b/Thoughts.xcodeproj/project.pbxproj index 935b54d..2405b6d 100644 --- a/Thoughts.xcodeproj/project.pbxproj +++ b/Thoughts.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ D852AF642B604AF500B77A3D /* ApplicationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF632B604AF500B77A3D /* ApplicationModel.swift */; }; D852AF662B604B1200B77A3D /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF652B604B1200B77A3D /* ComposeView.swift */; }; D852AF692B604B3E00B77A3D /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = D852AF682B604B3E00B77A3D /* MainMenu.swift */; }; + D87310CF2BD9B03300E2064A /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = D87310CE2BD9B03300E2064A /* Color.swift */; }; D892249A2BD70CC300DA0932 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = D89224992BD70CC300DA0932 /* Yams */; }; D892249C2BD735A100DA0932 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = D892249B2BD735A100DA0932 /* Metadata.swift */; }; D892249E2BD735BE00DA0932 /* ThoughtsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D892249D2BD735BE00DA0932 /* ThoughtsError.swift */; }; @@ -85,6 +86,7 @@ D852AF632B604AF500B77A3D /* ApplicationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationModel.swift; sourceTree = ""; }; D852AF652B604B1200B77A3D /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; D852AF682B604B3E00B77A3D /* MainMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainMenu.swift; sourceTree = ""; }; + D87310CE2BD9B03300E2064A /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; D892249B2BD735A100DA0932 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = ""; }; D892249D2BD735BE00DA0932 /* ThoughtsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThoughtsError.swift; sourceTree = ""; }; D892249F2BD735DA00DA0932 /* RegionalDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionalDate.swift; sourceTree = ""; }; @@ -152,6 +154,7 @@ isa = PBXGroup; children = ( D8DFFCC72BD82DE900C9532A /* CLAuthorizationStatus.swift */, + D87310CE2BD9B03300E2064A /* Color.swift */, D850C2D32B69D85400338546 /* KeyedDefaults.swift */, D89224A12BD7365C00DA0932 /* TimeZone.swift */, D83C2F872B857B8C00CE60F7 /* URL.swift */, @@ -434,6 +437,7 @@ D892249E2BD735BE00DA0932 /* ThoughtsError.swift in Sources */, D8C283F22BD995CC00161C82 /* TagView.swift in Sources */, D850C2D42B69D85400338546 /* KeyedDefaults.swift in Sources */, + D87310CF2BD9B03300E2064A /* Color.swift in Sources */, D852AF662B604B1200B77A3D /* ComposeView.swift in Sources */, D8F06D512B6731CE0039AEF4 /* SettingsView.swift in Sources */, D80E08A62B6761370023DD4F /* Document.swift in Sources */, diff --git a/Thoughts/Extensions/Color.swift b/Thoughts/Extensions/Color.swift new file mode 100644 index 0000000..c979f25 --- /dev/null +++ b/Thoughts/Extensions/Color.swift @@ -0,0 +1,360 @@ +// Copyright (c) 2024 Jason Morley +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import SwiftUI + +import HashRainbow + +extension Color { + + struct HTML { + + public static let aliceBlue = Color(0xF0F8FF) + public static let antiqueWhite = Color(0xFAEBD7) + public static let aqua = Color(0x00FFFF) + public static let aquamarine = Color(0x7FFFD4) + public static let azure = Color(0xF0FFFF) + public static let beige = Color(0xF5F5DC) + public static let bisque = Color(0xFFE4C4) + public static let black = Color(0x000000) + public static let blanchedAlmond = Color(0xFFEBCD) + public static let blue = Color(0x0000FF) + public static let blueViolet = Color(0x8A2BE2) + public static let brown = Color(0xA52A2A) + public static let burlyWood = Color(0xDEB887) + public static let cadetBlue = Color(0x5F9EA0) + public static let chartreuse = Color(0x7FFF00) + public static let chocolate = Color(0xD2691E) + public static let coral = Color(0xFF7F50) + public static let cornflowerBlue = Color(0x6495ED) + public static let cornsilk = Color(0xFFF8DC) + public static let crimson = Color(0xDC143C) + public static let cyan = Color(0x00FFFF) + public static let darkBlue = Color(0x00008B) + public static let darkCyan = Color(0x008B8B) + public static let darkGoldenRod = Color(0xB8860B) + public static let darkGray = Color(0xA9A9A9) + public static let darkGrey = Color(0xA9A9A9) + public static let darkGreen = Color(0x006400) + public static let darkKhaki = Color(0xBDB76B) + public static let darkMagenta = Color(0x8B008B) + public static let darkOliveGreen = Color(0x556B2F) + public static let darkOrange = Color(0xFF8C00) + public static let darkOrchid = Color(0x9932CC) + public static let darkRed = Color(0x8B0000) + public static let darkSalmon = Color(0xE9967A) + public static let darkSeaGreen = Color(0x8FBC8F) + public static let darkSlateBlue = Color(0x483D8B) + public static let darkSlateGray = Color(0x2F4F4F) + public static let darkSlateGrey = Color(0x2F4F4F) + public static let darkTurquoise = Color(0x00CED1) + public static let darkViolet = Color(0x9400D3) + public static let deepPink = Color(0xFF1493) + public static let deepSkyBlue = Color(0x00BFFF) + public static let dimGray = Color(0x696969) + public static let dimGrey = Color(0x696969) + public static let dodgerBlue = Color(0x1E90FF) + public static let fireBrick = Color(0xB22222) + public static let floralWhite = Color(0xFFFAF0) + public static let forestGreen = Color(0x228B22) + public static let fuchsia = Color(0xFF00FF) + public static let gainsboro = Color(0xDCDCDC) + public static let ghostWhite = Color(0xF8F8FF) + public static let gold = Color(0xFFD700) + public static let goldenRod = Color(0xDAA520) + public static let gray = Color(0x808080) + public static let grey = Color(0x808080) + public static let green = Color(0x008000) + public static let greenYellow = Color(0xADFF2F) + public static let honeyDew = Color(0xF0FFF0) + public static let hotPink = Color(0xFF69B4) + public static let indianRed = Color(0xCD5C5C) + public static let indigo = Color(0x4B0082) + public static let ivory = Color(0xFFFFF0) + public static let khaki = Color(0xF0E68C) + public static let lavender = Color(0xE6E6FA) + public static let lavenderBlush = Color(0xFFF0F5) + public static let lawnGreen = Color(0x7CFC00) + public static let lemonChiffon = Color(0xFFFACD) + public static let lightBlue = Color(0xADD8E6) + public static let lightCoral = Color(0xF08080) + public static let lightCyan = Color(0xE0FFFF) + public static let lightGoldenRodYellow = Color(0xFAFAD2) + public static let lightGray = Color(0xD3D3D3) + public static let lightGrey = Color(0xD3D3D3) + public static let lightGreen = Color(0x90EE90) + public static let lightPink = Color(0xFFB6C1) + public static let lightSalmon = Color(0xFFA07A) + public static let lightSeaGreen = Color(0x20B2AA) + public static let lightSkyBlue = Color(0x87CEFA) + public static let lightSlateGray = Color(0x778899) + public static let lightSlateGrey = Color(0x778899) + public static let lightSteelBlue = Color(0xB0C4DE) + public static let lightYellow = Color(0xFFFFE0) + public static let lime = Color(0x00FF00) + public static let limeGreen = Color(0x32CD32) + public static let linen = Color(0xFAF0E6) + public static let magenta = Color(0xFF00FF) + public static let maroon = Color(0x800000) + public static let mediumAquaMarine = Color(0x66CDAA) + public static let mediumBlue = Color(0x0000CD) + public static let mediumOrchid = Color(0xBA55D3) + public static let mediumPurple = Color(0x9370DB) + public static let mediumSeaGreen = Color(0x3CB371) + public static let mediumSlateBlue = Color(0x7B68EE) + public static let mediumSpringGreen = Color(0x00FA9A) + public static let mediumTurquoise = Color(0x48D1CC) + public static let mediumVioletRed = Color(0xC71585) + public static let midnightBlue = Color(0x191970) + public static let mintCream = Color(0xF5FFFA) + public static let mistyRose = Color(0xFFE4E1) + public static let moccasin = Color(0xFFE4B5) + public static let navajoWhite = Color(0xFFDEAD) + public static let navy = Color(0x000080) + public static let oldLace = Color(0xFDF5E6) + public static let olive = Color(0x808000) + public static let oliveDrab = Color(0x6B8E23) + public static let orange = Color(0xFFA500) + public static let orangeRed = Color(0xFF4500) + public static let orchid = Color(0xDA70D6) + public static let paleGoldenRod = Color(0xEEE8AA) + public static let paleGreen = Color(0x98FB98) + public static let paleTurquoise = Color(0xAFEEEE) + public static let paleVioletRed = Color(0xDB7093) + public static let papayaWhip = Color(0xFFEFD5) + public static let peachPuff = Color(0xFFDAB9) + public static let peru = Color(0xCD853F) + public static let pink = Color(0xFFC0CB) + public static let plum = Color(0xDDA0DD) + public static let powderBlue = Color(0xB0E0E6) + public static let purple = Color(0x800080) + public static let rebeccaPurple = Color(0x663399) + public static let red = Color(0xFF0000) + public static let rosyBrown = Color(0xBC8F8F) + public static let royalBlue = Color(0x4169E1) + public static let saddleBrown = Color(0x8B4513) + public static let salmon = Color(0xFA8072) + public static let sandyBrown = Color(0xF4A460) + public static let seaGreen = Color(0x2E8B57) + public static let seaShell = Color(0xFFF5EE) + public static let sienna = Color(0xA0522D) + public static let silver = Color(0xC0C0C0) + public static let skyBlue = Color(0x87CEEB) + public static let slateBlue = Color(0x6A5ACD) + public static let slateGray = Color(0x708090) + public static let slateGrey = Color(0x708090) + public static let snow = Color(0xFFFAFA) + public static let springGreen = Color(0x00FF7F) + public static let steelBlue = Color(0x4682B4) + public static let tan = Color(0xD2B48C) + public static let teal = Color(0x008080) + public static let thistle = Color(0xD8BFD8) + public static let tomato = Color(0xFF6347) + public static let turquoise = Color(0x40E0D0) + public static let violet = Color(0xEE82EE) + public static let wheat = Color(0xF5DEB3) + public static let white = Color(0xFFFFFF) + public static let whiteSmoke = Color(0xF5F5F5) + public static let yellow = Color(0xFFFF00) + public static let yellowGreen = Color(0x9ACD32) + + } + + init(_ value: Int32) { + let red = Double(((0xff << 16) & value) >> 16) + let green = Double(((0xff << 8) & value) >> 8) + let blue = Double(0xff & value) + self.init(red: red / 255, green: green / 255, blue: blue / 255) + } + + var gray: CGFloat? { + return cgColor?.converted(to: CGColorSpaceCreateDeviceGray(), + intent: CGColorRenderingIntent.defaultIntent, + options: nil)?.components?.first + } + +} + +extension Array where Element == Color { + + static let html: [Color] = [ + .HTML.aliceBlue, + .HTML.antiqueWhite, + .HTML.aqua, + .HTML.aquamarine, + .HTML.azure, + .HTML.beige, + .HTML.bisque, + .HTML.black, + .HTML.blanchedAlmond, + .HTML.blue, + .HTML.blueViolet, + .HTML.brown, + .HTML.burlyWood, + .HTML.cadetBlue, + .HTML.chartreuse, + .HTML.chocolate, + .HTML.coral, + .HTML.cornflowerBlue, + .HTML.cornsilk, + .HTML.crimson, + .HTML.cyan, + .HTML.darkBlue, + .HTML.darkCyan, + .HTML.darkGoldenRod, + .HTML.darkGray, + .HTML.darkGrey, + .HTML.darkGreen, + .HTML.darkKhaki, + .HTML.darkMagenta, + .HTML.darkOliveGreen, + .HTML.darkOrange, + .HTML.darkOrchid, + .HTML.darkRed, + .HTML.darkSalmon, + .HTML.darkSeaGreen, + .HTML.darkSlateBlue, + .HTML.darkSlateGray, + .HTML.darkSlateGrey, + .HTML.darkTurquoise, + .HTML.darkViolet, + .HTML.deepPink, + .HTML.deepSkyBlue, + .HTML.dimGray, + .HTML.dimGrey, + .HTML.dodgerBlue, + .HTML.fireBrick, + .HTML.floralWhite, + .HTML.forestGreen, + .HTML.fuchsia, + .HTML.gainsboro, + .HTML.ghostWhite, + .HTML.gold, + .HTML.goldenRod, + .HTML.gray, + .HTML.grey, + .HTML.green, + .HTML.greenYellow, + .HTML.honeyDew, + .HTML.hotPink, + .HTML.indianRed, + .HTML.indigo, + .HTML.ivory, + .HTML.khaki, + .HTML.lavender, + .HTML.lavenderBlush, + .HTML.lawnGreen, + .HTML.lemonChiffon, + .HTML.lightBlue, + .HTML.lightCoral, + .HTML.lightCyan, + .HTML.lightGoldenRodYellow, + .HTML.lightGray, + .HTML.lightGrey, + .HTML.lightGreen, + .HTML.lightPink, + .HTML.lightSalmon, + .HTML.lightSeaGreen, + .HTML.lightSkyBlue, + .HTML.lightSlateGray, + .HTML.lightSlateGrey, + .HTML.lightSteelBlue, + .HTML.lightYellow, + .HTML.lime, + .HTML.limeGreen, + .HTML.linen, + .HTML.magenta, + .HTML.maroon, + .HTML.mediumAquaMarine, + .HTML.mediumBlue, + .HTML.mediumOrchid, + .HTML.mediumPurple, + .HTML.mediumSeaGreen, + .HTML.mediumSlateBlue, + .HTML.mediumSpringGreen, + .HTML.mediumTurquoise, + .HTML.mediumVioletRed, + .HTML.midnightBlue, + .HTML.mintCream, + .HTML.mistyRose, + .HTML.moccasin, + .HTML.navajoWhite, + .HTML.navy, + .HTML.oldLace, + .HTML.olive, + .HTML.oliveDrab, + .HTML.orange, + .HTML.orangeRed, + .HTML.orchid, + .HTML.paleGoldenRod, + .HTML.paleGreen, + .HTML.paleTurquoise, + .HTML.paleVioletRed, + .HTML.papayaWhip, + .HTML.peachPuff, + .HTML.peru, + .HTML.pink, + .HTML.plum, + .HTML.powderBlue, + .HTML.purple, + .HTML.rebeccaPurple, + .HTML.red, + .HTML.rosyBrown, + .HTML.royalBlue, + .HTML.saddleBrown, + .HTML.salmon, + .HTML.sandyBrown, + .HTML.seaGreen, + .HTML.seaShell, + .HTML.sienna, + .HTML.silver, + .HTML.skyBlue, + .HTML.slateBlue, + .HTML.slateGray, + .HTML.slateGrey, + .HTML.snow, + .HTML.springGreen, + .HTML.steelBlue, + .HTML.tan, + .HTML.teal, + .HTML.thistle, + .HTML.tomato, + .HTML.turquoise, + .HTML.violet, + .HTML.wheat, + .HTML.white, + .HTML.whiteSmoke, + .HTML.yellow, + .HTML.yellowGreen, + ] + + static let tags: [Color] = { + return html.filter { color in + guard let gray = color.gray, + gray < 0.9, + gray > 0.1 + else { + return false + } + return true + } + }() + +} diff --git a/Thoughts/Views/TagView.swift b/Thoughts/Views/TagView.swift index 6289db0..e2118f2 100644 --- a/Thoughts/Views/TagView.swift +++ b/Thoughts/Views/TagView.swift @@ -22,344 +22,6 @@ import SwiftUI import HashRainbow -extension Color { - - struct HTML { - - public static let aliceBlue = Color(0xF0F8FF) - public static let antiqueWhite = Color(0xFAEBD7) - public static let aqua = Color(0x00FFFF) - public static let aquamarine = Color(0x7FFFD4) - public static let azure = Color(0xF0FFFF) - public static let beige = Color(0xF5F5DC) - public static let bisque = Color(0xFFE4C4) - public static let black = Color(0x000000) - public static let blanchedAlmond = Color(0xFFEBCD) - public static let blue = Color(0x0000FF) - public static let blueViolet = Color(0x8A2BE2) - public static let brown = Color(0xA52A2A) - public static let burlyWood = Color(0xDEB887) - public static let cadetBlue = Color(0x5F9EA0) - public static let chartreuse = Color(0x7FFF00) - public static let chocolate = Color(0xD2691E) - public static let coral = Color(0xFF7F50) - public static let cornflowerBlue = Color(0x6495ED) - public static let cornsilk = Color(0xFFF8DC) - public static let crimson = Color(0xDC143C) - public static let cyan = Color(0x00FFFF) - public static let darkBlue = Color(0x00008B) - public static let darkCyan = Color(0x008B8B) - public static let darkGoldenRod = Color(0xB8860B) - public static let darkGray = Color(0xA9A9A9) - public static let darkGrey = Color(0xA9A9A9) - public static let darkGreen = Color(0x006400) - public static let darkKhaki = Color(0xBDB76B) - public static let darkMagenta = Color(0x8B008B) - public static let darkOliveGreen = Color(0x556B2F) - public static let darkOrange = Color(0xFF8C00) - public static let darkOrchid = Color(0x9932CC) - public static let darkRed = Color(0x8B0000) - public static let darkSalmon = Color(0xE9967A) - public static let darkSeaGreen = Color(0x8FBC8F) - public static let darkSlateBlue = Color(0x483D8B) - public static let darkSlateGray = Color(0x2F4F4F) - public static let darkSlateGrey = Color(0x2F4F4F) - public static let darkTurquoise = Color(0x00CED1) - public static let darkViolet = Color(0x9400D3) - public static let deepPink = Color(0xFF1493) - public static let deepSkyBlue = Color(0x00BFFF) - public static let dimGray = Color(0x696969) - public static let dimGrey = Color(0x696969) - public static let dodgerBlue = Color(0x1E90FF) - public static let fireBrick = Color(0xB22222) - public static let floralWhite = Color(0xFFFAF0) - public static let forestGreen = Color(0x228B22) - public static let fuchsia = Color(0xFF00FF) - public static let gainsboro = Color(0xDCDCDC) - public static let ghostWhite = Color(0xF8F8FF) - public static let gold = Color(0xFFD700) - public static let goldenRod = Color(0xDAA520) - public static let gray = Color(0x808080) - public static let grey = Color(0x808080) - public static let green = Color(0x008000) - public static let greenYellow = Color(0xADFF2F) - public static let honeyDew = Color(0xF0FFF0) - public static let hotPink = Color(0xFF69B4) - public static let indianRed = Color(0xCD5C5C) - public static let indigo = Color(0x4B0082) - public static let ivory = Color(0xFFFFF0) - public static let khaki = Color(0xF0E68C) - public static let lavender = Color(0xE6E6FA) - public static let lavenderBlush = Color(0xFFF0F5) - public static let lawnGreen = Color(0x7CFC00) - public static let lemonChiffon = Color(0xFFFACD) - public static let lightBlue = Color(0xADD8E6) - public static let lightCoral = Color(0xF08080) - public static let lightCyan = Color(0xE0FFFF) - public static let lightGoldenRodYellow = Color(0xFAFAD2) - public static let lightGray = Color(0xD3D3D3) - public static let lightGrey = Color(0xD3D3D3) - public static let lightGreen = Color(0x90EE90) - public static let lightPink = Color(0xFFB6C1) - public static let lightSalmon = Color(0xFFA07A) - public static let lightSeaGreen = Color(0x20B2AA) - public static let lightSkyBlue = Color(0x87CEFA) - public static let lightSlateGray = Color(0x778899) - public static let lightSlateGrey = Color(0x778899) - public static let lightSteelBlue = Color(0xB0C4DE) - public static let lightYellow = Color(0xFFFFE0) - public static let lime = Color(0x00FF00) - public static let limeGreen = Color(0x32CD32) - public static let linen = Color(0xFAF0E6) - public static let magenta = Color(0xFF00FF) - public static let maroon = Color(0x800000) - public static let mediumAquaMarine = Color(0x66CDAA) - public static let mediumBlue = Color(0x0000CD) - public static let mediumOrchid = Color(0xBA55D3) - public static let mediumPurple = Color(0x9370DB) - public static let mediumSeaGreen = Color(0x3CB371) - public static let mediumSlateBlue = Color(0x7B68EE) - public static let mediumSpringGreen = Color(0x00FA9A) - public static let mediumTurquoise = Color(0x48D1CC) - public static let mediumVioletRed = Color(0xC71585) - public static let midnightBlue = Color(0x191970) - public static let mintCream = Color(0xF5FFFA) - public static let mistyRose = Color(0xFFE4E1) - public static let moccasin = Color(0xFFE4B5) - public static let navajoWhite = Color(0xFFDEAD) - public static let navy = Color(0x000080) - public static let oldLace = Color(0xFDF5E6) - public static let olive = Color(0x808000) - public static let oliveDrab = Color(0x6B8E23) - public static let orange = Color(0xFFA500) - public static let orangeRed = Color(0xFF4500) - public static let orchid = Color(0xDA70D6) - public static let paleGoldenRod = Color(0xEEE8AA) - public static let paleGreen = Color(0x98FB98) - public static let paleTurquoise = Color(0xAFEEEE) - public static let paleVioletRed = Color(0xDB7093) - public static let papayaWhip = Color(0xFFEFD5) - public static let peachPuff = Color(0xFFDAB9) - public static let peru = Color(0xCD853F) - public static let pink = Color(0xFFC0CB) - public static let plum = Color(0xDDA0DD) - public static let powderBlue = Color(0xB0E0E6) - public static let purple = Color(0x800080) - public static let rebeccaPurple = Color(0x663399) - public static let red = Color(0xFF0000) - public static let rosyBrown = Color(0xBC8F8F) - public static let royalBlue = Color(0x4169E1) - public static let saddleBrown = Color(0x8B4513) - public static let salmon = Color(0xFA8072) - public static let sandyBrown = Color(0xF4A460) - public static let seaGreen = Color(0x2E8B57) - public static let seaShell = Color(0xFFF5EE) - public static let sienna = Color(0xA0522D) - public static let silver = Color(0xC0C0C0) - public static let skyBlue = Color(0x87CEEB) - public static let slateBlue = Color(0x6A5ACD) - public static let slateGray = Color(0x708090) - public static let slateGrey = Color(0x708090) - public static let snow = Color(0xFFFAFA) - public static let springGreen = Color(0x00FF7F) - public static let steelBlue = Color(0x4682B4) - public static let tan = Color(0xD2B48C) - public static let teal = Color(0x008080) - public static let thistle = Color(0xD8BFD8) - public static let tomato = Color(0xFF6347) - public static let turquoise = Color(0x40E0D0) - public static let violet = Color(0xEE82EE) - public static let wheat = Color(0xF5DEB3) - public static let white = Color(0xFFFFFF) - public static let whiteSmoke = Color(0xF5F5F5) - public static let yellow = Color(0xFFFF00) - public static let yellowGreen = Color(0x9ACD32) - - } - - init(_ value: Int32) { - let red = Double(((0xff << 16) & value) >> 16) - let green = Double(((0xff << 8) & value) >> 8) - let blue = Double(0xff & value) - self.init(red: red / 255, green: green / 255, blue: blue / 255) - } - - var gray: CGFloat? { - return cgColor?.converted(to: CGColorSpaceCreateDeviceGray(), - intent: CGColorRenderingIntent.defaultIntent, - options: nil)?.components?.first - } - -} - -extension Array where Element == Color { - - static let html: [Color] = [ - .HTML.aliceBlue, - .HTML.antiqueWhite, - .HTML.aqua, - .HTML.aquamarine, - .HTML.azure, - .HTML.beige, - .HTML.bisque, - .HTML.black, - .HTML.blanchedAlmond, - .HTML.blue, - .HTML.blueViolet, - .HTML.brown, - .HTML.burlyWood, - .HTML.cadetBlue, - .HTML.chartreuse, - .HTML.chocolate, - .HTML.coral, - .HTML.cornflowerBlue, - .HTML.cornsilk, - .HTML.crimson, - .HTML.cyan, - .HTML.darkBlue, - .HTML.darkCyan, - .HTML.darkGoldenRod, - .HTML.darkGray, - .HTML.darkGrey, - .HTML.darkGreen, - .HTML.darkKhaki, - .HTML.darkMagenta, - .HTML.darkOliveGreen, - .HTML.darkOrange, - .HTML.darkOrchid, - .HTML.darkRed, - .HTML.darkSalmon, - .HTML.darkSeaGreen, - .HTML.darkSlateBlue, - .HTML.darkSlateGray, - .HTML.darkSlateGrey, - .HTML.darkTurquoise, - .HTML.darkViolet, - .HTML.deepPink, - .HTML.deepSkyBlue, - .HTML.dimGray, - .HTML.dimGrey, - .HTML.dodgerBlue, - .HTML.fireBrick, - .HTML.floralWhite, - .HTML.forestGreen, - .HTML.fuchsia, - .HTML.gainsboro, - .HTML.ghostWhite, - .HTML.gold, - .HTML.goldenRod, - .HTML.gray, - .HTML.grey, - .HTML.green, - .HTML.greenYellow, - .HTML.honeyDew, - .HTML.hotPink, - .HTML.indianRed, - .HTML.indigo, - .HTML.ivory, - .HTML.khaki, - .HTML.lavender, - .HTML.lavenderBlush, - .HTML.lawnGreen, - .HTML.lemonChiffon, - .HTML.lightBlue, - .HTML.lightCoral, - .HTML.lightCyan, - .HTML.lightGoldenRodYellow, - .HTML.lightGray, - .HTML.lightGrey, - .HTML.lightGreen, - .HTML.lightPink, - .HTML.lightSalmon, - .HTML.lightSeaGreen, - .HTML.lightSkyBlue, - .HTML.lightSlateGray, - .HTML.lightSlateGrey, - .HTML.lightSteelBlue, - .HTML.lightYellow, - .HTML.lime, - .HTML.limeGreen, - .HTML.linen, - .HTML.magenta, - .HTML.maroon, - .HTML.mediumAquaMarine, - .HTML.mediumBlue, - .HTML.mediumOrchid, - .HTML.mediumPurple, - .HTML.mediumSeaGreen, - .HTML.mediumSlateBlue, - .HTML.mediumSpringGreen, - .HTML.mediumTurquoise, - .HTML.mediumVioletRed, - .HTML.midnightBlue, - .HTML.mintCream, - .HTML.mistyRose, - .HTML.moccasin, - .HTML.navajoWhite, - .HTML.navy, - .HTML.oldLace, - .HTML.olive, - .HTML.oliveDrab, - .HTML.orange, - .HTML.orangeRed, - .HTML.orchid, - .HTML.paleGoldenRod, - .HTML.paleGreen, - .HTML.paleTurquoise, - .HTML.paleVioletRed, - .HTML.papayaWhip, - .HTML.peachPuff, - .HTML.peru, - .HTML.pink, - .HTML.plum, - .HTML.powderBlue, - .HTML.purple, - .HTML.rebeccaPurple, - .HTML.red, - .HTML.rosyBrown, - .HTML.royalBlue, - .HTML.saddleBrown, - .HTML.salmon, - .HTML.sandyBrown, - .HTML.seaGreen, - .HTML.seaShell, - .HTML.sienna, - .HTML.silver, - .HTML.skyBlue, - .HTML.slateBlue, - .HTML.slateGray, - .HTML.slateGrey, - .HTML.snow, - .HTML.springGreen, - .HTML.steelBlue, - .HTML.tan, - .HTML.teal, - .HTML.thistle, - .HTML.tomato, - .HTML.turquoise, - .HTML.violet, - .HTML.wheat, - .HTML.white, - .HTML.whiteSmoke, - .HTML.yellow, - .HTML.yellowGreen, - ] - - static let tags: [Color] = { - return html.filter { color in - guard let gray = color.gray, - gray < 0.9, - gray > 0.1 - else { - return false - } - return true - } - }() - -} - - public struct TagView: View { struct LayoutMetrics {