From fad9172a790298b60e05b0e0c835e88d1c2b20cb Mon Sep 17 00:00:00 2001 From: KrLite Date: Fri, 25 Oct 2024 23:39:44 +0800 Subject: [PATCH 001/314] very fundamental stuffs --- .../Components/LuminareButtonStyle.swift | 8 + .../Components/LuminareInfoView.swift | 38 +++-- .../Components/LuminareLabeledContent.swift | 156 ++++++++++++++++++ .../Luminare/Components/LuminareToggle.swift | 3 - 4 files changed, 191 insertions(+), 14 deletions(-) create mode 100644 Sources/Luminare/Components/LuminareLabeledContent.swift diff --git a/Sources/Luminare/Components/LuminareButtonStyle.swift b/Sources/Luminare/Components/LuminareButtonStyle.swift index 95d2c90..e3788ba 100644 --- a/Sources/Luminare/Components/LuminareButtonStyle.swift +++ b/Sources/Luminare/Components/LuminareButtonStyle.swift @@ -9,8 +9,10 @@ import SwiftUI public struct LuminareButtonStyle: ButtonStyle { @Environment(\.isEnabled) private var isEnabled: Bool + let innerCornerRadius: CGFloat = 2 let elementMinHeight: CGFloat = 34 + @State var isHovering: Bool = false public init() {} @@ -45,8 +47,10 @@ public struct LuminareButtonStyle: ButtonStyle { public struct LuminareDestructiveButtonStyle: ButtonStyle { @Environment(\.isEnabled) private var isEnabled: Bool + let innerCornerRadius: CGFloat = 2 let elementMinHeight: CGFloat = 34 + @State var isHovering: Bool = false public init() {} @@ -81,8 +85,10 @@ public struct LuminareDestructiveButtonStyle: ButtonStyle { public struct LuminareCosmeticButtonStyle: ButtonStyle { @Environment(\.isEnabled) private var isEnabled: Bool + let innerCornerRadius: CGFloat = 2 let elementMinHeight: CGFloat = 34 + @State var isHovering: Bool = false let icon: Image @@ -126,9 +132,11 @@ public struct LuminareCosmeticButtonStyle: ButtonStyle { public struct LuminareCompactButtonStyle: ButtonStyle { @Environment(\.isEnabled) private var isEnabled: Bool + let elementMinHeight: CGFloat = 34 let elementExtraMinHeight: CGFloat = 25 let extraCompact: Bool + @State var isHovering: Bool = false let cornerRadius: CGFloat = 8 diff --git a/Sources/Luminare/Components/LuminareInfoView.swift b/Sources/Luminare/Components/LuminareInfoView.swift index 99fd21b..ed24331 100644 --- a/Sources/Luminare/Components/LuminareInfoView.swift +++ b/Sources/Luminare/Components/LuminareInfoView.swift @@ -7,17 +7,33 @@ import SwiftUI -public struct LuminareInfoView: View { +public struct LuminareInfoView: View where Content: View { let color: Color - let description: LocalizedStringKey - @State var isShowingDescription: Bool = false - @State var isHovering: Bool = false - - @State var hoverTimer: Timer? - - public init(_ description: LocalizedStringKey, _ color: Color = .blue) { + let withoutPadding: Bool + @ViewBuilder private let content: () -> Content + + @State private var isShowingDescription: Bool = false + @State private var isHovering: Bool = false + @State private var hoverTimer: Timer? + + public init( + color: Color = .blue, + withoutPadding: Bool = false, + @ViewBuilder content: @escaping () -> Content + ) { self.color = color - self.description = description + self.withoutPadding = withoutPadding + self.content = content + } + + public init( + _ key: LocalizedStringKey, + color: Color = .blue, + withoutPadding: Bool = false + ) where Content == Text { + self.init(color: color, withoutPadding: withoutPadding) { + Text(key) + } } public var body: some View { @@ -43,9 +59,9 @@ public struct LuminareInfoView: View { } .popover(isPresented: $isShowingDescription, arrowEdge: .bottom) { - Text(description) + content() .multilineTextAlignment(.center) - .padding(8) + .padding(withoutPadding ? 0 : 8) } Spacer() diff --git a/Sources/Luminare/Components/LuminareLabeledContent.swift b/Sources/Luminare/Components/LuminareLabeledContent.swift new file mode 100644 index 0000000..1ee280b --- /dev/null +++ b/Sources/Luminare/Components/LuminareLabeledContent.swift @@ -0,0 +1,156 @@ +// +// LuminareLabeledContent.swift +// +// +// Created by KrLite on 2024/10/25. +// + +import SwiftUI + +struct LuminareLabeledContent: View where Label: View, Content: View, Info: View { + let elementMinHeight: CGFloat + let horizontalPadding: CGFloat + let disabled: Bool + private let hasInfo: Bool + + @ViewBuilder private let content: () -> Content + @ViewBuilder private let label: () -> Label + @ViewBuilder private let info: () -> LuminareInfoView + + init( + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + hasInfo: Bool, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder label: @escaping () -> Label, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) { + self.elementMinHeight = elementMinHeight + self.horizontalPadding = horizontalPadding + self.disabled = disabled + self.hasInfo = hasInfo + self.label = label + self.content = content + self.info = info + } + + public init( + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder label: @escaping () -> Label, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) { + self.init( + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled, hasInfo: true, + content: content, label: label, info: info + ) + } + + public init( + _ key: LocalizedStringKey, + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) where Label == Text { + self.init( + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled, hasInfo: true, + content: content + ) { + Text(key) + } info: { + info() + } + } + + public init( + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + infoKey: LocalizedStringKey, + infoWithoutPadding: Bool = false, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder label: @escaping () -> Label + ) where Info == Text { + self.init( + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled, hasInfo: true, + content: content, label: label + ) { + LuminareInfoView(infoKey, withoutPadding: infoWithoutPadding) + } + } + + public init( + _ key: LocalizedStringKey, + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + infoKey: LocalizedStringKey, + infoWithoutPadding: Bool = false, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) where Label == Text, Info == Text { + self.init( + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled, + infoKey: infoKey, infoWithoutPadding: infoWithoutPadding, + content: content + ) { + Text(key) + } + } + + public init( + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder label: @escaping () -> Label + ) where Info == EmptyView { + self.init( + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled, hasInfo: false, + content: content, label: label + ) { + LuminareInfoView { + EmptyView() + } + } + } + + public init( + _ key: LocalizedStringKey, + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + @ViewBuilder content: @escaping () -> Content + ) where Label == Text, Info == EmptyView { + self.init( + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled, + content: content + ) { + Text(key) + } + } + + var body: some View { + HStack { + HStack(spacing: 0) { + label() + + if hasInfo { + info() + } + } + .fixedSize(horizontal: false, vertical: true) + + Spacer() + + content() + .disabled(disabled) + } + .padding(.horizontal, horizontalPadding) + .frame(minHeight: elementMinHeight) + } +} diff --git a/Sources/Luminare/Components/LuminareToggle.swift b/Sources/Luminare/Components/LuminareToggle.swift index 0d54018..606141c 100644 --- a/Sources/Luminare/Components/LuminareToggle.swift +++ b/Sources/Luminare/Components/LuminareToggle.swift @@ -8,8 +8,6 @@ import SwiftUI public struct LuminareToggle: View { - @Environment(\.tintColor) var tintColor - let elementMinHeight: CGFloat = 34 let horizontalPadding: CGFloat = 8 @@ -18,7 +16,6 @@ public struct LuminareToggle: View { @Binding var value: Bool let disabled: Bool - @State var isShowingDescription: Bool = false public init( _ title: LocalizedStringKey, From 6eba2191ead3a0b065b7102ada88051ff496fc11 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 00:37:14 +0800 Subject: [PATCH 002/314] toggle & value adjuster --- .../Components/LuminareInfoView.swift | 6 + .../Components/LuminareLabeledContent.swift | 87 ++++---- .../Luminare/Components/LuminareToggle.swift | 101 ++++++--- .../Components/LuminareValueAdjuster.swift | 202 +++++++++++++----- 4 files changed, 264 insertions(+), 132 deletions(-) diff --git a/Sources/Luminare/Components/LuminareInfoView.swift b/Sources/Luminare/Components/LuminareInfoView.swift index ed24331..1a288e8 100644 --- a/Sources/Luminare/Components/LuminareInfoView.swift +++ b/Sources/Luminare/Components/LuminareInfoView.swift @@ -35,6 +35,12 @@ public struct LuminareInfoView: View where Content: View { Text(key) } } + + public init() where Content == EmptyView { + self.init { + EmptyView() + } + } public var body: some View { VStack { diff --git a/Sources/Luminare/Components/LuminareLabeledContent.swift b/Sources/Luminare/Components/LuminareLabeledContent.swift index 1ee280b..f5cc93a 100644 --- a/Sources/Luminare/Components/LuminareLabeledContent.swift +++ b/Sources/Luminare/Components/LuminareLabeledContent.swift @@ -10,25 +10,25 @@ import SwiftUI struct LuminareLabeledContent: View where Label: View, Content: View, Info: View { let elementMinHeight: CGFloat let horizontalPadding: CGFloat + let spacing: CGFloat? let disabled: Bool - private let hasInfo: Bool @ViewBuilder private let content: () -> Content @ViewBuilder private let label: () -> Label @ViewBuilder private let info: () -> LuminareInfoView - init( + public init( elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + spacing: CGFloat? = nil, disabled: Bool = false, - hasInfo: Bool, @ViewBuilder content: @escaping () -> Content, @ViewBuilder label: @escaping () -> Label, @ViewBuilder info: @escaping () -> LuminareInfoView ) { self.elementMinHeight = elementMinHeight self.horizontalPadding = horizontalPadding + self.spacing = spacing self.disabled = disabled - self.hasInfo = hasInfo self.label = label self.content = content self.info = info @@ -36,30 +36,37 @@ struct LuminareLabeledContent: View where Label: View, Con public init( elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + spacing: CGFloat? = nil, disabled: Bool = false, @ViewBuilder content: @escaping () -> Content, - @ViewBuilder label: @escaping () -> Label, - @ViewBuilder info: @escaping () -> LuminareInfoView - ) { + @ViewBuilder label: @escaping () -> Label + ) where Info == EmptyView { self.init( elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, - disabled: disabled, hasInfo: true, - content: content, label: label, info: info - ) + spacing: spacing, disabled: disabled + ) { + content() + } label: { + label() + } info: { + LuminareInfoView() + } } public init( _ key: LocalizedStringKey, elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + spacing: CGFloat? = nil, disabled: Bool = false, @ViewBuilder content: @escaping () -> Content, @ViewBuilder info: @escaping () -> LuminareInfoView ) where Label == Text { self.init( elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, - disabled: disabled, hasInfo: true, - content: content + spacing: spacing, disabled: disabled ) { + content() + } label: { Text(key) } info: { info() @@ -67,67 +74,55 @@ struct LuminareLabeledContent: View where Label: View, Con } public init( + _ key: LocalizedStringKey, elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + spacing: CGFloat? = nil, disabled: Bool = false, - infoKey: LocalizedStringKey, - infoWithoutPadding: Bool = false, - @ViewBuilder content: @escaping () -> Content, - @ViewBuilder label: @escaping () -> Label - ) where Info == Text { + @ViewBuilder content: @escaping () -> Content + ) where Label == Text, Info == EmptyView { self.init( + key, elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, - disabled: disabled, hasInfo: true, - content: content, label: label + spacing: spacing, disabled: disabled ) { - LuminareInfoView(infoKey, withoutPadding: infoWithoutPadding) + content() + } info: { + LuminareInfoView() } } public init( - _ key: LocalizedStringKey, elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + spacing: CGFloat? = nil, disabled: Bool = false, infoKey: LocalizedStringKey, infoWithoutPadding: Bool = false, @ViewBuilder content: @escaping () -> Content, - @ViewBuilder info: @escaping () -> LuminareInfoView - ) where Label == Text, Info == Text { - self.init( - elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, - disabled: disabled, - infoKey: infoKey, infoWithoutPadding: infoWithoutPadding, - content: content - ) { - Text(key) - } - } - - public init( - elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, - disabled: Bool = false, - @ViewBuilder content: @escaping () -> Content, @ViewBuilder label: @escaping () -> Label - ) where Info == EmptyView { + ) where Info == Text { self.init( elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, - disabled: disabled, hasInfo: false, + spacing: spacing, disabled: disabled, content: content, label: label ) { - LuminareInfoView { - EmptyView() - } + LuminareInfoView(infoKey, withoutPadding: infoWithoutPadding) } } public init( _ key: LocalizedStringKey, elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + spacing: CGFloat? = nil, disabled: Bool = false, - @ViewBuilder content: @escaping () -> Content - ) where Label == Text, Info == EmptyView { + infoKey: LocalizedStringKey, + infoWithoutPadding: Bool = false, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) where Label == Text, Info == Text { self.init( elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, - disabled: disabled, + spacing: spacing, disabled: disabled, + infoKey: infoKey, infoWithoutPadding: infoWithoutPadding, content: content ) { Text(key) @@ -139,7 +134,7 @@ struct LuminareLabeledContent: View where Label: View, Con HStack(spacing: 0) { label() - if hasInfo { + if Info.self != EmptyView.self { info() } } diff --git a/Sources/Luminare/Components/LuminareToggle.swift b/Sources/Luminare/Components/LuminareToggle.swift index 606141c..deb364c 100644 --- a/Sources/Luminare/Components/LuminareToggle.swift +++ b/Sources/Luminare/Components/LuminareToggle.swift @@ -7,48 +7,91 @@ import SwiftUI -public struct LuminareToggle: View { - let elementMinHeight: CGFloat = 34 - let horizontalPadding: CGFloat = 8 - - let title: LocalizedStringKey - let infoView: LuminareInfoView? - @Binding var value: Bool - +public struct LuminareToggle: View where Label: View, Info: View { + let elementMinHeight: CGFloat + let horizontalPadding: CGFloat let disabled: Bool + @ViewBuilder private let label: () -> Label + @ViewBuilder private let info: () -> LuminareInfoView + + @Binding var value: Bool public init( - _ title: LocalizedStringKey, - info: LuminareInfoView? = nil, isOn value: Binding, - disabled: Bool = false + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + @ViewBuilder label: @escaping () -> Label, + @ViewBuilder info: @escaping () -> LuminareInfoView ) { - self.title = title - self.infoView = info - self._value = value + self.elementMinHeight = elementMinHeight + self.horizontalPadding = horizontalPadding self.disabled = disabled + self.label = label + self.info = info + self._value = value + } + + public init( + isOn value: Binding, + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + @ViewBuilder label: @escaping () -> Label + ) where Info == EmptyView { + self.init( + isOn: value, + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled + ) { + label() + } info: { + LuminareInfoView() + } + } + + public init( + _ key: LocalizedStringKey, + isOn value: Binding, + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) where Label == Text { + self.init( + isOn: value, + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled + ) { + Text(key) + } info: { + info() + } + } + + public init( + _ key: LocalizedStringKey, + isOn value: Binding, + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + disabled: Bool = false + ) where Label == Text, Info == EmptyView { + self.init( + key, + isOn: value, + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, + disabled: disabled + ) { + LuminareInfoView() + } } public var body: some View { - HStack { - HStack(spacing: 0) { - Text(title) - - if let infoView { - infoView - } - } - .fixedSize(horizontal: false, vertical: true) - - Spacer() - + LuminareLabeledContent(elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, disabled: disabled) { Toggle("", isOn: $value.animation(LuminareConstants.animation)) .labelsHidden() .controlSize(.small) .toggleStyle(.switch) - .disabled(disabled) + } label: { + label() + } info: { + info() } - .padding(.horizontal, horizontalPadding) - .frame(minHeight: elementMinHeight) } } diff --git a/Sources/Luminare/Components/LuminareValueAdjuster.swift b/Sources/Luminare/Components/LuminareValueAdjuster.swift index f814103..698b8a9 100644 --- a/Sources/Luminare/Components/LuminareValueAdjuster.swift +++ b/Sources/Luminare/Components/LuminareValueAdjuster.swift @@ -7,7 +7,8 @@ import SwiftUI -public struct LuminareValueAdjuster: View where V: Strideable, V: BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { +public struct LuminareValueAdjuster: View +where Label: View, Info: View, Suffix: View, V: Strideable & BinaryFloatingPoint, V.Stride: BinaryFloatingPoint { public enum ControlSize { case regular case compact @@ -20,53 +21,57 @@ public struct LuminareValueAdjuster: View where V: Strideable, V: BinaryFloat } } - let horizontalPadding: CGFloat = 8 + private let horizontalPadding: CGFloat + private let disabled: Bool - let formatter: NumberFormatter - var totalRange: V { + private let formatter: NumberFormatter + private var totalRange: V { sliderRange.upperBound - sliderRange.lowerBound } - @State var isShowingTextBox = false + @State private var isShowingTextBox = false - // Focus enum FocusedField { case textbox } - @FocusState var focusedField: FocusedField? - - let title: LocalizedStringKey - let infoView: LuminareInfoView? - @Binding var value: V - let sliderRange: ClosedRange - let suffix: LocalizedStringKey? - var step: V - let upperClamp: Bool - let lowerClamp: Bool - let controlSize: LuminareValueAdjuster.ControlSize - - let decimalPlaces: Int + @FocusState private var focusedField: FocusedField? + + @ViewBuilder private let label: () -> Label + @ViewBuilder private let suffix: () -> Suffix + @ViewBuilder private let info: () -> LuminareInfoView + + @Binding private var value: V + private let sliderRange: ClosedRange + private var step: V + private let upperClamp: Bool + private let lowerClamp: Bool + private let controlSize: LuminareValueAdjuster.ControlSize + private let decimalPlaces: Int + @State var eventMonitor: AnyObject? - // TODO: MAX DIGIT SPACING FOR LABEL + // TODO: max digit spacing for label public init( - _ title: LocalizedStringKey, - info: LuminareInfoView? = nil, value: Binding, sliderRange: ClosedRange, - suffix: LocalizedStringKey? = nil, + horizontalPadding: CGFloat = 8, + disabled: Bool = false, step: V? = nil, lowerClamp: Bool = false, upperClamp: Bool = false, controlSize: LuminareValueAdjuster.ControlSize = .regular, - decimalPlaces: Int = 0 + decimalPlaces: Int = 0, + @ViewBuilder label: @escaping () -> Label, + @ViewBuilder suffix: @escaping () -> Suffix, + @ViewBuilder info: @escaping () -> LuminareInfoView ) { - self.title = title - self.infoView = info + self.label = label + self.suffix = suffix + self.info = info + self._value = value self.sliderRange = sliderRange - self.suffix = suffix self.lowerClamp = lowerClamp self.upperClamp = upperClamp self.controlSize = controlSize @@ -81,32 +86,126 @@ public struct LuminareValueAdjuster: View where V: Strideable, V: BinaryFloat } else { self.step = 1 } + + self.horizontalPadding = horizontalPadding + self.disabled = disabled + } + + public init( + value: Binding, + sliderRange: ClosedRange, + step: V? = nil, + lowerClamp: Bool = false, + upperClamp: Bool = false, + controlSize: LuminareValueAdjuster.ControlSize = .regular, + decimalPlaces: Int = 0, + @ViewBuilder label: @escaping () -> Label, + @ViewBuilder suffix: @escaping () -> Suffix + ) where Info == EmptyView { + self.init( + value: value, + sliderRange: sliderRange, + step: step, + lowerClamp: lowerClamp, + upperClamp: upperClamp, + controlSize: controlSize, + decimalPlaces: decimalPlaces + ) { + label() + } suffix: { + suffix() + } info: { + LuminareInfoView() + } + } + + public init( + _ key: LocalizedStringKey, + _ suffixKey: LocalizedStringKey, + value: Binding, + sliderRange: ClosedRange, + horizontalPadding: CGFloat = 8, + disabled: Bool = false, + step: V? = nil, + lowerClamp: Bool = false, + upperClamp: Bool = false, + controlSize: LuminareValueAdjuster.ControlSize = .regular, + decimalPlaces: Int = 0, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) where Label == Text, Suffix == Text { + self.init( + value: value, + sliderRange: sliderRange, + horizontalPadding: horizontalPadding, + disabled: disabled, + step: step, + lowerClamp: lowerClamp, + upperClamp: upperClamp, + controlSize: controlSize, + decimalPlaces: decimalPlaces + ) { + Text(key) + } suffix: { + Text(suffixKey) + } info: { + info() + } + } + + public init( + _ key: LocalizedStringKey, + _ suffixKey: LocalizedStringKey, + value: Binding, + sliderRange: ClosedRange, + horizontalPadding: CGFloat = 8, + disabled: Bool = false, + step: V? = nil, + lowerClamp: Bool = false, + upperClamp: Bool = false, + controlSize: LuminareValueAdjuster.ControlSize = .regular, + decimalPlaces: Int = 0 + ) where Label == Text, Suffix == Text, Info == EmptyView { + self.init( + key, + suffixKey, + value: value, + sliderRange: sliderRange, + horizontalPadding: horizontalPadding, + disabled: disabled, + step: step, + lowerClamp: lowerClamp, + upperClamp: upperClamp, + controlSize: controlSize, + decimalPlaces: decimalPlaces + ) { + LuminareInfoView() + } } public var body: some View { VStack { if controlSize == .regular { - HStack { - titleView() - - Spacer() - - labelView() + LuminareLabeledContent(horizontalPadding: horizontalPadding, disabled: disabled) { + content() + } label: { + label() + } info: { + info() } - sliderView() + slider() } else { - HStack(spacing: 12) { - titleView() - - Spacer(minLength: 0) - + LuminareLabeledContent(horizontalPadding: horizontalPadding, spacing: 12, disabled: disabled) { HStack(spacing: 12) { - sliderView() - - labelView() + slider() + + content() } .frame(width: 270) + } label: { + label() + } info: { + info() } } } @@ -116,18 +215,7 @@ public struct LuminareValueAdjuster: View where V: Strideable, V: BinaryFloat .animation(LuminareConstants.animation, value: isShowingTextBox) } - func titleView() -> some View { - HStack(spacing: 0) { - Text(title) - - if let infoView { - infoView - } - } - .fixedSize(horizontal: false, vertical: true) - } - - func sliderView() -> some View { + func slider() -> some View { Slider( value: Binding( get: { @@ -143,7 +231,7 @@ public struct LuminareValueAdjuster: View where V: Strideable, V: BinaryFloat } @ViewBuilder - func labelView() -> some View { + func content() -> some View { HStack { if isShowingTextBox { TextField( @@ -190,8 +278,8 @@ public struct LuminareValueAdjuster: View where V: Strideable, V: BinaryFloat .buttonStyle(PlainButtonStyle()) } - if let suffix { - Text(suffix) + if Suffix.self != EmptyView.self { + suffix() .padding(.leading, -6) } } From 344a93f25d6b67265782054a24ee6cf5b4d4a327 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 00:46:30 +0800 Subject: [PATCH 003/314] text field --- .../Components/LuminareTextField.swift | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/Sources/Luminare/Components/LuminareTextField.swift b/Sources/Luminare/Components/LuminareTextField.swift index fa7d5de..f8a9770 100644 --- a/Sources/Luminare/Components/LuminareTextField.swift +++ b/Sources/Luminare/Components/LuminareTextField.swift @@ -7,26 +7,39 @@ import SwiftUI -public struct LuminareTextField: View where F: ParseableFormatStyle, F.FormatOutput == String { - let elementMinHeight: CGFloat = 34 - let horizontalPadding: CGFloat = 8 - +public struct LuminareTextField: View +where F: ParseableFormatStyle, F.FormatOutput == String { + let elementMinHeight: CGFloat + let horizontalPadding: CGFloat + @Binding var value: F.FormatInput? - var format: F + let format: F let placeholder: LocalizedStringKey - let onSubmit: (() -> ())? @State var monitor: Any? - public init(_ placeholder: LocalizedStringKey, value: Binding, format: F, onSubmit: (() -> ())? = nil) { + public init( + _ placeholder: LocalizedStringKey, + value: Binding, format: F, + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8 + ) { + self.elementMinHeight = elementMinHeight + self.horizontalPadding = horizontalPadding self._value = value self.format = format self.placeholder = placeholder - self.onSubmit = onSubmit } - public init(_ placeholder: LocalizedStringKey, text: Binding, onSubmit: (() -> ())? = nil) where F == StringFormatStyle { - self.init(placeholder, value: .init(text), format: StringFormatStyle(), onSubmit: onSubmit) + public init( + _ placeholder: LocalizedStringKey, + text: Binding, + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8 + ) where F == StringFormatStyle { + self.init( + placeholder, + value: .init(text), format: StringFormatStyle(), + elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding + ) } public var body: some View { @@ -34,12 +47,6 @@ public struct LuminareTextField: View where F: ParseableFormatStyle, F.Format .padding(.horizontal, horizontalPadding) .frame(minHeight: elementMinHeight) .textFieldStyle(.plain) - .onSubmit { - if let onSubmit { - onSubmit() - } - } - .onAppear { guard monitor != nil else { return } From 56bb673774c21de869940faeee31290821a2fc0a Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 00:56:29 +0800 Subject: [PATCH 004/314] Text field & slider picker --- .../Components/LuminareSliderPicker.swift | 144 +++++++++++++----- .../Components/LuminareTextField.swift | 12 +- 2 files changed, 108 insertions(+), 48 deletions(-) diff --git a/Sources/Luminare/Components/LuminareSliderPicker.swift b/Sources/Luminare/Components/LuminareSliderPicker.swift index 4b2e655..ef556ba 100644 --- a/Sources/Luminare/Components/LuminareSliderPicker.swift +++ b/Sources/Luminare/Components/LuminareSliderPicker.swift @@ -7,33 +7,116 @@ import SwiftUI -public struct LuminareSliderPicker: View where V: Equatable { - let height: CGFloat = 70 +public struct LuminareSliderPicker: View +where Label: View, Content: View, Info: View, V: Equatable { + private let height: CGFloat + private let horizontalPadding: CGFloat + + @ViewBuilder private let label: () -> Label + @ViewBuilder private let content: (V) -> Content + @ViewBuilder private let info: () -> LuminareInfoView - let title: LocalizedStringKey + private let options: [V] + @Binding private var selection: V - let options: [V] - @Binding var selection: V - - let label: (V) -> LocalizedStringKey - - let horizontalPadding: CGFloat = 8 - - public init(_ title: LocalizedStringKey, _ options: [V], selection: Binding, label: @escaping (V) -> LocalizedStringKey) { - self.title = title + public init( + _ options: [V], selection: Binding, + height: CGFloat = 70, + horizontalPadding: CGFloat = 8, + @ViewBuilder label: @escaping () -> Label, + @ViewBuilder content: @escaping (V) -> Content, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) { + self.height = height + self.horizontalPadding = horizontalPadding + self.label = label + self.content = content + self.info = info self.options = options self._selection = selection - self.label = label + } + + public init( + _ key: LocalizedStringKey, + _ options: [V], selection: Binding, + height: CGFloat = 70, + horizontalPadding: CGFloat = 8, + contentKey: @escaping (V) -> LocalizedStringKey, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) where Label == Text, Content == Text { + self.init( + options, selection: selection, + height: height, + horizontalPadding: horizontalPadding + ) { + Text(key) + } content: { value in + Text(contentKey(value)) + } info: { + info() + } + } + + public init( + _ options: [V], selection: Binding, + height: CGFloat = 70, + horizontalPadding: CGFloat = 8, + @ViewBuilder label: @escaping () -> Label, + @ViewBuilder content: @escaping (V) -> Content + ) where Info == EmptyView { + self.init( + options, selection: selection, + height: height, + horizontalPadding: horizontalPadding + ) { + label() + } content: { value in + content(value) + } info: { + LuminareInfoView() + } + } + + public init( + _ key: LocalizedStringKey, + _ options: [V], selection: Binding, + height: CGFloat = 70, + horizontalPadding: CGFloat = 8, + contentKey: @escaping (V) -> LocalizedStringKey + ) where Label == Text, Content == Text, Info == EmptyView { + self.init( + key, + options, selection: selection, + height: height, + horizontalPadding: horizontalPadding, + contentKey: contentKey + ) { + LuminareInfoView() + } } public var body: some View { VStack { - HStack { - Text(title) - - Spacer() - - labelView() + LuminareLabeledContent(horizontalPadding: horizontalPadding) { + content(selection) + .contentTransition(.numericText()) + .multilineTextAlignment(.trailing) + .monospaced() + .padding(4) + .padding(.horizontal, 4) + .background { + ZStack { + Capsule() + .strokeBorder(.quaternary, lineWidth: 1) + + Capsule() + .foregroundStyle(.quinary.opacity(0.5)) + } + } + .fixedSize() + .clipShape(.capsule) + } label: { + label() } Slider( @@ -53,27 +136,4 @@ public struct LuminareSliderPicker: View where V: Equatable { .frame(height: height) .animation(LuminareConstants.animation, value: selection) } - - @ViewBuilder - func labelView() -> some View { - HStack { - Text(label(selection)) - .contentTransition(.numericText()) - .multilineTextAlignment(.trailing) - .monospaced() - .padding(4) - .padding(.horizontal, 4) - .background { - ZStack { - Capsule() - .strokeBorder(.quaternary, lineWidth: 1) - - Capsule() - .foregroundStyle(.quinary.opacity(0.5)) - } - } - .fixedSize() - .clipShape(.capsule) - } - } } diff --git a/Sources/Luminare/Components/LuminareTextField.swift b/Sources/Luminare/Components/LuminareTextField.swift index f8a9770..c3b7c75 100644 --- a/Sources/Luminare/Components/LuminareTextField.swift +++ b/Sources/Luminare/Components/LuminareTextField.swift @@ -9,14 +9,14 @@ import SwiftUI public struct LuminareTextField: View where F: ParseableFormatStyle, F.FormatOutput == String { - let elementMinHeight: CGFloat - let horizontalPadding: CGFloat + private let elementMinHeight: CGFloat + private let horizontalPadding: CGFloat - @Binding var value: F.FormatInput? - let format: F - let placeholder: LocalizedStringKey + @Binding private var value: F.FormatInput? + private let format: F + private let placeholder: LocalizedStringKey - @State var monitor: Any? + @State private var monitor: Any? public init( _ placeholder: LocalizedStringKey, From c6bd1a013ca5a32ec217140a7a8b9dae732e2ec4 Mon Sep 17 00:00:00 2001 From: KrLite <68179735+KrLite@users.noreply.github.com> Date: Sat, 26 Oct 2024 07:22:42 +0800 Subject: [PATCH 005/314] Remove padding in LuminareInfoView --- Sources/Luminare/Components/LuminareInfoView.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Sources/Luminare/Components/LuminareInfoView.swift b/Sources/Luminare/Components/LuminareInfoView.swift index 1a288e8..0739cfc 100644 --- a/Sources/Luminare/Components/LuminareInfoView.swift +++ b/Sources/Luminare/Components/LuminareInfoView.swift @@ -9,7 +9,6 @@ import SwiftUI public struct LuminareInfoView: View where Content: View { let color: Color - let withoutPadding: Bool @ViewBuilder private let content: () -> Content @State private var isShowingDescription: Bool = false @@ -18,20 +17,17 @@ public struct LuminareInfoView: View where Content: View { public init( color: Color = .blue, - withoutPadding: Bool = false, @ViewBuilder content: @escaping () -> Content ) { self.color = color - self.withoutPadding = withoutPadding self.content = content } public init( _ key: LocalizedStringKey, - color: Color = .blue, - withoutPadding: Bool = false + color: Color = .blue ) where Content == Text { - self.init(color: color, withoutPadding: withoutPadding) { + self.init(color: color) { Text(key) } } @@ -67,7 +63,6 @@ public struct LuminareInfoView: View where Content: View { .popover(isPresented: $isShowingDescription, arrowEdge: .bottom) { content() .multilineTextAlignment(.center) - .padding(withoutPadding ? 0 : 8) } Spacer() From 512c6f9d1d837df652e2be841b3a0f1be0e4ba96 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 18:35:50 +0800 Subject: [PATCH 006/314] Refine LuminareInfoView --- .../Color Picker/LuminareColorPicker.swift | 18 ++++----- .../Components/LuminareInfoView.swift | 37 +++++++++++++++++-- .../Components/LuminareLabeledContent.swift | 6 +-- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift b/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift index fb1aa1c..d9aa2c4 100644 --- a/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift +++ b/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift @@ -30,16 +30,16 @@ public struct LuminareColorPicker: View { LuminareTextField( "Hex Color", value: .init($text), - format: StringFormatStyle(parseStrategy: .hex(formatStrategy)), - onSubmit: { - if let newColor = Color(hex: text) { - currentColor = newColor - text = newColor.toHex() - } else { - text = currentColor.toHex() // revert to last valid color - } - } + format: StringFormatStyle(parseStrategy: .hex(formatStrategy)) ) + .onSubmit { + if let newColor = Color(hex: text) { + currentColor = newColor + text = newColor.toHex() + } else { + text = currentColor.toHex() // revert to last valid color + } + } .modifier(LuminareBordered()) Button { diff --git a/Sources/Luminare/Components/LuminareInfoView.swift b/Sources/Luminare/Components/LuminareInfoView.swift index 0739cfc..6ae7692 100644 --- a/Sources/Luminare/Components/LuminareInfoView.swift +++ b/Sources/Luminare/Components/LuminareInfoView.swift @@ -9,6 +9,7 @@ import SwiftUI public struct LuminareInfoView: View where Content: View { let color: Color + let arrowEdge: Edge @ViewBuilder private let content: () -> Content @State private var isShowingDescription: Bool = false @@ -16,18 +17,21 @@ public struct LuminareInfoView: View where Content: View { @State private var hoverTimer: Timer? public init( - color: Color = .blue, + color: Color = .accentColor, + arrowEdge: Edge = .bottom, @ViewBuilder content: @escaping () -> Content ) { self.color = color + self.arrowEdge = arrowEdge self.content = content } public init( _ key: LocalizedStringKey, - color: Color = .blue + color: Color = .accentColor, + arrowEdge: Edge = .bottom ) where Content == Text { - self.init(color: color) { + self.init(color: color, arrowEdge: arrowEdge) { Text(key) } } @@ -60,7 +64,7 @@ public struct LuminareInfoView: View where Content: View { } } - .popover(isPresented: $isShowingDescription, arrowEdge: .bottom) { + .popover(isPresented: $isShowingDescription, arrowEdge: arrowEdge) { content() .multilineTextAlignment(.center) } @@ -69,3 +73,28 @@ public struct LuminareInfoView: View where Content: View { } } } + +#Preview { + VStack { + HStack { + Text("A sentence") + + LuminareInfoView { + Text("An info description") + .padding() + } + } + .fixedSize(horizontal: false, vertical: true) + + HStack { + Text("A sentence") + + LuminareInfoView(color: .violet, arrowEdge: .leading) { + Text("An info description") + .padding() + } + } + .fixedSize(horizontal: false, vertical: true) + } + .padding() +} diff --git a/Sources/Luminare/Components/LuminareLabeledContent.swift b/Sources/Luminare/Components/LuminareLabeledContent.swift index f5cc93a..f685780 100644 --- a/Sources/Luminare/Components/LuminareLabeledContent.swift +++ b/Sources/Luminare/Components/LuminareLabeledContent.swift @@ -96,7 +96,6 @@ struct LuminareLabeledContent: View where Label: View, Con spacing: CGFloat? = nil, disabled: Bool = false, infoKey: LocalizedStringKey, - infoWithoutPadding: Bool = false, @ViewBuilder content: @escaping () -> Content, @ViewBuilder label: @escaping () -> Label ) where Info == Text { @@ -105,7 +104,7 @@ struct LuminareLabeledContent: View where Label: View, Con spacing: spacing, disabled: disabled, content: content, label: label ) { - LuminareInfoView(infoKey, withoutPadding: infoWithoutPadding) + LuminareInfoView(infoKey) } } @@ -115,14 +114,13 @@ struct LuminareLabeledContent: View where Label: View, Con spacing: CGFloat? = nil, disabled: Bool = false, infoKey: LocalizedStringKey, - infoWithoutPadding: Bool = false, @ViewBuilder content: @escaping () -> Content, @ViewBuilder info: @escaping () -> LuminareInfoView ) where Label == Text, Info == Text { self.init( elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding, spacing: spacing, disabled: disabled, - infoKey: infoKey, infoWithoutPadding: infoWithoutPadding, + infoKey: infoKey, content: content ) { Text(key) From 15d2fbe4e3f18d2d72e633331f29bae4e9060ac9 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 19:27:20 +0800 Subject: [PATCH 007/314] Lists & Sections - #1 --- .../Luminare/Components/DividedVStack.swift | 32 ++- .../Components/LuminareButtonStyle.swift | 2 +- .../Components/LuminareInfoView.swift | 21 +- .../Components/LuminareLabeledContent.swift | 18 ++ .../Luminare/Components/LuminareList.swift | 139 +++++------- .../Luminare/Components/LuminarePicker.swift | 8 +- .../Luminare/Components/LuminareSection.swift | 211 ++++++++++++++++-- .../Components/LuminareTextField.swift | 7 + .../Luminare/Components/LuminareToggle.swift | 7 + .../Components/LuminareValueAdjuster.swift | 17 ++ 10 files changed, 346 insertions(+), 116 deletions(-) diff --git a/Sources/Luminare/Components/DividedVStack.swift b/Sources/Luminare/Components/DividedVStack.swift index 0826a20..620a6d2 100644 --- a/Sources/Luminare/Components/DividedVStack.swift +++ b/Sources/Luminare/Components/DividedVStack.swift @@ -8,17 +8,23 @@ import SwiftUI -public struct DividedVStack: View { +public struct DividedVStack: View where Content: View { let spacing: CGFloat? let applyMaskToItems: Bool let showDividers: Bool - var content: Content + + @ViewBuilder let content: () -> Content - public init(spacing: CGFloat? = nil, applyMaskToItems: Bool = true, showDividers: Bool = true, @ViewBuilder content: () -> Content) { + public init( + spacing: CGFloat? = nil, + applyMaskToItems: Bool = true, + showDividers: Bool = true, + @ViewBuilder content: @escaping () -> Content + ) { self.spacing = spacing self.applyMaskToItems = applyMaskToItems self.showDividers = showDividers - self.content = content() + self.content = content } public var body: some View { @@ -29,7 +35,7 @@ public struct DividedVStack: View { showDividers: showDividers ) ) { - content + content() } } } @@ -38,7 +44,6 @@ struct DividedVStackLayout: _VariadicView_UnaryViewRoot { let spacing: CGFloat let applyMaskToItems: Bool let showDividers: Bool - let innerPadding: CGFloat = 4 init(spacing: CGFloat?, applyMaskToItems: Bool, showDividers: Bool) { @@ -88,8 +93,8 @@ public struct LuminareCroppedSectionItem: ViewModifier { let innerPadding: CGFloat = 4 let innerCornerRadius: CGFloat = 2 - let isFirstChild: Bool - let isLastChild: Bool + private let isFirstChild: Bool + private let isLastChild: Bool public init(isFirstChild: Bool, isLastChild: Bool) { self.isFirstChild = isFirstChild @@ -134,3 +139,14 @@ public struct LuminareCroppedSectionItem: ViewModifier { } } } + +#Preview { + LuminareSection { + DividedVStack { + ForEach(37..<43) { num in + Text("\(num)") + } + } + } + .padding() +} diff --git a/Sources/Luminare/Components/LuminareButtonStyle.swift b/Sources/Luminare/Components/LuminareButtonStyle.swift index e3788ba..db13878 100644 --- a/Sources/Luminare/Components/LuminareButtonStyle.swift +++ b/Sources/Luminare/Components/LuminareButtonStyle.swift @@ -13,7 +13,7 @@ public struct LuminareButtonStyle: ButtonStyle { let innerCornerRadius: CGFloat = 2 let elementMinHeight: CGFloat = 34 - @State var isHovering: Bool = false + @State private var isHovering: Bool = false public init() {} diff --git a/Sources/Luminare/Components/LuminareInfoView.swift b/Sources/Luminare/Components/LuminareInfoView.swift index 6ae7692..3057021 100644 --- a/Sources/Luminare/Components/LuminareInfoView.swift +++ b/Sources/Luminare/Components/LuminareInfoView.swift @@ -10,6 +10,7 @@ import SwiftUI public struct LuminareInfoView: View where Content: View { let color: Color let arrowEdge: Edge + @ViewBuilder private let content: () -> Content @State private var isShowingDescription: Bool = false @@ -75,26 +76,26 @@ public struct LuminareInfoView: View where Content: View { } #Preview { - VStack { - HStack { - Text("A sentence") - + LuminareSection { + LuminareLabeledContent { + } label: { + Text("Pops to bottom") + } info: { LuminareInfoView { Text("An info description") .padding() } } - .fixedSize(horizontal: false, vertical: true) - HStack { - Text("A sentence") - - LuminareInfoView(color: .violet, arrowEdge: .leading) { + LuminareLabeledContent { + } label: { + Text("Pops to trailing") + } info: { + LuminareInfoView(color: .violet, arrowEdge: .trailing) { Text("An info description") .padding() } } - .fixedSize(horizontal: false, vertical: true) } .padding() } diff --git a/Sources/Luminare/Components/LuminareLabeledContent.swift b/Sources/Luminare/Components/LuminareLabeledContent.swift index f685780..e2dfa86 100644 --- a/Sources/Luminare/Components/LuminareLabeledContent.swift +++ b/Sources/Luminare/Components/LuminareLabeledContent.swift @@ -147,3 +147,21 @@ struct LuminareLabeledContent: View where Label: View, Con .frame(minHeight: elementMinHeight) } } + +#Preview { + LuminareSection { + LuminareLabeledContent { + Button { + + } label: { + Text("Test") + .frame(height: 30) + .padding(.horizontal, 8) + } + .buttonStyle(LuminareCompactButtonStyle(extraCompact: true)) + } label: { + Text("Label") + } + } + .padding() +} diff --git a/Sources/Luminare/Components/LuminareList.swift b/Sources/Luminare/Components/LuminareList.swift index 435dd19..aadae60 100644 --- a/Sources/Luminare/Components/LuminareList.swift +++ b/Sources/Luminare/Components/LuminareList.swift @@ -7,106 +7,61 @@ import SwiftUI -public struct LuminareList: View where ContentA: View, ContentB: View, V: Hashable, ID: Hashable { - @Environment(\.tintColor) var tintColor - @Environment(\.clickedOutsideFlag) var clickedOutsideFlag - - let header: LocalizedStringKey? - @Binding var items: [V] - @Binding var selection: Set - let addAction: () -> () - let content: (Binding) -> ContentA - let emptyView: () -> ContentB +public struct LuminareList: View +where Header: View, ContentA: View, ContentB: View, Footer: View, V: Hashable, ID: Hashable { + @Environment(\.tintColor) private var tintColor + @Environment(\.clickedOutsideFlag) private var clickedOutsideFlag + + @Binding private var items: [V] + @Binding private var selection: Set + private let addAction: () -> () + + @ViewBuilder private let content: (Binding) -> ContentA + @ViewBuilder private let emptyView: () -> ContentB + @ViewBuilder private let header: () -> Header + @ViewBuilder private let footer: () -> Footer @State private var firstItem: V? @State private var lastItem: V? - let id: KeyPath + private let id: KeyPath - let addText: LocalizedStringKey - let removeText: LocalizedStringKey + private let addText: LocalizedStringKey + private let removeText: LocalizedStringKey - @State var canRefreshSelection = true - let cornerRadius: CGFloat = 2 - let lineWidth: CGFloat = 1.5 - @State var eventMonitor: AnyObject? + @State private var canRefreshSelection = true + @State private var eventMonitor: AnyObject? public init( - _ header: LocalizedStringKey? = nil, items: Binding<[V]>, selection: Binding>, + id: KeyPath, + addText: LocalizedStringKey, + removeText: LocalizedStringKey, addAction: @escaping () -> (), @ViewBuilder content: @escaping (Binding) -> ContentA, @ViewBuilder emptyView: @escaping () -> ContentB, - id: KeyPath, - addText: LocalizedStringKey, - removeText: LocalizedStringKey + @ViewBuilder header: @escaping () -> Header, + @ViewBuilder footer: @escaping () -> Footer ) { - self.header = header self._items = items self._selection = selection self.addAction = addAction - self.content = content - self.emptyView = emptyView self.id = id self.addText = addText self.removeText = removeText - } - - public init( - _ header: LocalizedStringKey? = nil, - addText: LocalizedStringKey, - removeText: LocalizedStringKey, - items: Binding<[V]>, - selection: Binding>, - id: KeyPath, - @ViewBuilder content: @escaping (Binding) -> ContentA, - @ViewBuilder emptyView: @escaping () -> ContentB, - addAction: @escaping () -> () - ) { - self.init( - header, - items: items, - selection: selection, - addAction: addAction, - content: content, - emptyView: emptyView, - id: id, - addText: addText, - removeText: removeText - ) - } - - public init( - _ header: LocalizedStringKey? = nil, - addText: LocalizedStringKey, - removeText: LocalizedStringKey, - items: Binding<[V]>, - selection: Binding>, - id: KeyPath, - @ViewBuilder content: @escaping (Binding) -> ContentA, - addAction: @escaping () -> () - ) where ContentB == EmptyView { - self.init( - header, - addText: addText, - removeText: removeText, - items: items, - selection: selection, - id: id, - content: content, - emptyView: { - EmptyView() - }, - addAction: addAction - ) + self.content = content + self.emptyView = emptyView + self.header = header + self.footer = footer } public var body: some View { - LuminareSection(header, disablePadding: true) { + LuminareSection(disablePadding: true) { HStack(spacing: 2) { Button(addText) { addAction() } + .buttonStyle(LuminareButtonStyle()) Button(removeText) { if !selection.isEmpty { @@ -166,6 +121,10 @@ public struct LuminareList: View where ContentA: View .scrollDisabled(true) .listStyle(.plain) } + } header: { + header() + } footer: { + footer() } .onChange(of: clickedOutsideFlag) { _ in withAnimation(LuminareConstants.animation) { @@ -234,10 +193,10 @@ public struct LuminareList: View where ContentA: View } struct LuminareListItem: View where Content: View, V: Hashable { - @Environment(\.tintColor) var tintColor + @Environment(\.tintColor) private var tintColor - @Binding var item: V - let content: (Binding) -> Content + @Binding private var item: V + @ViewBuilder private let content: (Binding) -> Content @Binding var items: [V] @Binding var selection: Set @@ -246,14 +205,14 @@ struct LuminareListItem: View where Content: View, V: Hashable { @Binding var lastItem: V? @Binding var canRefreshSelection: Bool - @State var isHovering = false + @State private var isHovering = false let cornerRadius: CGFloat = 2 let maxLineWidth: CGFloat = 1.5 - @State var lineWidth: CGFloat = .zero + @State private var lineWidth: CGFloat = .zero let maxTintOpacity: CGFloat = 0.15 - @State var tintOpacity: CGFloat = .zero + @State private var tintOpacity: CGFloat = .zero init( items: Binding<[V]>, @@ -480,3 +439,23 @@ extension NSTableView { selectionHighlightStyle = .none } } + +#Preview { + LuminareList( + items: .constant([37, 42, 1, 0]), + selection: .constant([0, 42]), + id: \.self, + addText: .init("Add"), + removeText: .init("Remove") + ) { + } content: { num in + Text("\(num.wrappedValue)") + } emptyView: { + Text("Empty") + } header: { + Text("Header") + } footer: { + Text("Footer") + } + .padding() +} diff --git a/Sources/Luminare/Components/LuminarePicker.swift b/Sources/Luminare/Components/LuminarePicker.swift index e827796..f4d58e8 100644 --- a/Sources/Luminare/Components/LuminarePicker.swift +++ b/Sources/Luminare/Components/LuminarePicker.swift @@ -12,7 +12,7 @@ public protocol LuminarePickerData { } public struct LuminarePicker: View where Content: View, V: Equatable { - @Environment(\.tintColor) var tintColor + @Environment(\.tintColor) private var tintColor let cornerRadius: CGFloat = 12 let innerPadding: CGFloat = 4 @@ -25,9 +25,9 @@ public struct LuminarePicker: View where Content: View, V: Equatable @Binding var selectedItem: V @State var internalSelection: V - let roundTop: Bool - let roundBottom: Bool - let content: (V) -> Content + private let roundTop: Bool + private let roundBottom: Bool + @ViewBuilder private let content: (V) -> Content public init( elements: [V], diff --git a/Sources/Luminare/Components/LuminareSection.swift b/Sources/Luminare/Components/LuminareSection.swift index db77906..bc03f8d 100644 --- a/Sources/Luminare/Components/LuminareSection.swift +++ b/Sources/Luminare/Components/LuminareSection.swift @@ -7,35 +7,196 @@ import SwiftUI -public struct LuminareSection: View { - let headerSpacing: CGFloat = 8 - let cornerRadius: CGFloat = 12 - let innerPadding: CGFloat = 4 - - let header: LocalizedStringKey? +public struct LuminareSection: View where Header: View, Content: View, Footer: View { let disablePadding: Bool let showDividers: Bool let noBorder: Bool - let content: () -> Content + + let headerSpacing: CGFloat + let footerSpacing: CGFloat + let cornerRadius: CGFloat + let innerPadding: CGFloat + + @ViewBuilder let content: () -> Content + @ViewBuilder let header: () -> Header + @ViewBuilder let footer: () -> Footer - public init(_ header: LocalizedStringKey? = nil, disablePadding: Bool = false, showDividers: Bool = true, noBorder: Bool = false, @ViewBuilder _ content: @escaping () -> Content) { - self.header = header + public init( + disablePadding: Bool = false, + showDividers: Bool = true, + noBorder: Bool = false, + headerSpacing: CGFloat = 8, footerSpacing: CGFloat = 8, + cornerRadius: CGFloat = 12, innerPadding: CGFloat = 4, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder header: @escaping () -> Header, + @ViewBuilder footer: @escaping () -> Footer + ) { self.disablePadding = disablePadding self.showDividers = showDividers self.noBorder = noBorder + self.headerSpacing = headerSpacing + self.footerSpacing = footerSpacing + self.cornerRadius = cornerRadius + self.innerPadding = innerPadding self.content = content + self.header = header + self.footer = footer + } + + public init( + _ headerKey: LocalizedStringKey, + _ footerKey: LocalizedStringKey, + disablePadding: Bool = false, + showDividers: Bool = true, + noBorder: Bool = false, + headerSpacing: CGFloat = 8, footerSpacing: CGFloat = 8, + cornerRadius: CGFloat = 12, innerPadding: CGFloat = 4, + @ViewBuilder content: @escaping () -> Content + ) where Header == Text, Footer == Text { + self.init( + disablePadding: disablePadding, + showDividers: showDividers, + noBorder: noBorder, + headerSpacing: headerSpacing, footerSpacing: footerSpacing, + cornerRadius: cornerRadius, innerPadding: innerPadding + ) { + content() + } header: { + Text(headerKey) + } footer: { + Text(footerKey) + } + } + + public init( + disablePadding: Bool = false, + showDividers: Bool = true, + noBorder: Bool = false, + headerSpacing: CGFloat = 8, footerSpacing: CGFloat = 8, + cornerRadius: CGFloat = 12, innerPadding: CGFloat = 4, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder header: @escaping () -> Header + ) where Footer == EmptyView { + self.init( + disablePadding: disablePadding, + showDividers: showDividers, + noBorder: noBorder, + headerSpacing: headerSpacing, footerSpacing: footerSpacing, + cornerRadius: cornerRadius, innerPadding: innerPadding + ) { + content() + } header: { + header() + } footer: { + EmptyView() + } + } + + public init( + _ headerKey: LocalizedStringKey, + disablePadding: Bool = false, + showDividers: Bool = true, + noBorder: Bool = false, + headerSpacing: CGFloat = 8, footerSpacing: CGFloat = 8, + cornerRadius: CGFloat = 12, innerPadding: CGFloat = 4, + @ViewBuilder content: @escaping () -> Content + ) where Header == Text, Footer == EmptyView { + self.init( + disablePadding: disablePadding, + showDividers: showDividers, + noBorder: noBorder, + headerSpacing: headerSpacing, footerSpacing: footerSpacing, + cornerRadius: cornerRadius, innerPadding: innerPadding + ) { + content() + } header: { + Text(headerKey) + } + } + + public init( + disablePadding: Bool = false, + showDividers: Bool = true, + noBorder: Bool = false, + headerSpacing: CGFloat = 8, footerSpacing: CGFloat = 8, + cornerRadius: CGFloat = 12, innerPadding: CGFloat = 4, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder footer: @escaping () -> Footer + ) where Header == EmptyView { + self.init( + disablePadding: disablePadding, + showDividers: showDividers, + noBorder: noBorder, + headerSpacing: headerSpacing, footerSpacing: footerSpacing, + cornerRadius: cornerRadius, innerPadding: innerPadding + ) { + content() + } header: { + EmptyView() + } footer: { + footer() + } + } + + public init( + _ footerKey: LocalizedStringKey, + disablePadding: Bool = false, + showDividers: Bool = true, + noBorder: Bool = false, + headerSpacing: CGFloat = 8, footerSpacing: CGFloat = 8, + cornerRadius: CGFloat = 12, innerPadding: CGFloat = 4, + @ViewBuilder content: @escaping () -> Content + ) where Header == EmptyView, Footer == Text { + self.init( + disablePadding: disablePadding, + showDividers: showDividers, + noBorder: noBorder, + headerSpacing: headerSpacing, footerSpacing: footerSpacing, + cornerRadius: cornerRadius, innerPadding: innerPadding + ) { + content() + } footer: { + Text(footerKey) + } + } + + public init( + disablePadding: Bool = false, + showDividers: Bool = true, + noBorder: Bool = false, + headerSpacing: CGFloat = 8, footerSpacing: CGFloat = 8, + cornerRadius: CGFloat = 12, innerPadding: CGFloat = 4, + @ViewBuilder content: @escaping () -> Content + ) where Header == EmptyView, Footer == EmptyView { + self.init( + disablePadding: disablePadding, + showDividers: showDividers, + noBorder: noBorder, + headerSpacing: headerSpacing, footerSpacing: footerSpacing, + cornerRadius: cornerRadius, innerPadding: innerPadding + ) { + content() + } header: { + EmptyView() + } footer: { + EmptyView() + } } public var body: some View { - VStack(spacing: headerSpacing) { - if let header { + VStack(spacing: 0) { + if Header.self != EmptyView.self { HStack { - Text(header) + header() + Spacer() } .foregroundStyle(.secondary) + + Spacer() + .frame(height: headerSpacing) } - + if noBorder { content() } else { @@ -50,6 +211,30 @@ public struct LuminareSection: View { .strokeBorder(.quaternary, lineWidth: 1) } } + + if Footer.self != EmptyView.self { + Spacer() + .frame(height: footerSpacing) + + HStack { + footer() + + Spacer() + } + .foregroundStyle(.secondary) + } + } } } + +#Preview { + LuminareSection { + Text("Content") + } header: { + Text("Header") + } footer: { + Text("Footer") + } + .padding() +} diff --git a/Sources/Luminare/Components/LuminareTextField.swift b/Sources/Luminare/Components/LuminareTextField.swift index c3b7c75..2c69762 100644 --- a/Sources/Luminare/Components/LuminareTextField.swift +++ b/Sources/Luminare/Components/LuminareTextField.swift @@ -71,3 +71,10 @@ where F: ParseableFormatStyle, F.FormatOutput == String { } } } + +#Preview { + LuminareSection { + LuminareTextField("Text Field", text: .constant("Test")) + } + .padding() +} diff --git a/Sources/Luminare/Components/LuminareToggle.swift b/Sources/Luminare/Components/LuminareToggle.swift index deb364c..e12b564 100644 --- a/Sources/Luminare/Components/LuminareToggle.swift +++ b/Sources/Luminare/Components/LuminareToggle.swift @@ -95,3 +95,10 @@ public struct LuminareToggle: View where Label: View, Info: View { } } } + +#Preview { + LuminareSection { + LuminareToggle("Toggle", isOn: .constant(true)) + } + .padding() +} diff --git a/Sources/Luminare/Components/LuminareValueAdjuster.swift b/Sources/Luminare/Components/LuminareValueAdjuster.swift index 698b8a9..6ccb0e5 100644 --- a/Sources/Luminare/Components/LuminareValueAdjuster.swift +++ b/Sources/Luminare/Components/LuminareValueAdjuster.swift @@ -363,3 +363,20 @@ private extension Comparable { min(max(self, limits.lowerBound), limits.upperBound) } } + +#Preview { + LuminareSection { + LuminareValueAdjuster( + value: .constant(42), + sliderRange: 0...128, + step: 1, + lowerClamp: true, + upperClamp: false + ) { + Text("Value Adjuster") + } suffix: { + Text("Suffix") + } + } + .padding() +} From 299652bcba8035492a29aa7ed3c908a52a1a39c3 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 19:45:33 +0800 Subject: [PATCH 008/314] Lists & Sections - #2 --- .../Luminare/Components/LuminareList.swift | 294 +++++++++++++++++- .../Luminare/Components/LuminareSection.swift | 4 +- 2 files changed, 292 insertions(+), 6 deletions(-) diff --git a/Sources/Luminare/Components/LuminareList.swift b/Sources/Luminare/Components/LuminareList.swift index aadae60..28a5b27 100644 --- a/Sources/Luminare/Components/LuminareList.swift +++ b/Sources/Luminare/Components/LuminareList.swift @@ -33,10 +33,8 @@ where Header: View, ContentA: View, ContentB: View, Footer: View, V: Hashable, I public init( items: Binding<[V]>, - selection: Binding>, - id: KeyPath, - addText: LocalizedStringKey, - removeText: LocalizedStringKey, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, addAction: @escaping () -> (), @ViewBuilder content: @escaping (Binding) -> ContentA, @ViewBuilder emptyView: @escaping () -> ContentB, @@ -54,6 +52,294 @@ where Header: View, ContentA: View, ContentB: View, Footer: View, V: Hashable, I self.header = header self.footer = footer } + + public init( + _ headerKey: LocalizedStringKey, + _ footerKey: LocalizedStringKey, + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder emptyView: @escaping () -> ContentB + ) where Header == Text, Footer == Text { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: emptyView, + header: { + Text(headerKey) + }, + footer: { + Text(footerKey) + } + ) + } + + public init( + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder header: @escaping () -> Header, + @ViewBuilder footer: @escaping () -> Footer + ) where ContentB == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: { + EmptyView() + }, + header: header, + footer: footer + ) + } + + public init( + _ headerKey: LocalizedStringKey, + _ footerKey: LocalizedStringKey, + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA + ) where Header == Text, ContentB == EmptyView, Footer == Text { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + header: { + Text(headerKey) + }, + footer: { + Text(footerKey) + } + ) + } + + public init( + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder emptyView: @escaping () -> ContentB, + @ViewBuilder header: @escaping () -> Header + ) where Footer == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: emptyView, + header: header, + footer: { + EmptyView() + } + ) + } + + public init( + headerKey: LocalizedStringKey, + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder emptyView: @escaping () -> ContentB + ) where Header == Text, Footer == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: emptyView, + header: { + Text(headerKey) + } + ) + } + + public init( + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder header: @escaping () -> Header + ) where ContentB == EmptyView, Footer == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: { + EmptyView() + }, + header: header + ) + } + + public init( + headerKey: LocalizedStringKey, + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA + ) where Header == Text, ContentB == EmptyView, Footer == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + header: { + Text(headerKey) + } + ) + } + + public init( + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder emptyView: @escaping () -> ContentB, + @ViewBuilder footer: @escaping () -> Footer + ) where Header == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: emptyView, + header: { + EmptyView() + }, + footer: footer + ) + } + + public init( + footerKey: LocalizedStringKey, + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder emptyView: @escaping () -> ContentB + ) where Header == EmptyView, Footer == Text { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: emptyView, + footer: { + Text(footerKey) + } + ) + } + + public init( + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder footer: @escaping () -> Footer + ) where Header == EmptyView, ContentB == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: { + EmptyView() + }, + footer: footer + ) + } + + public init( + footerKey: LocalizedStringKey, + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA + ) where Header == EmptyView, ContentB == EmptyView, Footer == Text { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + footer: { + Text(footerKey) + } + ) + } + + public init( + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA, + @ViewBuilder emptyView: @escaping () -> ContentB + ) where Header == EmptyView, Footer == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: emptyView, + header: { + EmptyView() + }, + footer: { + EmptyView() + } + ) + } + + public init( + items: Binding<[V]>, + selection: Binding>, id: KeyPath, + addText: LocalizedStringKey, removeText: LocalizedStringKey, + addAction: @escaping () -> (), + @ViewBuilder content: @escaping (Binding) -> ContentA + ) where Header == EmptyView, ContentB == EmptyView, Footer == EmptyView { + self.init( + items: items, + selection: selection, id: id, + addText: addText, removeText: removeText, + addAction: addAction, + content: content, + emptyView: { + EmptyView() + } + ) + } public var body: some View { LuminareSection(disablePadding: true) { diff --git a/Sources/Luminare/Components/LuminareSection.swift b/Sources/Luminare/Components/LuminareSection.swift index bc03f8d..a0989f6 100644 --- a/Sources/Luminare/Components/LuminareSection.swift +++ b/Sources/Luminare/Components/LuminareSection.swift @@ -93,7 +93,7 @@ public struct LuminareSection: View where Header: View, } public init( - _ headerKey: LocalizedStringKey, + headerKey: LocalizedStringKey, disablePadding: Bool = false, showDividers: Bool = true, noBorder: Bool = false, @@ -139,7 +139,7 @@ public struct LuminareSection: View where Header: View, } public init( - _ footerKey: LocalizedStringKey, + footerKey: LocalizedStringKey, disablePadding: Bool = false, showDividers: Bool = true, noBorder: Bool = false, From 91ab4a2e771a99f0ebe2b7fc629f77178b842dda Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 19:48:04 +0800 Subject: [PATCH 009/314] Update LuminareList.swift --- Sources/Luminare/Components/LuminareList.swift | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Sources/Luminare/Components/LuminareList.swift b/Sources/Luminare/Components/LuminareList.swift index 28a5b27..08e615d 100644 --- a/Sources/Luminare/Components/LuminareList.swift +++ b/Sources/Luminare/Components/LuminareList.swift @@ -384,13 +384,14 @@ where Header: View, ContentA: View, ContentB: View, Footer: View, V: Hashable, I items: $items, selection: $selection, item: item, - content: content, firstItem: $firstItem, lastItem: $lastItem, - canRefreshSelection: $canRefreshSelection + canRefreshSelection: $canRefreshSelection, + content: content ) } - // .onDelete(perform: deleteItems) // deleteItems crashes Loop, need to be investigated further + // TODO: `deleteItems` crashes Loop, need to be investigated further + // .onDelete(perform: deleteItems) .onMove { indices, newOffset in withAnimation(LuminareConstants.animation) { items.move(fromOffsets: indices, toOffset: newOffset) @@ -436,6 +437,7 @@ where Header: View, ContentA: View, ContentB: View, Footer: View, V: Hashable, I } } + // TODO: investigate this // #warning("onDelete & deleteItems WILL crash on macOS 14.5, but it's fine on 14.4 and below.") // private func deleteItems(at offsets: IndexSet) { // withAnimation { @@ -504,18 +506,18 @@ struct LuminareListItem: View where Content: View, V: Hashable { items: Binding<[V]>, selection: Binding>, item: Binding, - @ViewBuilder content: @escaping (Binding) -> Content, firstItem: Binding, lastItem: Binding, - canRefreshSelection: Binding + canRefreshSelection: Binding, + @ViewBuilder content: @escaping (Binding) -> Content ) { self._items = items self._selection = selection self._item = item - self.content = content self._firstItem = firstItem self._lastItem = lastItem self._canRefreshSelection = canRefreshSelection + self.content = content } var body: some View { From 1b66c854d8e04879ee9a6710a29882456fcdc0de Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 19:52:59 +0800 Subject: [PATCH 010/314] Picker --- .../Luminare/Components/LuminarePicker.swift | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/Sources/Luminare/Components/LuminarePicker.swift b/Sources/Luminare/Components/LuminarePicker.swift index f4d58e8..eae3516 100644 --- a/Sources/Luminare/Components/LuminarePicker.swift +++ b/Sources/Luminare/Components/LuminarePicker.swift @@ -11,7 +11,8 @@ public protocol LuminarePickerData { var selectable: Bool { get } } -public struct LuminarePicker: View where Content: View, V: Equatable { +public struct LuminarePicker: View +where Content: View, V: Equatable { @Environment(\.tintColor) private var tintColor let cornerRadius: CGFloat = 12 @@ -74,12 +75,13 @@ public struct LuminarePicker: View where Content: View, V: Equatable .frame(minHeight: 150) } } - // This will improve animation performance + // this improves animation performance .onChange(of: internalSelection) { _ in withAnimation(LuminareConstants.animation) { selectedItem = internalSelection } } + .buttonStyle(LuminareButtonStyle()) } @ViewBuilder func pickerButton(i: Int, j: Int) -> some View { @@ -170,3 +172,20 @@ extension Array { } } } + +#Preview { + LuminareSection { + Text("Pick Your Lucky Number") + .bold() + .font(.title3) + .padding() + + LuminarePicker( + elements: Array(30..<50), + selection: .constant(42) + ) { num in + Text("\(num)") + } + } + .padding() +} From efbfd9437f8a320fa29edf2f2de07c5d72497396 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 20:07:16 +0800 Subject: [PATCH 011/314] Color picker --- .../Color Picker/LuminareColorPicker.swift | 46 +++++++++++++++---- .../Utilities/StringFormatStyle.swift | 6 +++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift b/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift index d9aa2c4..0ee7ae4 100644 --- a/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift +++ b/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift @@ -7,22 +7,36 @@ import SwiftUI -public struct LuminareColorPicker: View { +public struct LuminareColorPicker: View +where F: ParseableFormatStyle, F.FormatInput == String, F.FormatOutput == String { @Binding var currentColor: Color @State private var text: String @State private var showColorPicker = false - let colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey) - let formatStrategy: StringFormatStyle.HexStrategy + private let colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey) + private let format: F public init( - color: Binding, colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey), - formatStrategy: StringFormatStyle.HexStrategy = .uppercasedWithWell + color: Binding, + colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey), + format: F ) { self._currentColor = color self._text = State(initialValue: color.wrappedValue.toHex()) self.colorNames = colorNames - self.formatStrategy = formatStrategy + self.format = format + } + + public init( + color: Binding, + colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey), + parseStrategy: StringFormatStyle.Strategy = .hex(.lowercasedWithWell) + ) where F == StringFormatStyle { + self.init( + color: color, + colorNames: colorNames, + format: .init(parseStrategy: parseStrategy) + ) } public var body: some View { @@ -30,14 +44,15 @@ public struct LuminareColorPicker: View { LuminareTextField( "Hex Color", value: .init($text), - format: StringFormatStyle(parseStrategy: .hex(formatStrategy)) + format: format ) .onSubmit { if let newColor = Color(hex: text) { currentColor = newColor text = newColor.toHex() } else { - text = currentColor.toHex() // revert to last valid color + // revert to last valid color + text = currentColor.toHex() } } .modifier(LuminareBordered()) @@ -62,3 +77,18 @@ public struct LuminareColorPicker: View { } } } + +// preview this as app to show the modal panel +#Preview { + LuminareColorPicker( + color: .constant(.accentColor), + colorNames: ( + red: .init("Red"), + green: .init("Green"), + blue: .init("Blue") + ), + parseStrategy: .hex(.custom(true, "$")) + ) + .monospaced() + .padding() +} diff --git a/Sources/Luminare/Utilities/StringFormatStyle.swift b/Sources/Luminare/Utilities/StringFormatStyle.swift index c786a46..02b2f5e 100644 --- a/Sources/Luminare/Utilities/StringFormatStyle.swift +++ b/Sources/Luminare/Utilities/StringFormatStyle.swift @@ -33,6 +33,7 @@ public struct StringFormatStyle: Codable, Equatable, Hashable, ParseableFormatSt public enum HexStrategy: Codable, Equatable, Hashable, ParseStrategy { public typealias ParseInput = String public typealias ParseOutput = String + public typealias Lowercased = Bool /// `#42ab0E` -> `42ab0e` case lowercased @@ -45,6 +46,9 @@ public struct StringFormatStyle: Codable, Equatable, Hashable, ParseableFormatSt /// `42ab0E` -> `#42AB0E` case uppercasedWithWell + + /// Customized case and prefix characters. + case custom(Lowercased, String) public func parse(_ value: String) throws -> String { switch self { @@ -58,6 +62,8 @@ public struct StringFormatStyle: Codable, Equatable, Hashable, ParseableFormatSt try "#" + Self.lowercased.parse(value) case .uppercasedWithWell: try "#" + Self.uppercased.parse(value) + case .custom(let lowercased, let prefix): + try prefix + (lowercased ? Self.lowercased : Self.uppercased).parse(value) } } } From 8bf1ec857956f0b743a22e94a9c62cab3eac9d1f Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 20:23:02 +0800 Subject: [PATCH 012/314] Compact picker - #1 --- .../Luminare/Components/DividedVStack.swift | 2 +- .../Components/LuminareCompactPicker.swift | 77 +++++++++++++++++++ .../Luminare/Components/LuminarePicker.swift | 19 +++-- 3 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 Sources/Luminare/Components/LuminareCompactPicker.swift diff --git a/Sources/Luminare/Components/DividedVStack.swift b/Sources/Luminare/Components/DividedVStack.swift index 620a6d2..21b454b 100644 --- a/Sources/Luminare/Components/DividedVStack.swift +++ b/Sources/Luminare/Components/DividedVStack.swift @@ -4,7 +4,7 @@ // // Created by Kai Azim on 2024-04-02. // -// Thank you https://movingparts.io/variadic-views-in-swiftui +// Thanks to https://movingparts.io/variadic-views-in-swiftui import SwiftUI diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift new file mode 100644 index 0000000..76dad58 --- /dev/null +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -0,0 +1,77 @@ +// +// LuminareCompactPicker.swift +// Luminare +// +// Created by KrLite on 2024/10/26. +// + +import SwiftUI + +struct LuminareCompactPicker: View +where Label: View, Content: View, Info: View, V: Hashable & Equatable { + let elementMinHeight: CGFloat + let horizontalPadding: CGFloat + let disabled: Bool = false + + @Binding private var selection: V + @ViewBuilder private let content: () -> Content + @ViewBuilder private let label: () -> Label + @ViewBuilder private let info: () -> LuminareInfoView + + public init( + elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, + selection: Binding, + @ViewBuilder content: @escaping () -> Content, + @ViewBuilder label: @escaping () -> Label, + @ViewBuilder info: @escaping () -> LuminareInfoView + ) { + self.elementMinHeight = elementMinHeight + self.horizontalPadding = horizontalPadding + self._selection = selection + self.content = content + self.label = label + self.info = info + } + + var body: some View { + LuminareLabeledContent(elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding) { + Picker("", selection: $selection) { + content() + } + .pickerStyle(.menu) + .background(.clear) + .clipShape(Capsule()) + .fixedSize() + .padding(4) + .background { + ZStack { + Capsule() + .strokeBorder(.quaternary, lineWidth: 1) + + Capsule() + .foregroundStyle(.quinary.opacity(0.5)) + } + } + .disabled(disabled) + } label: { + label() + } info: { + info() + } + } +} + +#Preview { + LuminareSection { + LuminareCompactPicker(selection: .constant(42)) { + ForEach(0..<200) { num in + Text("\(num)") + } + } label: { + Text("Picker") + } info: { + LuminareInfoView() + } + } + .padding() +} diff --git a/Sources/Luminare/Components/LuminarePicker.swift b/Sources/Luminare/Components/LuminarePicker.swift index eae3516..d8257e4 100644 --- a/Sources/Luminare/Components/LuminarePicker.swift +++ b/Sources/Luminare/Components/LuminarePicker.swift @@ -125,35 +125,44 @@ where Content: View, V: Equatable { } func getShape(i: Int, j: Int) -> some InsettableShape { - if j == 0, i == 0, roundTop { // Top left + // top left + if j == 0, i == 0, roundTop { UnevenRoundedRectangle( topLeadingRadius: cornerRadius - innerPadding, bottomLeadingRadius: (rowsIndex == 0 && roundBottom) ? cornerRadius - innerPadding : innerCornerRadius, bottomTrailingRadius: innerCornerRadius, topTrailingRadius: (columnsIndex == 0) ? cornerRadius - innerPadding : innerCornerRadius ) - } else if j == 0, i == rowsIndex, roundBottom { // Bottom left + } + // bottom left + else if j == 0, i == rowsIndex, roundBottom { UnevenRoundedRectangle( topLeadingRadius: innerCornerRadius, bottomLeadingRadius: cornerRadius - innerPadding, bottomTrailingRadius: (columnsIndex == 0) ? cornerRadius - innerPadding : innerCornerRadius, topTrailingRadius: innerCornerRadius ) - } else if j == columnsIndex, i == 0, roundTop { // Top right + } + // top right + else if j == columnsIndex, i == 0, roundTop { UnevenRoundedRectangle( topLeadingRadius: innerCornerRadius, bottomLeadingRadius: innerCornerRadius, bottomTrailingRadius: (rowsIndex == 0 && roundBottom) ? cornerRadius - innerPadding : innerCornerRadius, topTrailingRadius: cornerRadius - innerPadding ) - } else if j == columnsIndex, i == rowsIndex, roundBottom { // Bottom right + } + // bottom right + else if j == columnsIndex, i == rowsIndex, roundBottom { UnevenRoundedRectangle( topLeadingRadius: innerCornerRadius, bottomLeadingRadius: innerCornerRadius, bottomTrailingRadius: cornerRadius - innerPadding, topTrailingRadius: innerCornerRadius ) - } else { + } + // regular + else { UnevenRoundedRectangle( topLeadingRadius: innerCornerRadius, bottomLeadingRadius: innerCornerRadius, From 421b1f29540d4f2695a182dd4264551c18cc5ce8 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 20:52:34 +0800 Subject: [PATCH 013/314] Compact picker - #2 --- Sources/Luminare/Components/LuminareCompactPicker.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index 76dad58..6ede96d 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -38,8 +38,8 @@ where Label: View, Content: View, Info: View, V: Hashable & Equatable { Picker("", selection: $selection) { content() } - .pickerStyle(.menu) - .background(.clear) + .buttonStyle(.borderless) + .padding(.leading, -4) .clipShape(Capsule()) .fixedSize() .padding(4) From fd4135943f1b08bca433d4d62558ffd75d064301 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 21:17:20 +0800 Subject: [PATCH 014/314] Compact picker - #3 --- .../Components/LuminareCompactPicker.swift | 84 ++++++++++--------- .../Components/LuminareLabeledContent.swift | 7 +- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index 6ede96d..dfea735 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -7,57 +7,53 @@ import SwiftUI -struct LuminareCompactPicker: View -where Label: View, Content: View, Info: View, V: Hashable & Equatable { - let elementMinHeight: CGFloat - let horizontalPadding: CGFloat - let disabled: Bool = false +struct LuminareCompactPicker: View +where Content: View, V: Hashable & Equatable { + let cornerRadius: CGFloat @Binding private var selection: V @ViewBuilder private let content: () -> Content - @ViewBuilder private let label: () -> Label - @ViewBuilder private let info: () -> LuminareInfoView + + @State var isHovering: Bool = false public init( - elementMinHeight: CGFloat = 34, horizontalPadding: CGFloat = 8, selection: Binding, - @ViewBuilder content: @escaping () -> Content, - @ViewBuilder label: @escaping () -> Label, - @ViewBuilder info: @escaping () -> LuminareInfoView + cornerRadius: CGFloat = 8, + @ViewBuilder content: @escaping () -> Content ) { - self.elementMinHeight = elementMinHeight - self.horizontalPadding = horizontalPadding + self.cornerRadius = cornerRadius self._selection = selection self.content = content - self.label = label - self.info = info } var body: some View { - LuminareLabeledContent(elementMinHeight: elementMinHeight, horizontalPadding: horizontalPadding) { - Picker("", selection: $selection) { - content() + Picker(selection: $selection) { + content() + } label: { + EmptyView() + } + .padding(.leading, 2) + .buttonStyle(.borderless) + .fixedSize() + .background { + if isHovering { + Rectangle() + .foregroundStyle(.quaternary.opacity(0.7)) + } else { + Rectangle() + .foregroundStyle(.quinary) } - .buttonStyle(.borderless) - .padding(.leading, -4) - .clipShape(Capsule()) - .fixedSize() - .padding(4) - .background { - ZStack { - Capsule() - .strokeBorder(.quaternary, lineWidth: 1) - - Capsule() - .foregroundStyle(.quinary.opacity(0.5)) - } + } + .background { + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(.quaternary, lineWidth: 1) + } + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)).onHover { hover in + withAnimation(LuminareConstants.fastAnimation) { + isHovering = hover } - .disabled(disabled) - } label: { - label() - } info: { - info() } + .animation(LuminareConstants.fastAnimation, value: isHovering) } } @@ -67,11 +63,19 @@ where Label: View, Content: View, Info: View, V: Hashable & Equatable { ForEach(0..<200) { num in Text("\(num)") } - } label: { - Text("Picker") - } info: { - LuminareInfoView() } + + LuminareLabeledContent("Button") { + Button { + + } label: { + Text("Test") + .frame(height: 30) + .padding(.horizontal, 8) + } + .buttonStyle(LuminareCompactButtonStyle(extraCompact: true)) + } + .padding(.trailing, -4) } .padding() } diff --git a/Sources/Luminare/Components/LuminareLabeledContent.swift b/Sources/Luminare/Components/LuminareLabeledContent.swift index e2dfa86..f023052 100644 --- a/Sources/Luminare/Components/LuminareLabeledContent.swift +++ b/Sources/Luminare/Components/LuminareLabeledContent.swift @@ -128,7 +128,7 @@ struct LuminareLabeledContent: View where Label: View, Con } var body: some View { - HStack { + HStack(spacing: spacing) { HStack(spacing: 0) { label() @@ -150,7 +150,7 @@ struct LuminareLabeledContent: View where Label: View, Con #Preview { LuminareSection { - LuminareLabeledContent { + LuminareLabeledContent("Label") { Button { } label: { @@ -159,9 +159,8 @@ struct LuminareLabeledContent: View where Label: View, Con .padding(.horizontal, 8) } .buttonStyle(LuminareCompactButtonStyle(extraCompact: true)) - } label: { - Text("Label") } + .padding(.trailing, -4) } .padding() } From a16c26a8382d3474484c06a87cf953c9c5f5358d Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 21:27:13 +0800 Subject: [PATCH 015/314] Compact picker - #4 --- .../Components/LuminareCompactPicker.swift | 42 ++++++------------- 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index dfea735..101c602 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -9,19 +9,13 @@ import SwiftUI struct LuminareCompactPicker: View where Content: View, V: Hashable & Equatable { - let cornerRadius: CGFloat - @Binding private var selection: V @ViewBuilder private let content: () -> Content - @State var isHovering: Bool = false - public init( selection: Binding, - cornerRadius: CGFloat = 8, @ViewBuilder content: @escaping () -> Content ) { - self.cornerRadius = cornerRadius self._selection = selection self.content = content } @@ -32,38 +26,26 @@ where Content: View, V: Hashable & Equatable { } label: { EmptyView() } - .padding(.leading, 2) + .padding(.trailing, -2) .buttonStyle(.borderless) - .fixedSize() - .background { - if isHovering { - Rectangle() - .foregroundStyle(.quaternary.opacity(0.7)) - } else { - Rectangle() - .foregroundStyle(.quinary) - } - } - .background { - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(.quaternary, lineWidth: 1) - } - .clipShape(RoundedRectangle(cornerRadius: cornerRadius)).onHover { hover in - withAnimation(LuminareConstants.fastAnimation) { - isHovering = hover - } - } - .animation(LuminareConstants.fastAnimation, value: isHovering) } } #Preview { LuminareSection { - LuminareCompactPicker(selection: .constant(42)) { - ForEach(0..<200) { num in - Text("\(num)") + LuminareLabeledContent("Picker") { + Picker(selection: .constant(42)) { + ForEach(0..<200) { num in + Text("\(num)") + } + } label: { + EmptyView() } + .frame(height: 30) + .padding(.horizontal, 8) + .buttonStyle(LuminareCompactButtonStyle(extraCompact: true)) } + .padding(.trailing, -4) LuminareLabeledContent("Button") { Button { From a862aceb2880b26ac3dba275723b33140f714161 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 22:48:26 +0800 Subject: [PATCH 016/314] Partly solves compact picker --- .../Components/LuminareButtonStyle.swift | 35 +++++++++--- .../Components/LuminareCompactPicker.swift | 53 +++++++++++++++---- .../Components/Popover/PopoverHolder.swift | 14 ++--- 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/Sources/Luminare/Components/LuminareButtonStyle.swift b/Sources/Luminare/Components/LuminareButtonStyle.swift index db13878..246a629 100644 --- a/Sources/Luminare/Components/LuminareButtonStyle.swift +++ b/Sources/Luminare/Components/LuminareButtonStyle.swift @@ -136,12 +136,14 @@ public struct LuminareCompactButtonStyle: ButtonStyle { let elementMinHeight: CGFloat = 34 let elementExtraMinHeight: CGFloat = 25 let extraCompact: Bool + let borderlessWhileNotHovering: Bool @State var isHovering: Bool = false let cornerRadius: CGFloat = 8 - public init(extraCompact: Bool = false) { + public init(extraCompact: Bool = false, borderlessWhileNotHovering: Bool = false) { self.extraCompact = extraCompact + self.borderlessWhileNotHovering = borderlessWhileNotHovering } public func makeBody(configuration: Configuration) -> some View { @@ -149,10 +151,7 @@ public struct LuminareCompactButtonStyle: ButtonStyle { .padding(.horizontal, extraCompact ? 0 : 12) .frame(maxWidth: .infinity, maxHeight: .infinity) .background(backgroundForState(isPressed: configuration.isPressed)) - .background { - RoundedRectangle(cornerRadius: cornerRadius) - .strokeBorder(.quaternary, lineWidth: 1) - } + .background(border()) .fixedSize(horizontal: extraCompact, vertical: extraCompact) .clipShape(.rect(cornerRadius: cornerRadius)) .onHover { hover in @@ -164,15 +163,35 @@ public struct LuminareCompactButtonStyle: ButtonStyle { .frame(minHeight: extraCompact ? elementExtraMinHeight : elementMinHeight) .opacity(isEnabled ? 1 : 0.5) } + + @ViewBuilder private func border() -> some View { + Group { + if isHovering || !borderlessWhileNotHovering { + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(.quaternary, lineWidth: 1) + } else { + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(.clear, lineWidth: 1) + } + } + } - private func backgroundForState(isPressed: Bool) -> some View { + @ViewBuilder private func backgroundForState(isPressed: Bool) -> some View { Group { if isPressed { Rectangle().foregroundStyle(.quaternary) } else if isHovering { - Rectangle().foregroundStyle(.quaternary.opacity(0.7)) + if borderlessWhileNotHovering { + Rectangle().foregroundStyle(.quinary) + } else { + Rectangle().foregroundStyle(.quaternary.opacity(0.7)) + } } else { - Rectangle().foregroundStyle(.quinary) + if borderlessWhileNotHovering { + Rectangle().foregroundStyle(.clear) + } else { + Rectangle().foregroundStyle(.quinary) + } } } } diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index 101c602..0b08c51 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -9,41 +9,74 @@ import SwiftUI struct LuminareCompactPicker: View where Content: View, V: Hashable & Equatable { + let elementMinHeight: CGFloat + let horizontalPadding: CGFloat + let cornerRadius: CGFloat + @Binding private var selection: V @ViewBuilder private let content: () -> Content + @State var isHovering: Bool = false + public init( selection: Binding, + elementMinHeight: CGFloat = 30, horizontalPadding: CGFloat = 4, + cornerRadius: CGFloat = 8, @ViewBuilder content: @escaping () -> Content ) { self._selection = selection + self.elementMinHeight = elementMinHeight + self.horizontalPadding = horizontalPadding + self.cornerRadius = cornerRadius self.content = content } var body: some View { - Picker(selection: $selection) { + Picker("", selection: $selection) { content() - } label: { - EmptyView() } - .padding(.trailing, -2) + .labelsHidden() + .pickerStyle(.menu) .buttonStyle(.borderless) + .padding(.trailing, -2) + .onHover { hover in + withAnimation(LuminareConstants.fastAnimation) { + isHovering = hover + } + } + .frame(minHeight: elementMinHeight) + .padding(.horizontal, horizontalPadding) + .background { + if isHovering { + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(.quaternary, lineWidth: 1) + } else { + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(.clear, lineWidth: 1) + } + } + .background { + if isHovering { + Rectangle() + .foregroundStyle(.quinary) + } else { + Rectangle() + .foregroundStyle(.clear) + } + } + .clipShape(.rect(cornerRadius: cornerRadius)) + .animation(LuminareConstants.fastAnimation, value: isHovering) } } #Preview { LuminareSection { LuminareLabeledContent("Picker") { - Picker(selection: .constant(42)) { + LuminareCompactPicker(selection: .constant(42)) { ForEach(0..<200) { num in Text("\(num)") } - } label: { - EmptyView() } - .frame(height: 30) - .padding(.horizontal, 8) - .buttonStyle(LuminareCompactButtonStyle(extraCompact: true)) } .padding(.trailing, -4) diff --git a/Sources/Luminare/Components/Popover/PopoverHolder.swift b/Sources/Luminare/Components/Popover/PopoverHolder.swift index 7337b00..1183742 100644 --- a/Sources/Luminare/Components/Popover/PopoverHolder.swift +++ b/Sources/Luminare/Components/Popover/PopoverHolder.swift @@ -44,9 +44,9 @@ public struct PopoverHolder: NSViewRepresentable { super.init() } - // View is optional bevause it is not needed to close the popup + // view is optional bevause it is not needed to close the popup func setVisible(_ isPresented: Bool, in view: NSView? = nil) { - // If we're going to be closing the window + // if we're going to be closing the window guard isPresented else { popover?.resignKey() return @@ -58,22 +58,22 @@ public struct PopoverHolder: NSViewRepresentable { initializePopup() guard let popover else { return } - // Popover size + // popover size let targetSize = NSSize(width: 300, height: 300) let extraPadding: CGFloat = 10 - // Get coordinates to place popopver + // get coordinates to place popopver guard let windowFrame = view.window?.frame else { return } let viewBounds = view.bounds - var targetPoint = view.convert(viewBounds, to: nil).origin // Convert to window coordinates + var targetPoint = view.convert(viewBounds, to: nil).origin // convert to window coordinates originalYPoint = targetPoint.y - // Correct popover position + // correct popover position targetPoint.y += windowFrame.minY targetPoint.x += windowFrame.minX targetPoint.y -= targetSize.height + extraPadding - // Set position and show popover + // set position and show popover popover.setContentSize(targetSize) popover.setFrameOrigin(targetPoint) popover.makeKeyAndOrderFront(nil) From c9670cb7bf5b920decd06ff896ec75c4503efa10 Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 22:53:03 +0800 Subject: [PATCH 017/314] Update LuminareCompactPicker.swift --- .../Luminare/Components/LuminareCompactPicker.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Luminare/Components/LuminareCompactPicker.swift b/Sources/Luminare/Components/LuminareCompactPicker.swift index 0b08c51..f76c7de 100644 --- a/Sources/Luminare/Components/LuminareCompactPicker.swift +++ b/Sources/Luminare/Components/LuminareCompactPicker.swift @@ -12,6 +12,7 @@ where Content: View, V: Hashable & Equatable { let elementMinHeight: CGFloat let horizontalPadding: CGFloat let cornerRadius: CGFloat + let borderless: Bool @Binding private var selection: V @ViewBuilder private let content: () -> Content @@ -22,12 +23,14 @@ where Content: View, V: Hashable & Equatable { selection: Binding, elementMinHeight: CGFloat = 30, horizontalPadding: CGFloat = 4, cornerRadius: CGFloat = 8, + borderless: Bool = true, @ViewBuilder content: @escaping () -> Content ) { self._selection = selection self.elementMinHeight = elementMinHeight self.horizontalPadding = horizontalPadding self.cornerRadius = cornerRadius + self.borderless = borderless self.content = content } @@ -50,9 +53,12 @@ where Content: View, V: Hashable & Equatable { if isHovering { RoundedRectangle(cornerRadius: cornerRadius) .strokeBorder(.quaternary, lineWidth: 1) - } else { + } else if borderless { RoundedRectangle(cornerRadius: cornerRadius) .strokeBorder(.clear, lineWidth: 1) + } else { + RoundedRectangle(cornerRadius: cornerRadius) + .strokeBorder(.quaternary.opacity(0.7), lineWidth: 1) } } .background { @@ -72,7 +78,7 @@ where Content: View, V: Hashable & Equatable { #Preview { LuminareSection { LuminareLabeledContent("Picker") { - LuminareCompactPicker(selection: .constant(42)) { + LuminareCompactPicker(selection: .constant(42), borderless: false) { ForEach(0..<200) { num in Text("\(num)") } From 5b91e68179cc4c000890d6b6a28b5179dfc461ba Mon Sep 17 00:00:00 2001 From: KrLite Date: Sat, 26 Oct 2024 23:57:15 +0800 Subject: [PATCH 018/314] Rename to compose --- .../{ => Auxiliary}/DividedVStack.swift | 0 .../{ => Auxiliary}/LuminareButtonStyle.swift | 0 .../{ => Auxiliary}/LuminareInfoView.swift | 4 +- .../Color Picker/ColorHueSliderView.swift | 9 ++- .../Color Picker/ColorPickerModalView.swift | 74 +++++++++++++------ .../ColorSaturationBrightnessView.swift | 15 +++- .../Color Picker/LuminareColorPicker.swift | 20 ++--- .../Color Picker/RGBInputField.swift | 17 ++++- .../LuminareCompose.swift} | 6 +- .../LuminareSliderPickerCompose.swift} | 68 ++++++++++++++--- .../LuminareToggleCompose.swift} | 11 ++- .../LuminareValueAdjusterCompose.swift} | 22 +++--- .../Components/LuminareCompactPicker.swift | 8 +- .../Luminare/Components/LuminareSection.swift | 39 ++++++---- 14 files changed, 206 insertions(+), 87 deletions(-) rename Sources/Luminare/Components/{ => Auxiliary}/DividedVStack.swift (100%) rename Sources/Luminare/Components/{ => Auxiliary}/LuminareButtonStyle.swift (100%) rename Sources/Luminare/Components/{ => Auxiliary}/LuminareInfoView.swift (97%) rename Sources/Luminare/Components/{LuminareLabeledContent.swift => Compose/LuminareCompose.swift} (96%) rename Sources/Luminare/Components/{LuminareSliderPicker.swift => Compose/LuminareSliderPickerCompose.swift} (70%) rename Sources/Luminare/Components/{LuminareToggle.swift => Compose/LuminareToggleCompose.swift} (88%) rename Sources/Luminare/Components/{LuminareValueAdjuster.swift => Compose/LuminareValueAdjusterCompose.swift} (93%) diff --git a/Sources/Luminare/Components/DividedVStack.swift b/Sources/Luminare/Components/Auxiliary/DividedVStack.swift similarity index 100% rename from Sources/Luminare/Components/DividedVStack.swift rename to Sources/Luminare/Components/Auxiliary/DividedVStack.swift diff --git a/Sources/Luminare/Components/LuminareButtonStyle.swift b/Sources/Luminare/Components/Auxiliary/LuminareButtonStyle.swift similarity index 100% rename from Sources/Luminare/Components/LuminareButtonStyle.swift rename to Sources/Luminare/Components/Auxiliary/LuminareButtonStyle.swift diff --git a/Sources/Luminare/Components/LuminareInfoView.swift b/Sources/Luminare/Components/Auxiliary/LuminareInfoView.swift similarity index 97% rename from Sources/Luminare/Components/LuminareInfoView.swift rename to Sources/Luminare/Components/Auxiliary/LuminareInfoView.swift index 3057021..ac963cf 100644 --- a/Sources/Luminare/Components/LuminareInfoView.swift +++ b/Sources/Luminare/Components/Auxiliary/LuminareInfoView.swift @@ -77,7 +77,7 @@ public struct LuminareInfoView: View where Content: View { #Preview { LuminareSection { - LuminareLabeledContent { + LuminareCompose { } label: { Text("Pops to bottom") } info: { @@ -87,7 +87,7 @@ public struct LuminareInfoView: View where Content: View { } } - LuminareLabeledContent { + LuminareCompose { } label: { Text("Pops to trailing") } info: { diff --git a/Sources/Luminare/Components/Color Picker/ColorHueSliderView.swift b/Sources/Luminare/Components/Color Picker/ColorHueSliderView.swift index 27be997..394ec70 100644 --- a/Sources/Luminare/Components/Color Picker/ColorHueSliderView.swift +++ b/Sources/Luminare/Components/Color Picker/ColorHueSliderView.swift @@ -14,7 +14,7 @@ struct ColorHueSliderView: View { @State private var selectionCornerRadius: CGFloat = 0 @State private var selectionWidth: CGFloat = 0 - // Gradient for the color spectrum slider + // gradient for the color spectrum slider private let colorSpectrumGradient = Gradient( colors: stride(from: 0.0, through: 1.0, by: 0.01) .map { @@ -109,3 +109,10 @@ struct ColorHueSliderView: View { return 15 * edgeFactor } } + +#Preview { + LuminareSection { + ColorHueSliderView(selectedColor: .constant(.accentColor)) + } + .padding() +} diff --git a/Sources/Luminare/Components/Color Picker/ColorPickerModalView.swift b/Sources/Luminare/Components/Color Picker/ColorPickerModalView.swift index 1146637..b2a01c1 100644 --- a/Sources/Luminare/Components/Color Picker/ColorPickerModalView.swift +++ b/Sources/Luminare/Components/Color Picker/ColorPickerModalView.swift @@ -7,17 +7,26 @@ import SwiftUI -// View for the color popup as a whole -struct ColorPickerModalView: View { +public typealias RGBColorNames = ( + red: R, + green: G, + blue: B +) + + +// view for the color popup as a whole +struct ColorPickerModalView: View where R: View, G: View, B: View { + typealias ColorNames = RGBColorNames + @Binding var color: Color @Binding var hexColor: String @State private var redComponent: Double = 0 @State private var greenComponent: Double = 0 @State private var blueComponent: Double = 0 - let colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey) + let colorNames: ColorNames - // Main view containing all components of the color picker + // main view containing all components of the color picker var body: some View { Group { LuminareSection(disablePadding: true, showDividers: false) { @@ -57,34 +66,40 @@ struct ColorPickerModalView: View { } } - // View for RGB input fields + // view for RGB input fields private var RGBInputFields: some View { HStack(spacing: 8) { - RGBInputField(label: colorNames.red, value: $redComponent) - .onChange(of: redComponent) { _ in - setColor(updateColorFromRGB()) - } - - RGBInputField(label: colorNames.green, value: $greenComponent) - .onChange(of: greenComponent) { _ in - setColor(updateColorFromRGB()) - } - - RGBInputField(label: colorNames.blue, value: $blueComponent) - .onChange(of: blueComponent) { _ in - setColor(updateColorFromRGB()) - } + RGBInputField(value: $redComponent) { + colorNames.red + } + .onChange(of: redComponent) { _ in + setColor(updateColorFromRGB()) + } + + RGBInputField(value: $greenComponent) { + colorNames.green + } + .onChange(of: greenComponent) { _ in + setColor(updateColorFromRGB()) + } + + RGBInputField(value: $blueComponent) { + colorNames.blue + } + .onChange(of: blueComponent) { _ in + setColor(updateColorFromRGB()) + } } } - // Set the color based on the source of change + // set the color based on the source of change private func setColor(_ newColor: Color) { withAnimation(LuminareConstants.fastAnimation) { color = newColor } } - // Update the color from RGB components + // update the color from RGB components private func updateColorFromRGB() -> Color { Color( red: redComponent / 255.0, @@ -93,7 +108,7 @@ struct ColorPickerModalView: View { ) } - // Update components when the color changes + // update components when the color changes private func updateComponents(newValue: Color) { hexColor = newValue.toHex() let rgb = newValue.toRGB() @@ -102,3 +117,18 @@ struct ColorPickerModalView: View { blueComponent = rgb.blue } } + +#Preview { + LuminareSection { + ColorPickerModalView( + color: .constant(.accentColor), + hexColor: .constant("ffffff"), + colorNames: ( + red: Text("Red"), + green: Text("Green"), + blue: Text("Blue") + )) + } + .padding() + .frame(width: 300, height: 375) +} diff --git a/Sources/Luminare/Components/Color Picker/ColorSaturationBrightnessView.swift b/Sources/Luminare/Components/Color Picker/ColorSaturationBrightnessView.swift index f145bc7..3a155a6 100644 --- a/Sources/Luminare/Components/Color Picker/ColorSaturationBrightnessView.swift +++ b/Sources/Luminare/Components/Color Picker/ColorSaturationBrightnessView.swift @@ -7,7 +7,7 @@ import SwiftUI -// View for adjusting the lightness of a selected color +// view for adjusting the lightness of a selected color struct ColorSaturationBrightnessView: View { @Binding var selectedColor: Color @@ -74,12 +74,12 @@ struct ColorSaturationBrightnessView: View { } } - // Update the position of the circle based on user interaction + // update the position of the circle based on user interaction private func updateColor(_ location: CGPoint, _ viewSize: CGSize) { let adjustedX = max(0, min(location.x, viewSize.width)) let adjustedY = max(0, min(location.y, viewSize.height)) - // Only adjust brightness if dragging, to avoid overwriting with white or black + // only adjust brightness if dragging, to avoid overwriting with white or black if isDragging { let saturation = (adjustedX / viewSize.width) let brightness = 1 - (adjustedY / viewSize.height) @@ -96,7 +96,7 @@ struct ColorSaturationBrightnessView: View { } } - // Initialize the position of the circle based on the current color + // initialize the position of the circle based on the current color private func updateCirclePosition(_ viewSize: CGSize) { let hsb = selectedColor.toHSB() @@ -143,3 +143,10 @@ struct ColorPickerCircle: View { .animation(LuminareConstants.animation, value: [isHovering, isDragging]) } } + +#Preview { + LuminareSection { + ColorSaturationBrightnessView(selectedColor: .constant(.accentColor)) + } + .padding() +} diff --git a/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift b/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift index 0ee7ae4..b5fc08d 100644 --- a/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift +++ b/Sources/Luminare/Components/Color Picker/LuminareColorPicker.swift @@ -7,18 +7,20 @@ import SwiftUI -public struct LuminareColorPicker: View -where F: ParseableFormatStyle, F.FormatInput == String, F.FormatOutput == String { +public struct LuminareColorPicker: View +where R: View, G: View, B: View, F: ParseableFormatStyle, F.FormatInput == String, F.FormatOutput == String { + public typealias ColorNames = RGBColorNames + @Binding var currentColor: Color @State private var text: String @State private var showColorPicker = false - private let colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey) + private let colorNames: ColorNames private let format: F public init( - color: Binding, - colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey), + color: Binding, + colorNames: ColorNames, format: F ) { self._currentColor = color @@ -29,7 +31,7 @@ where F: ParseableFormatStyle, F.FormatInput == String, F.FormatOutput == String public init( color: Binding, - colorNames: (red: LocalizedStringKey, green: LocalizedStringKey, blue: LocalizedStringKey), + colorNames: ColorNames, parseStrategy: StringFormatStyle.Strategy = .hex(.lowercasedWithWell) ) where F == StringFormatStyle { self.init( @@ -83,9 +85,9 @@ where F: ParseableFormatStyle, F.FormatInput == String, F.FormatOutput == String LuminareColorPicker( color: .constant(.accentColor), colorNames: ( - red: .init("Red"), - green: .init("Green"), - blue: .init("Blue") + red: Text("Red"), + green: Text("Green"), + blue: Text("Blue") ), parseStrategy: .hex(.custom(true, "$")) ) diff --git a/Sources/Luminare/Components/Color Picker/RGBInputField.swift b/Sources/Luminare/Components/Color Picker/RGBInputField.swift index 5adb191..6a04966 100644 --- a/Sources/Luminare/Components/Color Picker/RGBInputField.swift +++ b/Sources/Luminare/Components/Color Picker/RGBInputField.swift @@ -7,14 +7,14 @@ import SwiftUI -// Custom input field for RGB values -struct RGBInputField: View { - var label: LocalizedStringKey +// custom input field for RGB values +struct RGBInputField