From 5c43035c1b66111e9d0556acd09a465705a6376d Mon Sep 17 00:00:00 2001 From: Joshua Nozzi Date: Sat, 14 Sep 2024 13:35:06 -0400 Subject: [PATCH] v1.0 Initial public offering --- .gitignore | 8 + LICENSE | 21 +++ Package.swift | 25 +++ README.md | 71 ++++++++ Sources/LeaderView/Leader.swift | 280 ++++++++++++++++++++++++++++++++ 5 files changed, 405 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/LeaderView/Leader.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc659fc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Joshua Nozzi + +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/Package.swift b/Package.swift new file mode 100644 index 0000000..8f783d4 --- /dev/null +++ b/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "LeaderView", + platforms: [ + .macOS(.v14), + .iOS(.v17) + ], + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "LeaderView", + targets: ["LeaderView"]) + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "LeaderView") + + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f21d43 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# … LeaderView + +![Platform](https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20tvOS%20%7C%20watchOS-blue) + +`LeaderView` is a configurable typographical element that renders a baseline "leader line" from one view to the next, as found in a book index: + +``` +Chapter One: The Dottening ……………………………… 1 +Chapter Two: The Dots Return ………………………… 12 +Chapter Three: These Things Again?! ……… 18 +... +Chapter Twenty-Six: Where +the Hell Are They All +Coming From?! ………………………………… 206 +``` + +## Features + +You can configure: + +* Inter-view spacing +* Inter-dot spacing +* Dot diameter +* Dot color +* Layout Priority (leading / trailing views) +* Vertical alignment (default/best is last-baseline) + + +### Example + +To create a simple leader: + +```swift +import LeaderView + +Leader { + Text("Chapter One: The Dottening") + .font(.title2) +} trailingView: { + Text("1") + .font(.title3) + .monospaced() + } +``` + +To get all fancy with it: + +```swift +import LeaderView + +Leader( + dotDiameter: dotDiameter, + viewSpacing: viewSpacing, + dotSpacing: dotSpacing, + minLeaderWidth: minLeaderWidth +) { + Text("Introduction") + .font(.title2) +} trailingView: { + Text("ii") + .font(.title3) + .monospaced() + .italic() +} +``` + +### Contributing +Contributions are welcome! If you'd like to contribute to LeaderView, please fork the repository and submit a pull request. + +### License +This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/Sources/LeaderView/Leader.swift b/Sources/LeaderView/Leader.swift new file mode 100644 index 0000000..cae9541 --- /dev/null +++ b/Sources/LeaderView/Leader.swift @@ -0,0 +1,280 @@ +// +// Leader.swift +// LeaderView +// +// Created by Joshua Nozzi on 8/11/24. + +import SwiftUI + +private let LeaderPriority: Double = -900 +private let DefaultLeadingViewPriority: Double = 800 +private let DefaultTrailingViewPriority: Double = 1000 +private let DefaultDotDiameter: Double = 3 +private let DefaultDotSpacing: Double = 8 +private let DefaultViewSpacing: Double = 10 +private let DefaultLeaderColor: Color = .secondary +private let DefaultMinLeaderWidth: Double = 10 + +/// A typographical leader (dotted line at last baseline) to lead the eye from the leading view to the trailing view (usually but not limited to text). +struct Leader: View { + + /// The leading view (such as a chapter title) + @ViewBuilder let leadingView: () -> LeadingView + + /// The trailing view (such as the chapter's page number) + @ViewBuilder let trailingView: () -> TrailingView + + /// The vertical alignment of the leading/trailing/leader views. + /// Default is .lastTextBaseline + var alignment: VerticalAlignment + + /// The layout priority of the leading view. + /// Default is 800 + var leadingPriority: Double + + /// The layout priority of the trailing view. + /// Default is 1000 + var trailingPriority: Double + + /// The diameter of the leader dots. + /// Default is 3 + var dotDiameter: Double + + /// The color of the leader dots. + /// Default is Color.secondary + var leaderColor: Color + + /// The spacing between leading view, trailing view, and leader. + /// Default is 10 + var viewSpacing: Double + + /// The spacing between the leader dots. + /// Default is 8 + var dotSpacing: Double + + // The minimum width allowed for the leader dots. + // Default is 10 + var minLeaderWidth: Double + + init( + alignment: VerticalAlignment = .lastTextBaseline, + leadingPriority: Double = DefaultLeadingViewPriority, + trailingPriority: Double = DefaultTrailingViewPriority, + dotDiameter: Double = DefaultDotDiameter, + leaderColor: Color = DefaultLeaderColor, + viewSpacing: Double = DefaultViewSpacing, + dotSpacing: Double = DefaultDotSpacing, + minLeaderWidth: Double = DefaultMinLeaderWidth, + leadingView: @escaping () -> LeadingView, + trailingView: @escaping () -> TrailingView + ) { + self.leadingView = leadingView + self.trailingView = trailingView + self.alignment = alignment + self.leadingPriority = leadingPriority + self.trailingPriority = trailingPriority + self.dotDiameter = dotDiameter + self.leaderColor = leaderColor + self.viewSpacing = viewSpacing + self.dotSpacing = dotSpacing + self.minLeaderWidth = minLeaderWidth + } + + var body: some View { + + // It's all contained in an HStack whose alignment and spacing is configurable + HStack( + alignment: alignment, + spacing: viewSpacing + ) { + + // We'll render the leading view first with the given priority + leadingView() + .layoutPriority(leadingPriority) + + // Now the leader dots... + // (we need to know the available space) + GeometryReader { g in + + // Some basic metrics + let dotSpaceWidth = dotDiameter + dotSpacing + let availableDotWidth: Double = g.size.width / dotSpaceWidth + + // Don't bother if there's no room + if availableDotWidth >= 2 { + + // We skip the first dot for appearances + ForEach(1..