-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
288 additions
and
85 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,70 +1,79 @@ | ||
import SwiftUI | ||
|
||
struct MenuBarSettings: View { | ||
@State var speed: Float = Settings.snowflakeSpeedRange.upperBound | ||
@State var size: Float = Settings.snowflakeSizeRange.upperBound | ||
@State var speed: ClosedRange<Float> = Settings.snowflakeSpeedRange | ||
@State var size: ClosedRange<Float> = Settings.snowflakeSizeRange | ||
@State var maxSnowflakes: Float = Float(Settings.maxSnowflakes) | ||
@State var windowInteraction: Bool = Settings.windowInteraction | ||
@State var windStrength: Float = Settings.windStrength * 1000 | ||
|
||
var body: some View { | ||
VStack(spacing: 2) { | ||
VStack(alignment: .leading, spacing: 2) { | ||
Group { | ||
Row(title: "Snowflake speed: \(speed)") | ||
.foregroundColor(.secondary) | ||
Slider(value: $speed, in: 1...20.0) | ||
.padding(.horizontal) | ||
.padding(.vertical, 4) | ||
} | ||
VStack(alignment: .leading, spacing: 12) { | ||
VStack(alignment: .leading) { | ||
Text("Snowflake speed") | ||
RangedSliderView(value: $speed, in: 1...20) | ||
Divider() | ||
} | ||
VStack(alignment: .leading, spacing: 2) { | ||
Group { | ||
Row(title: "Snowflake size: \(size)") | ||
.foregroundColor(.secondary) | ||
Slider(value: $size, in: 4...40.0) | ||
.padding(.horizontal) | ||
.padding(.vertical, 4) | ||
} | ||
|
||
VStack(alignment: .leading) { | ||
Text("Snowflake size") | ||
RangedSliderView(value: $size, in: 3...40) | ||
Divider() | ||
} | ||
|
||
VStack(alignment: .leading) { | ||
Text("Snowflake count \(Int(maxSnowflakes))") | ||
Slider(value: $maxSnowflakes, in: 1...4000) | ||
Divider() | ||
} | ||
|
||
VStack(alignment: .leading, spacing: 2) { | ||
Group { | ||
Row(title: "Snowflake count: \(maxSnowflakes)") | ||
.foregroundColor(.secondary) | ||
Slider(value: $maxSnowflakes, in: 1...4000.0) | ||
.padding(.horizontal) | ||
.padding(.vertical, 4) | ||
VStack(alignment: .leading) { | ||
Text("Wind strength \(Int(windStrength / 100))") | ||
Slider(value: $windStrength, in: 1...4000) | ||
Divider() | ||
} | ||
|
||
Toggle("Snow interact with window", isOn: $windowInteraction) | ||
|
||
HStack { | ||
Button("Close app") { | ||
NSApplication.shared.terminate(nil) | ||
} | ||
.buttonStyle(.borderless) | ||
|
||
Spacer() | ||
|
||
Button("Reset", role: .destructive) { | ||
Settings.reset() | ||
speed = Settings.snowflakeSpeedRange | ||
size = Settings.snowflakeSizeRange | ||
maxSnowflakes = Float(Settings.maxSnowflakes) | ||
windowInteraction = Settings.windowInteraction | ||
windStrength = Settings.windStrength * 1000 | ||
} | ||
} | ||
} | ||
.frame(maxWidth: 250) | ||
.padding(.vertical, 4) | ||
.onChange(of: speed) { oldValue, newValue in | ||
Settings.snowflakeSpeedRange = Settings.snowflakeSpeedRange.lowerBound...newValue | ||
.padding() | ||
.onChange(of: speed) { _, newValue in | ||
Settings.snowflakeSpeedRange = newValue | ||
} | ||
.onChange(of: size) { oldValue, newValue in | ||
Settings.snowflakeSizeRange = Settings.snowflakeSizeRange.lowerBound...newValue | ||
.onChange(of: size) { _, newValue in | ||
Settings.snowflakeSizeRange = newValue | ||
} | ||
.onChange(of: maxSnowflakes) { oldValue, newValue in | ||
.onChange(of: maxSnowflakes) { _, newValue in | ||
Settings.maxSnowflakes = Int(newValue) | ||
} | ||
.onChange(of: windowInteraction) { _, newValue in | ||
Settings.windowInteraction = newValue | ||
} | ||
.onChange(of: windStrength) { _, newValue in | ||
Settings.windStrength = newValue / 1000 | ||
} | ||
} | ||
} | ||
|
||
struct Row: View { | ||
let title : String | ||
var hasCheckmark = false | ||
|
||
var body: some View { | ||
HStack(spacing: 4) { | ||
Group { | ||
if hasCheckmark { | ||
Image(systemName: "checkmark") | ||
} else { | ||
Text("") | ||
} | ||
} | ||
.frame(width: 12) | ||
Text(title) | ||
} | ||
} | ||
#Preview { | ||
MenuBarSettings() | ||
.scaledToFit() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import SwiftUI | ||
|
||
struct RangedSliderView: View { | ||
let currentValue: Binding<ClosedRange<Float>> | ||
let sliderBounds: ClosedRange<Int> | ||
|
||
public init(value: Binding<ClosedRange<Float>>, in bounds: ClosedRange<Int>) { | ||
self.currentValue = value | ||
self.sliderBounds = bounds | ||
} | ||
|
||
var body: some View { | ||
GeometryReader { geomentry in | ||
sliderView(sliderSize: geomentry.size) | ||
|
||
} | ||
.padding(.init(top: 16, leading: 10, bottom: 6, trailing: 10)) | ||
} | ||
|
||
|
||
@ViewBuilder private func sliderView(sliderSize: CGSize) -> some View { | ||
let sliderViewYCenter = sliderSize.height / 2 | ||
ZStack { | ||
RoundedRectangle(cornerRadius: 2) | ||
.fill(Color.secondary.opacity(0.2)) | ||
.frame(height: 4) | ||
.padding(.horizontal, -10) | ||
|
||
ZStack { | ||
let sliderBoundDifference = sliderBounds.count | ||
let stepWidthInPixel = CGFloat(sliderSize.width) / CGFloat(sliderBoundDifference) | ||
|
||
// Calculate Left Thumb initial position | ||
let leftThumbLocation: CGFloat = currentValue.wrappedValue.lowerBound == Float(sliderBounds.lowerBound) | ||
? 0 | ||
: CGFloat(currentValue.wrappedValue.lowerBound - Float(sliderBounds.lowerBound)) * stepWidthInPixel | ||
|
||
// Calculate right thumb initial position | ||
let rightThumbLocation = CGFloat(currentValue.wrappedValue.upperBound) * stepWidthInPixel | ||
|
||
// Path between both handles | ||
lineBetweenThumbs(from: .init(x: leftThumbLocation, y: sliderViewYCenter), to: .init(x: rightThumbLocation, y: sliderViewYCenter)) | ||
|
||
// Left Thumb Handle | ||
let leftThumbPoint = CGPoint(x: leftThumbLocation, y: sliderViewYCenter) | ||
thumbView(position: leftThumbPoint, value: Float(currentValue.wrappedValue.lowerBound)) | ||
.highPriorityGesture(DragGesture().onChanged { dragValue in | ||
|
||
let dragLocation = dragValue.location | ||
let xThumbOffset = min(max(0, dragLocation.x), sliderSize.width) | ||
|
||
let newValue = Float(sliderBounds.lowerBound) + Float(xThumbOffset / stepWidthInPixel) | ||
|
||
// Stop the range thumbs from colliding each other | ||
if newValue < currentValue.wrappedValue.upperBound { | ||
currentValue.wrappedValue = newValue...currentValue.wrappedValue.upperBound | ||
} | ||
}) | ||
|
||
// Right Thumb Handle | ||
thumbView(position: CGPoint(x: rightThumbLocation, y: sliderViewYCenter), value: currentValue.wrappedValue.upperBound) | ||
.highPriorityGesture(DragGesture().onChanged { dragValue in | ||
let dragLocation = dragValue.location | ||
let xThumbOffset = min(max(CGFloat(leftThumbLocation), dragLocation.x), sliderSize.width) | ||
|
||
var newValue = Float(xThumbOffset / stepWidthInPixel) // convert back the value bound | ||
newValue = min(newValue, Float(sliderBounds.upperBound)) | ||
|
||
// Stop the range thumbs from colliding each other | ||
if newValue > currentValue.wrappedValue.lowerBound { | ||
currentValue.wrappedValue = currentValue.wrappedValue.lowerBound...newValue | ||
} | ||
}) | ||
} | ||
} | ||
} | ||
|
||
@ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View { | ||
Path { path in | ||
path.move(to: from) | ||
path.addLine(to: to) | ||
}.stroke(Color.accentColor, lineWidth: 4) | ||
} | ||
|
||
@ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View { | ||
ZStack { | ||
Text(String(Int(value))) | ||
.font(.system(size: 10, weight: .semibold)) | ||
.offset(y: -20) | ||
Circle() | ||
.frame(width: 20, height: 20) | ||
.foregroundColor(Color.gray) | ||
.shadow(color: Color.black.opacity(0.16), radius: 8, x: 0, y: 2) | ||
.contentShape(Rectangle()) | ||
} | ||
.position(x: position.x, y: position.y) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,47 @@ | ||
import Cocoa | ||
import SwiftUI | ||
|
||
struct Settings { | ||
static var snowflakeSizeRange: ClosedRange<Float> = 4...10 | ||
@AppStorage("snowflakeSizeRange") | ||
static var snowflakeSizeRange: ClosedRange<Float> = 3...13 | ||
@AppStorage("maxSnowflakes") | ||
static var maxSnowflakes = 1000 | ||
@AppStorage("snowflakeSpeedRange") | ||
static var snowflakeSpeedRange: ClosedRange<Float> = 1...3 | ||
static var windStrength: Float = 0.5 | ||
@AppStorage("windStrength") | ||
static var windStrength: Float = 2.5 | ||
@AppStorage("semltingSpeed") | ||
static var semltingSpeed: Float = 0.05 | ||
@AppStorage("windowInteraction") | ||
static var windowInteraction: Bool = true | ||
|
||
static func reset() { | ||
UserDefaults.standard.dictionaryRepresentation().keys.forEach({ UserDefaults.standard.removeObject(forKey: $0) }) | ||
} | ||
} | ||
|
||
// MARK: - Extensions | ||
|
||
extension Float: @retroactive RawRepresentable { | ||
public var rawValue: String { | ||
String(self) | ||
} | ||
|
||
public typealias RawValue = String | ||
|
||
public init?(rawValue: String) { | ||
self = Float(rawValue)! | ||
} | ||
} | ||
|
||
extension ClosedRange: @retroactive RawRepresentable where Bound == Float { | ||
public init?(rawValue: String) { | ||
let split = rawValue.split(separator: "...") | ||
|
||
self.init(uncheckedBounds: (lower: Float(split[0])!, upper: Float(split[1])!)) | ||
} | ||
|
||
public var rawValue: String { | ||
return String(lowerBound) + "..." + String(upperBound) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.